Дорогой прыжок с GCC 5.4.0

У меня была функция, которая выглядела так (показывала только важную часть):

double CompareShifted(const std::vector<uint16_t>& l, const std::vector<uint16_t> &curr, int shift, int shiftY)  {
...
  for(std::size_t i=std::max(0,-shift);i<max;i++) {
     if ((curr[i] < 479) && (l[i + shift] < 479)) {
       nontopOverlap++;
     }
     ...
  }
...
}

Написанная так, на моей машине эта функция заняла ~ 34 мс. После изменения условия на умножение логического типа (чтобы код выглядел так):

double CompareShifted(const std::vector<uint16_t>& l, const std::vector<uint16_t> &curr, int shift, int shiftY)  {
...
  for(std::size_t i=std::max(0,-shift);i<max;i++) {
     if ((curr[i] < 479) * (l[i + shift] < 479)) {
       nontopOverlap++;
     }
     ...
  }
...
}

время выполнения уменьшилось до ~ 19 мс.

Используемый компилятор - GCC 5.4.0 с -O3, и после проверки сгенерированный asm-код с использованием godbolt.org я обнаружил, что первый пример генерирует скачок, а второй - нет. Я решил попробовать GCC 6.2.0, который также генерирует инструкцию перехода при использовании первого примера, но GCC 7, похоже, больше не генерирует ее.

Обнаружение этого способа ускорения кода было довольно ужасным и заняло довольно много времени. Почему компилятор так себя ведет? Это задумано, и программисты должны на это обратить внимание? Есть ли еще что-нибудь похожее на это?


