Является <быстрее, чем <=?

Я читаю книгу, в которой автор говорит, что if( a < 901 ) быстрее, чем if( a <= 900 ).

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

1276
задан Vinícius Magalhães Horta 27 авг. '12 в 5:10
источник поделиться

13 ответов

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

  • A test или cmp, которая устанавливает EFLAGS
  • И Jcc (переход), в зависимости от типа сравнения (и макета кода):
    • jne - Jump if not equal → ZF = 0
    • jz - Перейти, если ноль (равный) → ZF = 1
    • jg - Перейти, если больше → ZF = 0 and SF = OF
    • (и т.д...)

Пример (Отредактировано для краткости) Скомпилировано с помощью $ gcc -m32 -S -masm=intel test.c

    if (a < b) {
        // Do something 1
    }

Скомпилируется:

    mov     eax, DWORD PTR [esp+24]      ; a
    cmp     eax, DWORD PTR [esp+28]      ; b
    jge     .L2                          ; jump if a is >= b
    ; Do something 1
.L2:

и

    if (a <= b) {
        // Do something 2
    }

Скомпилируется:

    mov     eax, DWORD PTR [esp+24]      ; a
    cmp     eax, DWORD PTR [esp+28]      ; b
    jg      .L5                          ; jump if a is > b
    ; Do something 2
.L5:

Таким образом, единственное различие между ними - это инструкция jg против a jge. Эти два будут занимать одинаковое количество времени.


Я хотел бы обратиться к комментарию, что ничто не указывает на то, что разные инструкции перехода занимают одинаковое количество времени. Это немного сложно ответить, но вот что я могу дать: В Справочник по наборам инструкций Intel все они сгруппированы по одной общей инструкции, Jcc (Перейти, если условие выполнено). Та же группировка составлена ​​в Справочном руководстве по оптимизации, в Приложении C. Задержка и пропускная способность.

Задержка. - Количество тактовых циклов, которые необходимы для ядро выполнения для завершения выполнения всех μops, которые формируют инструкция.

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

Значения для Jcc:

      Latency   Throughput
Jcc     N/A        0.5

со следующей сноской на Jcc:

7). Выбор инструкций условного перехода должен основываться на рекомендации раздела 3.4.1 "Оптимизация прогноза ветвей" для улучшения предсказуемости веток. Когда ветки предсказаны успешно, латентность Jcc равна нулю.

Итак, ничто в документах Intel никогда не рассматривает одну инструкцию Jcc по-другому, чем другие.

Если вы думаете о фактической схеме, используемой для реализации инструкций, можно предположить, что для разных битов в EFLAGS были бы установлены простые логики AND/OR на разных битах, чтобы определить, выполнены ли условия. Тогда нет причин, по которым команда, тестирующая два бита, должна занимать больше или меньше времени, чем одно тестирование только одного (Игнорирование задержки распространения затвора, которое намного меньше периода синхронизации).


Изменить: плавающая точка

Это справедливо и для x87-плавающей запятой: (Довольно много того же кода, что и выше, но с double вместо int.)

        fld     QWORD PTR [esp+32]
        fld     QWORD PTR [esp+40]
        fucomip st, st(1)              ; Compare ST(0) and ST(1), and set CF, PF, ZF in EFLAGS
        fstp    st(0)
        seta    al                     ; Set al if above (CF=0 and ZF=0).
        test    al, al
        je      .L2
        ; Do something 1
.L2:

        fld     QWORD PTR [esp+32]
        fld     QWORD PTR [esp+40]
        fucomip st, st(1)              ; (same thing as above)
        fstp    st(0)
        setae   al                     ; Set al if above or equal (CF=0).
        test    al, al
        je      .L5
        ; Do something 2
.L5:
        leave
        ret
1507
ответ дан Jonathon Reinhart 27 авг. '12 в 5:13
источник поделиться

Исторически (мы говорим о 1980-х и начале 1990-х годов), были некоторые архитектуры, в которых это было правдой. Корневая проблема заключается в том, что целочисленное сравнение реализуется посредством целочисленных вычитаний. Это приводит к следующим случаям.

Comparison     Subtraction
----------     -----------
A < B      --> A - B < 0
A = B      --> A - B = 0
A > B      --> A - B > 0

Теперь, когда A < B, вычитание должно занять высокий бит для правильного вычитания, так же, как вы переносите и занимаете при добавлении и вычитании вручную. Этот "заимствованный" бит обычно упоминается как бит переноса и может быть проверен инструкцией по ветвлению. Второй бит, называемый нулевым битом, будет установлен, если вычитание будет тождественно равным нулю, что подразумевает равенство.

Обычно были как минимум две условные инструкции ветвления, одна для ветвления на бит переноса и одна на нулевом бите.

Теперь, чтобы понять суть вопроса, позвольте развернуть предыдущую таблицу, чтобы включить результаты переноса и нулевого бита.

