<Быстрее чем <=?

1431

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

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

  • 130
    Я не вижу причин, по которым этот вопрос должен быть закрыт (и особенно не удален, как показывают голоса в настоящее время), учитывая его историческое значение, качество ответа и тот факт, что другие главные вопросы по эффективности остаются открытыми. Самое большее, это должно быть заблокировано. Кроме того, даже если сам вопрос дезинформирован / наивен, тот факт, что он появился в книге, означает, что первоначальная дезинформация существует где-то в «заслуживающих доверия» источниках, и поэтому этот вопрос конструктивен, поскольку помогает прояснить это.
  • 29
    Вы никогда не говорили нам, на какую книгу вы ссылаетесь.
Показать ещё 4 комментария
Теги:
performance
assembly
relational-operators

14 ответов

1528
Лучший ответ

Нет, это не будет быстрее для большинства архитектур. Вы не указали, но на 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
  • 230
    @Dyppl фактически jg и jnle одни и те же инструкции, 7F :-)
  • 0
    @JonathonReinhart Вы уверены, что ваш пример не наоборот? Т.е. не < скомпилировано в jg и <= в jge ?
Показать ещё 16 комментариев
575

Исторически (мы говорим о 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, но сегодня это почти не имеет значения.

  • 10
    Кроме того, архитектуры, такие как x86, реализуют инструкции, такие как jge , которые проверяют флаги «ноль» и «знак / перенос».
  • 131
    +1 за историческую перспективу.
Показать ещё 16 комментариев
89

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

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

  • 6
    +1 согласен Ни < ни <= имеют скорости, пока компилятор не решит, какую скорость они будут иметь. Это очень простая оптимизация для компиляторов, если учесть, что они, как правило, уже выполняют оптимизацию мертвого кода, оптимизацию хвостовых вызовов, поднятие цикла (иногда развертывание), автоматическое распараллеливание различных циклов и т. Д. Зачем тратить время на обдумывание преждевременной оптимизации ? Запустите прототип, профилируйте его, чтобы определить, где лежат наиболее существенные оптимизации, выполните эти оптимизации в порядке значимости и снова выполните профиль для измерения прогресса ...
  • 0
    Есть еще некоторые крайние случаи, когда сравнение с одним постоянным значением может быть медленнее при <=, например, когда преобразование из (a < C) в (a <= C-1) (для некоторой константы C ) приводит к тому, что C будет сложнее кодировать в наборе команд. Например, набор команд может быть способен представлять константы со знаком от -127 до 128 в компактной форме в сравнениях, но константы вне этого диапазона должны загружаться с использованием либо более длинной, более медленной кодировки, либо полностью другой инструкции. Таким образом, сравнение типа (a < -127) может не иметь прямого преобразования.
Показать ещё 2 комментария
63

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

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
  • 9
    Обратите внимание, что это относится к x86.
  • 0
    На самом деле - я должен был сказать это - но любой компилятор может быть достаточно умен для генерации этого кода
Показать ещё 21 комментарий
52

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

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.

  • 0
    К счастью, это не работает так на x86 (x87). fucomip устанавливает ZF и CF.
  • 3
    @JonathonReinhart: Я думаю , вы недоразумение , что делает PowerPC - состояние регистра cr эквивалентно флагам , как ZF и CF на x86. (Хотя CR более гибок.) О чем говорит плакат, так это о переносе результата в GPR: для этого требуется две инструкции на PowerPC, но в x86 есть инструкция условного перемещения.
Показать ещё 2 комментария
34

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

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

  • 1
    Конечно, в «отладочной» сборке, но для (a >= 1) медленного запуска (a > 0) потребуется плохой компилятор, поскольку первый может быть тривиально преобразован во второй с помощью оптимизатора.
  • 1
    @BeeOnRope Иногда меня удивляет, какие сложные вещи оптимизатор может оптимизировать, и какие простые вещи он не может сделать.
Показать ещё 1 комментарий
31

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

  • 0
    Почему! (A> b) является оптимизированной версией a <= b. Разве! (A> b) 2 операции в одном?
  • 4
    @AbhishekSingh NOT просто создается другой инструкцией ( je vs. jne )
15

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

13

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

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

  • 1
    ЕСЛИ была разница в циклах. 1) это не будет обнаружено 2) Любой компилятор, достойный своей соли, уже будет преобразовывать медленную форму в более быструю, не меняя смысла кода. Таким образом, полученная инструкция была бы идентична.
  • 0
    Согласитесь полностью, это будет довольно банальная и глупая разница в любом случае. Конечно, нечего упоминать в книге, которая должна быть независимой от платформы.
Показать ещё 1 комментарий
11

TL; DR ответ

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

Полный ответ

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

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

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

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

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

