AFAIK C++ гарантирует, что в выражении a-expr && b-expr
b-expr
не оценивается, если a-expr
равно false
. (Аналогично и для выражений or)
означает ли это, что в сгенерированном коде произойдет скачок, который очистит конвейер процессора? Или как можно/можно это предотвратить?
что, если выражения настолько просты, что глупое вычисление всех результатов будет быстрее, чем короткое замыкание? Может ли это быть обнаружено/использовано компиляторами?
Или программисту придется разделить все выражения на отдельные присваивания переменных и, возможно, использовать вместо них поразрядные логические операторы, чтобы предотвратить такие скачки?
Ветви не «очищают конвейер» с самого начала, даже если они неправильно предсказаны (что действительно вызывает беспокойство и может иметь значительные затраты, но это не «очистка конвейера»), в любом случае не на соответствующем процессоре.
Вы обеспокоены тем, что могут быть сценарии, в которых bool a = a-expr; bool b = b-expr; bool c= a-expr && b-expr;
работает лучше, чем bool c = a-expr && b-expr;
? Предположим, что так оно и было, тогда я ожидаю, что авторы компиляторов тоже об этом знают. В любом случае не стоит писать код с учетом таких микрооптимизаций.
Самый простой способ для компилятора избежать вычисления b-expr
— использовать условные инструкции, отличные от разветвлений. Например. CMOV
— «условный ход» на x86. Классический ARM имеет условные варианты практически каждой инструкции.
Но, как вы правильно заметили, оценка b-expr
также может быть верным подходом. Составители знают, что происходит как побочный эффект оценки b-expr
, и могут увидеть, что эти эффекты отсутствуют или безвредны.
Современные процессоры имеют довольно хорошие предсказатели ветвей, а это означает, что простые ветки, подобные этому, не так затратны, как раньше.
но условный ход по-прежнему требует предварительного расчета аргумента. Это может просто помочь исключить необходимый переход при условном присваивании, но не может исключить вычисление условия.
@vlad_tepesch: циклы ЦП довольно дешевы, доступ к памяти часто является более медленной операцией.
Компиляторы могут конвертировать &&
в &
и ||
в |
на bool
, когда стоимость перехода и возможное неправильное предсказание перехода больше, чем стоимость выполнения «бесполезных» вычислений на любом современном оборудовании, это делается только тогда, когда компилятор может сказать, что оценка «потраченной» ветки не имеет побочного эффекта. (по правилу «как если бы»).
возьмем следующие две функции
int foo(int a, int b)
{
if (a == 1 & b == 2)
{
return 1;
}
return 0;
}
int bar(int a, int b)
{
if (a == 1 && b == 2)
{
return 1;
}
return 0;
}
И gcc
, и clang
создают один и тот же код для обеих функций в -O1
, который не включает ветку, в то время как MSVC, похоже, все еще создает ветвь даже в O2
godbolt demo
эта оптимизация возможна только в том случае, если компилятор может убедиться, что эта операция не имеет побочного эффекта. Если вы вызываете библиотечную функцию, которая не встроена, эта оптимизация может не произойти, и в этом случае имеет смысл либо встроить небольшие функции в заголовке или вручную используйте побитовые операции вместо логических.
Стандарты C или C++ не гарантируют, что второе выражение не будет вычислено, они гарантируют, что вы не сможете обнаружить разницу.
Например, если гарантировано, что второе выражение не имеет видимых побочных эффектов, если первое выражение ложно, то можно вычислить оба выражения. Или можно было бы оценить части, которые не имеют побочных эффектов. if (x > 0 && a[i++] == 1) может оценить a[i], но должен увеличивать i только в том случае, если x > 0 истинно (если i гарантированно является допустимым индексом).
Для простых выражений вычисление обоих иногда допустимо, а иногда быстрее, и хорошие компиляторы сделают это в таких случаях. Если необходим условный переход, в современных процессорах используется много аппаратного обеспечения для снижения стоимости, часто до нуля.
Резюме: нет никаких причин избегать && и || в вашем исходном коде.
Компилятор работает по правилу «как будто». Он может делать с кодом что угодно, пока наблюдаемое поведение остается прежним. Не беспокойтесь об оптимизации компилятора, если только вы 1) не обнаружите, что ваша программа не соответствует требованиям к скорости, и 2) не профилируете свой код и не обнаружите, что логическое вычисление является узким местом в вашей программе.