Comparison     Subtraction  Carry Bit  Zero Bit
----------     -----------  ---------  --------
A < B      --> A - B < 0    0          0
A = B      --> A - B = 0    1          1
A > B      --> A - B > 0    1          0

Итак, реализация ветки для A < B может быть выполнена в одной команде, потому что бит переноса является ясным только в этом случае, то есть

;; Implementation of "if (A < B) goto address;"
cmp  A, B          ;; compare A to B
bcz  address       ;; Branch if Carry is Zero to the new address

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

;; Implementation of "if (A <= B) goto address;"
cmp A, B           ;; compare A to B
bcz address        ;; branch if A < B
bzs address        ;; also, Branch if the Zero bit is Set

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

535
ответ дан Lucas 27 авг. '12 в 20:53
источник поделиться

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

Если была некоторая платформа, где < была быстрее, чем <= для простых целых типов, компилятор всегда должен преобразовывать <= в < для констант. Любой компилятор, который не просто был бы плохим компилятором (для этой платформы).

75
ответ дан David Schwartz 27 авг. '12 в 5:31
источник поделиться

Я вижу, что это не так. Компилятор генерирует один и тот же машинный код в каждом условии с другим значением.

if(a < 901)
cmpl  $900, -4(%rbp)
jg .L2

if(a <=901)
cmpl  $901, -4(%rbp)
jg .L3

Мой пример if - это GCC на платформе x86_64 на Linux.

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

Я заметил, что если он не является константой, то тот же машинный код генерируется в любом случае.

int b;
if(a < b)
cmpl  -4(%rbp), %eax
jge   .L2

if(a <=b)
cmpl  -4(%rbp), %eax
jg .L3
64
ответ дан Adrian Cornish 27 авг. '12 в 5:16
источник поделиться

Для кода с плавающей точкой сравнение <= действительно может быть медленнее (по одной инструкции) даже на современных архитектурах. Здесь первая функция:

int compare_strict(double a, double b) { return a < b; }

В PowerPC сначала выполняется сравнение с плавающей запятой (которое обновляет cr, регистр условий), а затем переводит регистр условий в GPR, сдвигает бит "сравнивается меньше", а затем возвращается. Он принимает четыре инструкции.

Теперь рассмотрим эту функцию:

int compare_loose(double a, double b) { return a <= b; }

Для этого требуется такая же работа, как compare_strict выше, но теперь есть два бита интереса: "было меньше" и "было равно". Для этого требуется дополнительная команда (cror - регистр условия побитовое ИЛИ), чтобы объединить эти два бита в один. Поэтому compare_loose требуется пять инструкций, а compare_strict - четыре.

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

int compare_loose(double a, double b) { return ! (a > b); }

Однако это неправильно обрабатывает NaN. NaN1 <= NaN2 и NaN1 > NaN2 должны оцениваться как false.

48
ответ дан ridiculous_fish 27 авг. '12 в 21:32
источник поделиться

Возможно, автор этой неназванной книги прочитал, что a > 0 работает быстрее, чем a >= 1, и считает, что это истинно универсально.

Но это связано с тем, что задействован 0 (поскольку CMP может, в зависимости от архитектуры, заменить, например, на OR), а не из-за <.

33
ответ дан glglgl 27 авг. '12 в 16:05
источник поделиться

По крайней мере, если бы это было так, то компилятор мог бы тривиально оптимизировать <= b to! (a > b), и поэтому даже если бы сравнение было фактически медленнее, со всеми, кроме самого наивного компилятора, не заметите разницы.

26
ответ дан Eliot Ball 27 авг. '12 в 12:23
источник поделиться

Они имеют одинаковую скорость. Возможно, в какой-то особой архитектуре, что он/она сказал правильно, но в семье x86, по крайней мере, я знаю, что они одинаковы. Потому что для этого CPU выполнит субстрат (a - b), а затем проверит флаги регистра флага. Два бита этого регистра называются ZF (нулевой флаг) и SF (флаг знака), и это выполняется за один цикл, потому что он будет делать это с одной операцией маски.

15
ответ дан Masoud 27 авг. '12 в 11:25
источник поделиться

Это будет сильно зависеть от базовой архитектуры, с которой компилируется C. Некоторые процессоры и архитектуры могут иметь явные инструкции для равных или меньших и равных, которые выполняются в разных количествах циклов.

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

12
ответ дан Telgin 27 авг. '12 в 5:15
источник поделиться

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

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

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

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

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

9
ответ дан Mark Booth 31 авг. '12 в 21:33
источник поделиться

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

6
ответ дан shinkou 27 авг. '12 в 5:17
источник поделиться

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

3
ответ дан Ecksters 29 авг. '12 в 5:47
источник поделиться

На самом деле, они будут точно такой же скоростью, потому что на уровне сборки они берут одну строку. Например:

  • jl ax,dx (перескакивает, если AX меньше DX)
  • jle ax,dx (прыгает, если AX меньше или равно DX)

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

-2
ответ дан Kevin Usher 31 авг. '12 в 17:59
источник поделиться

Другие вопросы по меткам