6

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

  • 1
    Что плохая практика? Увеличивать или уменьшать счетчик? Как вы храните индексную нотацию тогда?
  • 5
    Он имеет в виду, если вы делаете сравнение двух типов переменных. Конечно, это просто, если вы устанавливаете значение для цикла или чего-то еще. Но если у вас есть x <= y, а y неизвестно, было бы медленнее «оптимизировать» его до x <y + 1
Показать ещё 1 комментарий
3

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

  • 0
    Я несколько не согласен. В конкурентном программировании языки сценариев часто предлагают самое быстрое решение проблемы, но для получения правильного решения необходимо применять правильные методы (читай: оптимизация).
1

Когда я писал этот ответ, я рассматривал только заглавный вопрос о <vs. <= в целом, а не конкретный пример константы a < 901 против a <= 900. Многие компиляторы всегда уменьшают величину констант путем преобразования между < и <=, например, потому что непосредственный операнд x86 имеет более короткую 1-байтовую кодировку для -128.. 127.

Для ARM и особенно для AArch64 возможность кодирования как непосредственного зависит от возможности поворота узкого поля в любую позицию в слове. Таким образом, cmp w0, #0x00f000 будет кодируемым, а cmp w0, #0x00effff может и не быть. Таким образом, правило сравнения с константой времени компиляции не всегда применимо к AArch64.


<vs. <= в целом, в том числе для переменных во время выполнения

На языке ассемблера на большинстве машин сравнение для <= имеет ту же стоимость, что и сравнение для <. Это применимо, независимо от того, веткитесь ли вы на нем, логизируете его для создания целого числа 0/1 или используете его в качестве предиката для операции выбора без ответвлений (например, CMOV x86). Другие ответы касались только этой части вопроса.

Но этот вопрос касается операторов C++, входных данных для оптимизатора. Обычно они оба одинаково эффективны; совет из книги звучит совершенно фиктивно, потому что компиляторы всегда могут преобразовать сравнение, которое они реализуют в asm. Но есть по крайней мере одно исключение, когда использование <= может случайно создать что-то, что компилятор не может оптимизировать.

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

Неподписанное переполнение четко определено как обход по основанию 2, в отличие от подписанного переполнения (UB). Счетчики циклов со знаком, как правило, защищены от этого, поскольку компиляторы, которые оптимизируют на основе UB со ++i <= size переполнения, не происходят: ++i <= size всегда в конечном итоге станет ложным. (Что каждый программист C должен знать о неопределенном поведении)

