Сегодня я начал экспериментировать с ветвлением, которое проверяет наличие двух логических значений. Я был почти уверен, что на каком-то уровне оптимизации они будут просто добавлены, а затем проверены, но это не относится к gcc и clang. Почему gcc не оптимизирует две проверки типа bool, заменяя их сложением и одной проверкой? Позвольте мне показать вам пример:

void test(bool a, bool b)
{ 
    // Branch 1
    if (a && b)
    {
        std::cout << "Branch 1";
    }

    // Branch 2
    if (static_cast<int>(a) + static_cast<int>(b))
    {
        std::cout << "Branch 2";
    }
}

Gcc (даже с максимальным уровнем оптимизации) генерирует следующий код для ветви 1:

test   dil,dil
je     400794 <test(bool, bool)+0x14>
test   sil,sil
jne    4007b0 <test(bool, bool)+0x30>

В то время как он генерирует следующий код для ветви 2:

movzx  ebx,bl
movzx  ebp,bpl
add    ebx,ebp
jne    4007cf <test(bool, bool)+0x4f>

Разве две ветви (test + je) не должны быть медленнее, чем добавление и ветвь (add + jne)?

Изменить: на самом деле я имел в виду умножение, потому что в случае истинного и ложного (1 + 0) сложение дает истину (1), но умножение дает правильный результат (0).

3
Elviss Strazdins 6 Янв 2017 в 18:12
1
Для начала, сложение - это не &&, а ||. Кроме того, хотя я не уверен в текущем стандарте C ++, но раньше представление было 0 для false и все остальное для true. Таким образом, у вас могут быть -1 и +1 для двух истинных значений, и их добавление не очень хорошо. Точно так же у вас могут быть 1 и 2, и побитовый and для них не подходит.
 – 
Jester
6 Янв 2017 в 18:18
6
@Jester: переменные C ++ bool могут быть только true или false думаю , что это верно и для значений C bool). При преобразовании в int, они должны преобразовать в 1 или 0 соответственно.
 – 
Martin Bonner supports Monica
6 Янв 2017 в 18:20
4
Сложение не ведет себя так же, как логическое и. Умножение - это то, что вы ищете.
 – 
eerorika
6 Янв 2017 в 18:21
1
Возможно, связано с этим случаем; GCC не оптимизируется так эффективно, как мог бы со значениями bool.
 – 
Cody Gray
6 Янв 2017 в 19:05
1
Вы не хотите умножения, @user. Так будет медленнее. Что не так с простым побитовым И? (Ничего. В этом нет ничего плохого. Это то, что вам следует использовать, если вы хотите принудительно провести оптимизацию.)
 – 
Cody Gray
6 Янв 2017 в 19:08

3 ответа

Почему gcc не оптимизирует две проверки типа bool, заменяя их сложением и одной проверкой?

Предлагаемая оптимизация неверна. сложение не является подходящей заменой для оператора &&, потому что первый будет оценивать как true, когда хотя бы (но не оба) одно условие это true.

Остается вопрос, как можно оптимизировать?

Стандарт C ++ гарантирует, что значения bool преобразуются в int с определенными значениями: false преобразуется в 0, а true преобразуется в 1. Следовательно, следующая конструкция является полностью юридическим эквивалентом (при условии, что и a, и b являются просто переменными bool):

if (a & b) // OK, integral promotions take place (bool ---> int)

Предполагая, что компилятор всегда хранит значения bool с одним и тем же внутренним представлением для true (например, 0x1) и false (0x0), условие можно напрямую преобразовать в x86 test инструкция:

test    sil, dil

Вот и самая сложная часть. По-видимому, компилятор GCC изменил свое поведение между основной веткой 4.6 и 4.7. Даже с явным приведением к int он сохраняет два отдельных перехода. Состояние:

if (static_cast<int>(a) & static_cast<int>(b))

Генерирует код (GCC 6.2.0 с уровнем оптимизации -O3):

test    dil, dil
je      .L1
test    sil, sil
je      .L1

С другой стороны, компиляторы ICC и MSVC 2015 выполняют «истинное» побитовое и:

movzx   eax, BYTE PTR b$[rsp]
test    al, BYTE PTR a$[rsp]
je  SHORT $LN6@main

Что также относится к GCC до версии 4.7 (GCC 4.6.3 с -O3):

movzx   edi, dil
test    esi, edi
2
Grzegorz Szpetkowski 6 Янв 2017 в 22:26
1
Мне кажется, что это упущенная возможность оптимизации как для GCC, так и для Clang. Как вы заметили, и ICC, и MSVC понимают это правильно и выдают простую инструкцию test. Кто-то должен отправить отчет о дефекте как для GCC, так и для Clang. Я не могу придумать ни одной причины, по которой было бы необходимо тестировать значения независимо, и это, конечно, не оптимально.
 – 
Cody Gray
8 Янв 2017 в 09:09

Вы забываете, что логические операторы выполняют оценку короткого замыкания.

Для логического и, если левая часть ложна, правая часть не оценивается.

-2
Some programmer dude 6 Янв 2017 в 18:17
4
Верно, но не имеет значения, поскольку оба уже являются логическими, нечего "оценивать", компилятор вполне мог бы оптимизировать это.
 – 
Jester
6 Янв 2017 в 18:19
2
Как порядок оценки влияет на мой код? Они проверены по порядку или просто добавлены, дадут тот же результат. Результаты не могут отличаться друг от друга, поэтому это можно оптимизировать.
 – 
Elviss Strazdins
6 Янв 2017 в 18:19

На уровне абстрактной машины && заставляет второе выражение не вычисляться, если первое ложно. По правилу «как если бы» компилятор мог выбрать оценку второго выражения в любом случае - если он сможет доказать, что имеет определенное поведение (или неопределенное поведение не будет иметь значения) и никаких побочных эффектов; однако авторы компилятора ясно решили, что это не стоит того.

Если вы не хотите сокращать текст, & может быть полезным (с комментарием).

6
Martin Bonner supports Monica 6 Янв 2017 в 18:17
Разве это не странно, что оптимизатор не видит, что короткое замыкание не требуется? Например. a && b даст тот же результат, что и a + b (или a * b), поэтому его можно использовать вместо этого.
 – 
Elviss Strazdins
6 Янв 2017 в 18:41
Они могут этого не делать, потому что делают это в общем. a && b && c && d && e && f сможет ничего не оценивать, кроме a, если a ложно, тогда как, как и в случае умножения, ему придется оценивать / умножать все результаты вместе.
 – 
NathanOliver
6 Янв 2017 в 18:49
1
@ElvissStrazdins: обратите внимание, что a && b не даст такого же результата, как a + b во всех возможных случаях. Рассмотрим a = true и b = false.
 – 
Grzegorz Szpetkowski
6 Янв 2017 в 18:53
2
Обратите внимание, что внутренне на нескольких платформах gcc заменяет a&&b на a&b раньше (см. -Fdump-tree-gimple). Затем серверная часть решает расширить (a&b)!=0 как 2 перехода, поскольку считает, что это в среднем более эффективно.
 – 
Marc Glisse
7 Янв 2017 в 12:47
1
Я до сих пор не понимаю, почему два прыжка будут быстрее. Вы подсчитываете стоимость расширения байта нулями? Это не обязательно: вы можете просто нажать test sil, dil + je. Как это могло быть медленнее, чем test sil, sil + je + test dil, dil + je?
 – 
Cody Gray
8 Янв 2017 в 15:05