person Jakub Jůza    schedule 06.12.2016    source источник
comment
Почему компилятор так себя ведет? Компилятор может делать все, что хочет, если сгенерированный код правильный. Некоторые компиляторы просто лучше оптимизируют, чем другие.   -  person Jabberwocky    schedule 06.12.2016
comment
Также было бы полезно опубликовать полный пример с кодом, который действительно компилируется, и ссылку на страницы Godbolt ...   -  person Jens    schedule 06.12.2016
comment
Я предполагаю, что это вызывает оценка короткого замыкания &&.   -  person Jens    schedule 06.12.2016
comment
Обратите внимание, что именно поэтому у нас также есть &.   -  person rubenvb    schedule 06.12.2016
comment
@Jakub нет: второе условие не должно оцениваться, если первое условие ложно. Обратите внимание на слово «если» в этом предложении, которое напрямую переводится как «прыжок». Я действительно удивлен, что предсказатель ветвления ЦП не решает эту проблему. Вы случайно не отсортировали данные?   -  person rubenvb    schedule 06.12.2016
comment
да, действительно несортированный   -  person Jakub Jůza    schedule 06.12.2016
comment
Сортировка @Jakub, скорее всего, увеличит скорость выполнения, см. этот вопрос.   -  person rubenvb    schedule 06.12.2016
comment
@ JakubJůza Одна вещь, которую вы можете попробовать, - это использовать оптимизацию на основе профиля. Это должно помочь в предсказании ветвлений в реальном коде. Но если значения случайны, вы мало что можете сделать.   -  person Jens    schedule 06.12.2016
comment
изменение на (curr[i] < 479) & (l[i + shift] < 479) может еще больше повысить производительность   -  person phuclv    schedule 06.12.2016
comment
@rubenvb: Я подозреваю (глядя на имя функции), что сортировка массивов невозможна. Вектор реализует функцию индекса, а не просто содержит набор значений.   -  person Martin Bonner supports Monica    schedule 06.12.2016
comment
@rubenvb не должен оцениваться, на самом деле ничего не означает для выражения, которое не имеет побочных эффектов. Я подозреваю, что вектор выполняет проверку границ, и что GCC не может доказать, что он не выйдет за пределы. РЕДАКТИРОВАТЬ: На самом деле, я не думаю, что вы делаете что-нибудь, чтобы i + shift не выходил за пределы.   -  person Random832    schedule 06.12.2016
comment
@ Random832 это что-то значит: сравнение справа от && должно быть оценено, что означает выборку переменной из памяти. теперь, если утверждение было чем-то вроде if(pointer && pointer->something), то, что нельзя оценивать, действительно что-то означает. Здесь применимо то же самое: если operator[] нужно было сказать, получить доступ к диску или другим ресурсам, короткое замыкание (т. Е. Не вычисление выражения справа от &&) действительно желательно и полезно. Проверка границ тут ни при чем.   -  person rubenvb    schedule 06.12.2016
comment
@rubenvb Компилятору разрешено удалять лишний доступ к памяти. Таким образом, в примере OP разрешено поворачивать & или *. Теоретически он также может оптимизировать && в код без ветвления, но для этого потребуется доказать, что l[i + shift] не имеет побочных эффектов, в частности, что он не вызывает нарушение доступа к памяти, поэтому это выглядит довольно маловероятной оптимизацией.   -  person CodesInChaos    schedule 06.12.2016
comment
@CodesInChaos: нарушение доступа к памяти не является побочным эффектом. Это неопределенное поведение, и поэтому компилятор может предположить, что этого не происходит. Вероятно, проблема в том, что std::vector должен диагностировать и генерировать исключение для индексов, выходящих за границы.   -  person R.. GitHub STOP HELPING ICE    schedule 08.12.2016
comment
@R .. Я имел в виду то, что сказал. UB - это концепция, которая применяется к коду C, а не к сборке, которую генерирует компилятор C. Компилятор должен убедиться, что он не генерирует доступ к памяти, который вызывает нарушение прав доступа с правой стороны в случае короткого замыкания, поскольку код должен вести себя так, как будто доступа к памяти не происходит.   -  person CodesInChaos    schedule 08.12.2016
comment
@CodesInChaos: Думаю, я прочитал ваш комментарий задом наперед.   -  person R.. GitHub STOP HELPING ICE    schedule 08.12.2016
comment
На какой платформе вы компилируете? GCC отдает приоритет размеру кода, а не производительности, по крайней мере, на встроенных платформах. Я никогда не проверял это на ПК, но ожидал, что будет так же. GCC используется в качестве основного компилятора на многих платформах с очень ограниченным пространством кода, поэтому имеет смысл попытаться максимально уменьшить размер кода и сделать оптимизацию производительности второй проблемой. В большинстве приложений выбор в первую очередь производительности там, где это применимо, не приведет к огромной разнице в общей производительности, но приведет к увеличению затрат там, где вам понадобится микроконтроллер с большим объемом памяти для хранения кода.   -  person Drunken Code Monkey    schedule 08.12.2016
comment
Почему a*b != 0 быстрее, чем a != 0 && b != 0?   -  person phuclv    schedule 20.03.2021


Ответы (4)


Логический оператор И (&&) использует оценку короткого замыкания, что означает, что второй тест выполняется только в том случае, если первое сравнение оценивается как истина. Часто это именно та семантика, которая вам нужна. Например, рассмотрим следующий код:

if ((p != nullptr) && (p->first > 0))

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

Также возможно, что оценка короткого замыкания дает прирост производительности в тех случаях, когда оценка условий является дорогостоящим процессом. Например:

if ((DoLengthyCheck1(p) && (DoLengthyCheck2(p))

Если DoLengthyCheck1 не удается, нет смысла вызывать DoLengthyCheck2.

Однако в результирующем двоичном файле операция короткого замыкания часто приводит к двум ветвям, поскольку это самый простой способ для компилятора сохранить эту семантику. (Вот почему, с другой стороны, оценка короткого замыкания может иногда подавить потенциал оптимизации.) Вы можете убедиться в этом, посмотрев на соответствующую часть объектного кода, сгенерированную для вашего оператора if GCC 5.4:

    movzx   r13d, WORD PTR [rbp+rcx*2]
    movzx   eax,  WORD PTR [rbx+rcx*2]

    cmp     r13w, 478         ; (curr[i] < 479)
    ja      .L5

    cmp     ax, 478           ; (l[i + shift] < 479)
    ja      .L5

    add     r8d, 1            ; nontopOverlap++

Здесь вы видите два сравнения (cmp инструкции), каждое из которых сопровождается отдельным условным переходом / ветвью (ja, или переходом, если указано выше).

Основное правило гласит, что ветви медленные, поэтому их следует избегать в виде узких петель. Это было верно практически для всех процессоров x86, начиная с скромного 8088 (у которого медленное время выборки и чрезвычайно малая очередь предварительной выборки [сопоставимая с кешем инструкций] в сочетании с полным отсутствием предсказания ветвления, означало, что взятые ветки требовали сброса кеша. ) до современных реализаций (чьи длинные конвейеры делают неверно предсказанные ответвления столь же дорогими). Обратите внимание на небольшую оговорку, которую я здесь сделал. Современные процессоры, начиная с Pentium Pro, имеют усовершенствованные механизмы прогнозирования переходов, которые позволяют минимизировать затраты на переходы. Если направление ветки можно правильно спрогнозировать, затраты будут минимальными. В большинстве случаев это работает хорошо, но если вы попадаете в патологические случаи, когда предсказатель ветвления не на вашей стороне, ваш код может работать очень медленно. Предположительно, это то место, где вы находитесь, поскольку вы говорите, что ваш массив не отсортирован.

Вы говорите, что тесты подтвердили, что замена && на * делает код заметно быстрее. Причина этого очевидна, когда мы сравниваем соответствующую часть объектного кода:

    movzx   r13d, WORD PTR [rbp+rcx*2]
    movzx   eax,  WORD PTR [rbx+rcx*2]

    xor     r15d, r15d        ; (curr[i] < 479)
    cmp     r13w, 478
    setbe   r15b

    xor     r14d, r14d        ; (l[i + shift] < 479)
    cmp     ax, 478
    setbe   r14b

    imul    r14d, r15d        ; meld results of the two comparisons

    cmp     r14d, 1           ; nontopOverlap++
    sbb     r8d, -1

Это немного противоречит интуиции, что это могло быть быстрее, поскольку здесь есть больше инструкций, но иногда оптимизация работает именно так. Вы видите здесь те же сравнения (cmp), но теперь каждому из них предшествует xor, а за ним - setbe. XOR - это просто стандартный прием для очистки регистра. setbe - это инструкция x86, которая устанавливает бит в зависимости от значения флага и часто используется для реализации кода без ветвей. Здесь setbe - это обратное значение ja. Он устанавливает свой целевой регистр в 1, если сравнение было ниже или равно (поскольку регистр был предварительно обнулен, в противном случае он будет равен 0), тогда как ja разветвлен, если сравнение было выше. Как только эти два значения были получены в регистрах r15b и r14b, они умножаются вместе с помощью imul. Умножение традиционно было относительно медленной операцией, но на современных процессорах оно чертовски быстро, и это будет особенно быстро, потому что оно умножает только два байтовых значения.

С таким же успехом вы могли бы заменить умножение побитовым оператором И (&), который не выполняет оценку короткого замыкания. Это делает код более понятным и обычно распознается компиляторами. Но когда вы делаете это со своим кодом и компилируете его с GCC 5.4, он продолжает генерировать первую ветвь:

    movzx   r13d, WORD PTR [rbp+rcx*2]
    movzx   eax,  WORD PTR [rbx+rcx*2]

    cmp     r13w, 478         ; (curr[i] < 479)
    ja      .L4

    cmp     ax, 478           ; (l[i + shift] < 479)
    setbe   r14b

    cmp     r14d, 1           ; nontopOverlap++
    sbb     r8d, -1

Нет никакой технической причины, по которой он должен был генерировать код таким образом, но по какой-то причине его внутренняя эвристика говорит ему, что это быстрее. , вероятно, был бы быстрее, если бы предсказатель ветвления был на вашей стороне, но, скорее всего, он будет медленнее, если предсказание ветвления будет чаще неудачным, чем успешным.

Новые поколения компиляторов (и других компиляторов, таких как Clang) знают это правило и иногда используют его для генерации того же кода, который вы искали бы при ручной оптимизации. Я регулярно вижу, как Clang переводит && выражения в тот же код, который был бы выдан, если бы я использовал &. Ниже приведен соответствующий вывод GCC 6.2 с вашим кодом с использованием обычного оператора &&:

    movzx   r13d, WORD PTR [rbp+rcx*2]
    movzx   eax,  WORD PTR [rbx+rcx*2]

    cmp     r13d, 478         ; (curr[i] < 479)
    jg      .L7

    xor     r14d, r14d        ; (l[i + shift] < 479)
    cmp     eax, 478
    setle   r14b

    add     esi, r14d         ; nontopOverlap++

Обратите внимание, насколько это умно ! В нем используются условия со знаком (jg и setle), а не беззнаковые условия (ja и setbe), но это не важно. Вы можете видеть, что он по-прежнему выполняет сравнение и ветвление для первого условия, как и более старая версия, и использует ту же инструкцию setCC для генерации кода без ветвления для второго условия, но он стал намного более эффективным в том, как он выполняет приращение. Вместо того, чтобы выполнять второе избыточное сравнение для установки флагов для операции sbb, он использует знание того, что r14d будет либо 1, либо 0, чтобы просто безоговорочно добавить это значение к nontopOverlap. Если r14d равно 0, то добавление не выполняется; в противном случае он добавляет 1, как и положено.

GCC 6.2 фактически создает более эффективный код, когда вы используете сокращающий оператор &&, чем побитовый оператор &:

    movzx   r13d, WORD PTR [rbp+rcx*2]
    movzx   eax,  WORD PTR [rbx+rcx*2]

    cmp     r13d, 478         ; (curr[i] < 479)
    jg      .L6

    cmp     eax, 478          ; (l[i + shift] < 479)
    setle   r14b

    cmp     r14b, 1           ; nontopOverlap++
    sbb     esi, -1

Ветвь и условный набор все еще существуют, но теперь он возвращается к менее умному способу увеличения nontopOverlap. Это важный урок, почему вы должны быть осторожны, пытаясь перехитрить свой компилятор!

Но если вы можете доказать с помощью тестов, что код ветвления на самом деле медленнее, тогда вам будет стоить попытаться перехитрить ваш компилятор. Вам просто нужно сделать это, внимательно изучив дизассемблер, и быть готовым пересмотреть свои решения при обновлении компилятора до более поздней версии. Например, имеющийся у вас код можно переписать как:

nontopOverlap += ((curr[i] < 479) & (l[i + shift] < 479));

Здесь вообще нет оператора if, и подавляющее большинство компиляторов никогда не подумают о создании кода ветвления для этого. GCC - не исключение; все версии генерируют что-то вроде следующего:

    movzx   r14d, WORD PTR [rbp+rcx*2]
    movzx   eax,  WORD PTR [rbx+rcx*2]

    cmp     r14d, 478         ; (curr[i] < 479)
    setle   r15b

    xor     r13d, r13d        ; (l[i + shift] < 479)
    cmp     eax, 478
    setle   r13b

    and     r13d, r15d        ; meld results of the two comparisons
    add     esi, r13d         ; nontopOverlap++

Если вы следовали предыдущим примерам, это должно показаться вам очень знакомым. Оба сравнения выполняются без ответвлений, промежуточные результаты and объединяются, а затем этот результат (который будет либо 0, либо 1) add привязан к nontopOverlap. Если вам нужен автономный код, это практически гарантирует, что вы его получите.

GCC 7 стал еще умнее. Теперь он генерирует практически идентичный код (за исключением некоторой небольшой перестановки инструкций) для вышеупомянутого трюка, что и исходный код. Итак, ответ на ваш вопрос: «Почему компилятор ведет себя так?», вероятно, потому, что они не идеальны! Они пытаются использовать эвристику для создания наиболее оптимального кода, но не всегда принимают наилучшие решения. Но, по крайней мере, со временем они могут стать умнее!

Один из способов взглянуть на эту ситуацию состоит в том, что код ветвления имеет лучшую производительность в лучшем случае. Если прогнозирование ветвления выполнено успешно, пропуск ненужных операций приведет к немного более быстрому выполнению. Однако безветвленный код имеет лучшую производительность в худшем случае. Если предсказание ветвления не удается, выполнение нескольких дополнительных инструкций, необходимых для предотвращения ветвления, будет определенно быстрее, чем неверно предсказанное ветвление. Даже самым умным и умным компиляторам будет нелегко сделать этот выбор.

И на ваш вопрос о том, следует ли программистам остерегаться этого, ответ почти наверняка отрицательный, за исключением определенных горячих циклов, которые вы пытаетесь ускорить с помощью микрооптимизации. Затем вы садитесь за разборку и находите способы ее настроить. И, как я сказал ранее, будьте готовы пересмотреть эти решения при обновлении до более новой версии компилятора, потому что он может либо сделать что-то глупое с вашим хитрым кодом, либо он, возможно, изменил свою эвристику оптимизации настолько, что вы можете вернуться использовать ваш исходный код. Комментируйте внимательно!

person Cody Gray    schedule 06.12.2016
comment
Из любопытства, насколько быстрее последний код (с &)? - person BЈовић; 06.12.2016
comment
Что ж, универсального лучше не бывает. Все зависит от вашей ситуации, поэтому вам абсолютно необходимо проводить тесты, когда вы выполняете такую ​​низкоуровневую оптимизацию производительности. Как я объяснил в ответе, если вы теряете размер предсказания ветвления, неверно предсказанные ветки будут замедлять ваш код на много. Последний фрагмент кода не использует никаких ветвей (обратите внимание на отсутствие инструкций j*), поэтому в этом случае он будет быстрее. [продолжение] - person Cody Gray; 06.12.2016
comment
Однако вы также должны быть осторожны, пытаясь универсализировать это правило, потому что, если предсказание ветвления будет успешным, этот код, вероятно, будет немного медленнее, просто потому, что он выполняет больше инструкций. Насколько значительным будет эффект, почти невозможно сказать без его измерения, и он очень сильно зависит от взаимодействия этого фрагмента кода с кодом вокруг него. Если процессор может сделать что-то из этого не по порядку, дополнительные инструкции не будут иметь большого значения. Если он находится на критическом пути, вы увидите замедление. Но в худшем случае все еще в порядке. @ BЈовић - person Cody Gray; 06.12.2016
comment
... скромный 8088 (у которого чрезвычайно маленький кеш инструкций и медленное время выборки в сочетании с полным отсутствием предсказания ветвлений означало, что для взятых веток требуется сброс кеша) - я не думаю, что 8088 имел кеш для сброса. - person 8bittree; 07.12.2016
comment
Учитывая, насколько это одобрено, жаль, что он упускает из виду тот факт, что короткое замыкание предотвращает некоторые оптимизации. (в частности, компилятор не может воспользоваться каким-либо неопределенным поведением, которое могло бы произойти в невычисленном выражении) - person ; 07.12.2016
comment
@ 8bit Боб прав. Я имел в виду очередь предварительной выборки. Наверное, мне не следовало называть это тайником, но я не особо беспокоился о формулировках и не тратил много времени, пытаясь вспомнить подробности, так как я не думал, что кого-то особо заботит, кроме исторического любопытства. Если вам нужны подробности, бесценен «Дзен языка ассемблера» Майкла Абраша. Вся книга доступна в различных местах в Интернете; вот применимая часть ветвления, но вы также должны прочитать и понять части, касающиеся предварительной загрузки. - person Cody Gray; 07.12.2016
comment
@Hurkyl Я чувствую, что весь ответ отвечает на этот вопрос. Вы правы, я не особо об этом говорил, но казалось, что это уже достаточно долго. :-) Любой, кто найдет время, чтобы прочитать всю книгу, должен получить достаточное понимание этого момента. Но если вы считаете, что чего-то не хватает или вам нужно больше разъяснений, не стесняйтесь редактировать ответ, чтобы включить его. Некоторым это не нравится, но я совершенно не против. Я добавил краткий комментарий по этому поводу вместе с модификацией моей формулировки, предложенной 8bittree. - person Cody Gray; 07.12.2016
comment
@CodyGray Вау !! Как я могу достичь такого уровня знаний? Есть ли у вас какие-либо указания на ресурсы, пути, то, что нужно и что нельзя делать по вашим стопам? - person green diod; 08.12.2016
comment
Ха, спасибо за дополнение, @green. Я не могу предложить ничего особенного. Как и во всем, вы становитесь экспертом, делая, видя и переживая. Я прочитал все, что смог достать, когда дело доходит до архитектуры x86, оптимизации, внутреннего устройства компилятора и других низкоуровневых вещей, и я все еще знаю лишь часть всего, что нужно знать. Лучший способ научиться - это копаться в грязных руках. Но прежде чем вы сможете даже надеяться начать, вам понадобится твердое понимание C (или C ++), указателей, языка ассемблера и всех других низкоуровневых основ. - person Cody Gray; 09.12.2016
comment
Большинство действительно хороших ресурсов старые. Zen of Assembly Абраша (связанный и упомянутый выше) фантастичен, но он с начала 1990-х и даже тогда обсуждался процессор, который уже был в значительной степени устаревшим (8088). Основы, которые вы можете извлечь из этого, по-прежнему неоценимы, в основном это часть дзен, научиться думать, но современные детали будут другими, так что их изучение еще не закончено. И все меньше интереса к миру веб-скриптовых языков. Мне трудно найти работу, в которой действительно использовались бы мои знания. - person Cody Gray; 09.12.2016
comment
Спасибо за ваш вклад! Я действительно ценю это! Я начал искать книгу Дантеманна, чтобы поиграть с чем-нибудь более свежим. Но я посмотрю книгу Абраша. - person green diod; 09.12.2016

Следует отметить одну важную вещь:

(curr[i] < 479) && (l[i + shift] < 479)

а также

(curr[i] < 479) * (l[i + shift] < 479)

семантически не эквивалентны! В частности, если у вас когда-либо была ситуация, когда:

  • 0 <= i и i < curr.size() оба верны
  • curr[i] < 479 ложно
  • i + shift < 0 или i + shift >= l.size() верно

тогда выражение (curr[i] < 479) && (l[i + shift] < 479) гарантированно будет четко определенным логическим значением. Например, это не вызывает ошибки сегментации.

Однако в этих обстоятельствах выражение (curr[i] < 479) * (l[i + shift] < 479) является неопределенным поведением; разрешено вызывать ошибку сегментации.

Это означает, что для исходного фрагмента кода, например, компилятор не может просто написать цикл, который выполняет как сравнения, так и операцию and, если только компилятор не докажет, что l[i + shift] никогда не вызовет segfault в ситуации, в которой это требуется, а не к.

Короче говоря, исходный фрагмент кода предлагает меньше возможностей для оптимизации, чем последний. (конечно, признает ли компилятор эту возможность - это совершенно другой вопрос)

Вы можете исправить исходную версию, вместо этого выполнив

bool t1 = (curr[i] < 479);
bool t2 = (l[i + shift] < 479);
if (t1 && t2) {
    // ...
person Community    schedule 06.12.2016
comment
Этот! В зависимости от значения shiftmax) здесь есть UB ... - person Matthieu M.; 08.12.2016

Оператор && реализует оценку короткого замыкания. Это означает, что второй операнд оценивается только в том случае, если первый имеет значение true. В этом случае это обязательно приведет к скачку.

Вы можете создать небольшой пример, чтобы показать это:

#include <iostream>

bool f(int);
bool g(int);

void test(int x, int y)
{
  if ( f(x) && g(x)  )
  {
    std::cout << "ok";
  }
}

Выходные данные ассемблера можно найти здесь.

Вы можете видеть, что сгенерированный код сначала вызывает f(x), затем проверяет вывод и переходит к оценке g(x), когда это было true. В противном случае он покидает функцию.

Вместо этого использование «логического» умножения заставляет вычислять оба операнда каждый раз и, таким образом, не требует перехода.

В зависимости от данных скачок может вызвать замедление, потому что он нарушает конвейер процессора и другие вещи, такие как спекулятивное выполнение. Обычно предсказание ветвления помогает, но если ваши данные случайны, их не так много можно предсказать.

person Jens    schedule 06.12.2016
comment
Почему вы утверждаете, что умножение вынуждает каждый раз вычислять оба операнда? 0 * x = x * 0 = 0 независимо от значения x. В качестве оптимизации компилятор может также сократить умножение. См. Например, stackoverflow.com/questions/8145894/. Более того, в отличие от оператора &&, умножение может производиться ленивым вычислением либо с первым, либо со вторым аргументом, что дает больше свободы для оптимизации. - person SomeWittyUsername; 06.12.2016
comment
@Jens - Обычно предсказание ветвления помогает, но если ваши данные случайны, их не так много можно предсказать. - дает хороший ответ. - person SChepurin; 06.12.2016
comment
@SomeWittyUsername Хорошо, компилятор, конечно, может выполнять любую оптимизацию, которая сохраняет наблюдаемое поведение. Это может или не может трансформировать его и не учитывать вычисления. если вы вычисляете 0 * f() и f имеет наблюдаемое поведение, компилятор должен его вызвать. Разница в том, что оценка короткого замыкания является обязательной для &&, но разрешена, если она может показать, что она эквивалентна для *. - person Jens; 06.12.2016
comment
@SomeWittyUsername только в тех случаях, когда значение 0 можно предсказать из переменной или константы. Думаю, таких случаев очень мало. Конечно, оптимизация не может быть проведена в случае OP, поскольку задействован доступ к массиву. - person Diego Sevilla; 06.12.2016
comment
@Jens: Оценка короткого замыкания не является обязательной. От кода требуется только поведение как если бы короткое замыкание; компилятору разрешено использовать любые средства для достижения результата. - person ; 06.12.2016
comment
@Jens Да, и в приведенном здесь примере наблюдаемое поведение не меняется (скорее всего, если у операторов [] и ‹нет какой-то странной перегрузки) - person SomeWittyUsername; 06.12.2016
comment
@DiegoSevilla Как вы думаете, почему существует ограничение на предсказание? - person SomeWittyUsername; 06.12.2016
comment
@Hurkyl Может быть, я не сказал об этом прямо, но всегда действует правило «как если бы». Если компилятор может доказать, что наблюдаемое поведение не меняется, он может делать что угодно. Но это не особенность «&&», умножения или чего-то еще. - person Jens; 07.12.2016

Это может быть связано с тем, что при использовании логического оператора && компилятор должен проверить два условия для успешного выполнения оператора if. Однако во втором случае, поскольку вы неявно преобразуете значение int в bool, компилятор делает некоторые предположения на основе передаваемых типов и значений вместе с (возможно) одним условием перехода. Также возможно, что компилятор полностью оптимизирует jmps с битовыми сдвигами.

person crezefire    schedule 06.12.2016
comment
Переход происходит из-за того, что второе условие оценивается тогда и только тогда, когда истинно первое. В противном случае код не должен оценивать его, поэтому компилятор не может оптимизировать это лучше и по-прежнему быть правильным (если только он не может вывести первый оператор, всегда будет истинным). - person rubenvb; 06.12.2016