void foo(unsigned size) {
    unsigned upper_bound = size - 1;  // or any calculation that could produce UINT_MAX
    for(unsigned i=0 ; i <= upper_bound ; i++)
        ...

Компиляторы могут оптимизировать только таким образом, чтобы сохранить (определенное и юридически наблюдаемое) поведение источника C++ для всех возможных входных значений, кроме тех, которые приводят к неопределенному поведению.

(Простой i <= size тоже создал бы проблему, но я думал, что вычисление верхней границы было более реалистичным примером случайного введения возможности бесконечного цикла для ввода, который вас не интересует, но который должен учитывать компилятор. )

В этом случае size=0 приводит к upper_bound=UINT_MAX, а i <= UINT_MAX всегда имеет значение true. Так что этот цикл бесконечен для size=0, и компилятор должен это учитывать, даже если вы, как программист, вероятно, никогда не намереваетесь передать size = 0. Если компилятор может встроить эту функцию в вызывающую функцию, где он может доказать, что size = 0 невозможен, то отлично, он может оптимизировать так же, как и для i < size.

Asm, как if(!size) skip the loop; do{...}while(--size); в do{...}while(--size); это один обычно эффективный способ оптимизировать цикл for( i<size ), если фактическое значение i не требуется внутри цикла (Почему циклы всегда компилируются в стиле "do... while" (переход через хвост)?).

Но это делает {}, хотя не может быть бесконечным: если введено с size==0, мы получим 2 ^ n итераций. (Итерация по всем целым числам без знака в цикле for C позволяет выразить цикл по всем целым числам без знака, включая ноль, но без флага переноса это нелегко, как в asm.)

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

Пример: сумма целых чисел от 1 до n

Использование unsigned i <= n побеждает распознавание идиома clang, которое оптимизирует циклы sum(1.. n) с замкнутой формой на основе формулы Гаусса n * (n+1)/2.

unsigned sum_1_to_n_finite(unsigned n) {
    unsigned total = 0;
    for (unsigned i = 0 ; i < n+1 ; ++i)
        total += i;
    return total;
}

unsigned sum_1_to_n(unsigned n) {%0A++++unsigned total = 0;%0A++++for (unsigned я = 0+; i<%3Dn+; ++i){%0A++++ total += i;%0A++++}%0A++++return total; } unsigned sum_1_to_n_finite(unsigned n) {%0A++++unsigned total = 0;%0A++++for (unsigned я = 0+; я < n%2B1+; ++i){%0A++++ total += i;%0A++++}%0A++++return total; } '),l:'5',n:'0',o:'C++ source #1',t:'0')),k:42.03135828865001,l:'4',m:100,n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:clang700,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'1',trim:'1'),fontScale:1.2899450879999999,lang:c++,libs:!(),options:'-O3 -Wall -Wextra -march=haswell',source:1),l:'5',n:'0',o:'x86-64+Clang 7.0.0+(Editor #1,+Compiler+#2)+C++',t:'0')),header:(),k:28.984320855674994,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:g82,filters:(b:'0',binary:'1',commentOnly:'0',demangle:'0',directives:'0',execute:'1',intel:'0',libraryCode:'1',trim:'1'),fontScale:1.2899450879999999,lang:c++,libs:!(),options:'-O3 -Wall -Wextra -fverbose-asm',source:1),l:'5',n:'0',o:'x86-64 gcc 8.2+(Editor #1,+Compiler+#1)+C++',t:'0')),header:(),k:28.984320855674994,l:'4',m:100,n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4 rel="nofollow noreferrer">x86-64 asm из clang7.0 и gcc8.2 в проводнике компилятора Godbolt

 # clang7.0 -O3 closed-form
    cmp     edi, -1       # n passed in EDI: x86-64 System V calling convention
    je      .LBB1_1       # if (n == UINT_MAX) return 0;  // C++ loop runs 0 times
          # else fall through into the closed-form calc
    mov     ecx, edi         # zero-extend n into RCX
    lea     eax, [rdi - 1]   # n-1
    imul    rax, rcx         # n * (n-1)             # 64-bit
    shr     rax              # n * (n-1) / 2
    add     eax, edi         # n + (stuff / 2) = n * (n+1) / 2   # truncated to 32-bit
    ret          # computed without possible overflow of the product before right shifting
.LBB1_1:
    xor     eax, eax
    ret

Но для наивной версии мы просто получаем тупую петлю от лязга.

unsigned sum_1_to_n_naive(unsigned n) {
    unsigned total = 0;
    for (unsigned i = 0 ; i<=n ; ++i)
        total += i;
    return total;
}
# clang7.0 -O3
sum_1_to_n(unsigned int):
    xor     ecx, ecx           # i = 0
    xor     eax, eax           # retval = 0
.LBB0_1:                       # do {
    add     eax, ecx             # retval += i
    add     ecx, 1               # ++1
    cmp     ecx, edi
    jbe     .LBB0_1            # } while( i<n );
    ret

GCC в любом случае не использует замкнутую форму, поэтому выбор условия цикла на самом деле не повредит; он автоматически векторизуется с добавлением целочисленного значения SIMD, параллельно выполняя значения 4 i в элементах регистра XMM.

# "naive" inner loop
.L3:
    add     eax, 1       # do {
    paddd   xmm0, xmm1    # vect_total_4.6, vect_vec_iv_.5
    paddd   xmm1, xmm2    # vect_vec_iv_.5, tmp114
    cmp     edx, eax      # bnd.1, ivtmp.14     # bound and induction-variable tmp, I think.
    ja      .L3 #,       # }while( n > i )

 "finite" inner loop
  # before the loop:
  # xmm0 = 0 = totals
  # xmm1 = {0,1,2,3} = i
  # xmm2 = set1_epi32(4)
 .L13:                # do {
    add     eax, 1       # i++
    paddd   xmm0, xmm1    # total[0..3] += i[0..3]
    paddd   xmm1, xmm2    # i[0..3] += 4
    cmp     eax, edx
    jne     .L13      # }while( i != upper_limit );

     then horizontal sum xmm0
     and peeled cleanup for the last n%3 iterations, or something.

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

Кстати, оба этих цикла тратят впустую инструкцию (и моп на процессорах семейства Sandybridge) на издержки цикла. sub eax,1/jnz вместо add eax,1/cmp/jcc будет более эффективным. 1 моп вместо 2 (после макро-слияния sub/jcc или cmp/jcc). Код после обоих циклов безоговорочно записывает EAX, поэтому он не использует окончательное значение счетчика цикла.

  • 0
    Хороший надуманный пример. А как насчет вашего другого комментария о возможном влиянии на исполнение ордеров из-за использования EFLAGS? Это чисто теоретически или действительно может случиться так, что JB приведет к лучшему конвейеру, чем JBE?
  • 0
    @rustyx: я комментировал это где-то под другим ответом? Компиляторы не собираются выдавать код, который вызывает частичные остановки флагов, и уж точно не для C < или <= . Но конечно, test ecx,ecx / bt eax, 3 / jbe будет перескакивать, если ZF установлен (ecx == 0) или если установлен CF (бит 3 EAX == 1), вызывая частичное срыв флага на большинстве процессоров потому что флаги, которые он читает, не все взяты из последней инструкции, чтобы написать какие-либо флаги. На семействе Сэндибридж он не останавливается, просто нужно вставить объединяющий элемент. cmp / test записывает все флаги, но bt оставляет ZF без изменений. felixcloutier.com/x86/bt
-8

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

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню