<Быстрее чем <=?
Я читаю книгу, где автор говорит, что if( a < 901 )
быстрее чем if( a <= 900 )
,
Не совсем так, как в этом простом примере, но есть небольшие изменения производительности сложного кода цикла. Я полагаю, что это связано с созданным машинным кодом на случай, если это правда.
17 ответов
Нет, это не будет быстрее на большинстве архитектур. Вы не указали, но на x86 все интегральные сравнения будут обычно реализованы в двух машинных инструкциях:
-
test
или жеcmp
инструкция, которая устанавливаетEFLAGS
- И
Jcc
(переход) инструкция, в зависимости от типа сравнения (и раскладки кода):jne
- прыгать, если не равно ->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
по сравнению с jge
инструкция. Оба займут одинаковое количество времени.
Я хотел бы обратиться к комментарию, что ничто не указывает на то, что различные инструкции перехода занимают одинаковое количество времени. На этот вопрос сложно ответить, но вот что я могу дать: в Справочнике по набору инструкций Intel все они сгруппированы по одной общей инструкции: Jcc
(Перейти, если условие выполнено). Такая же группировка выполняется вместе в Справочном руководстве по оптимизации, в Приложении C. Задержка и пропускная способность.
Задержка - количество тактов, которые требуются ядру выполнения для завершения выполнения всех мопов, которые формируют инструкцию.
Пропускная способность - количество тактов, необходимое для ожидания, пока порты выдачи не смогут снова принять ту же инструкцию. Для многих инструкций пропускная способность команды может быть значительно меньше, чем ее задержка
Значения для Jcc
являются:
Latency Throughput
Jcc N/A 0.5
со следующей сноской на Jcc
:
7) Выбор инструкций условного перехода должен основываться на рекомендации раздела 3.4.1 "Оптимизация прогнозирования ветвлений", чтобы улучшить предсказуемость ветвей. Когда ветви предсказаны успешно, задержка
jcc
фактически равен нулю.
Таким образом, ничто в документах Intel никогда не рассматривает один Jcc
инструкция по-другому, чем другие.
Если задуматься о фактической схеме, используемой для реализации инструкций, можно предположить, что в разных битах будут простые И / ИЛИ вентили. EFLAGS
, чтобы определить, выполнены ли условия. Таким образом, нет никаких причин, по которым инструкция, проверяющая два бита, должна занимать больше или меньше времени, чем одна проверка только одного (Игнорирование задержки распространения затвора, которая намного меньше, чем период времени).
Изменить: с плавающей точкой
Это верно и для плавающей запятой 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
Исторически (мы говорим о 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, но сегодня это практически не имеет значения.
Предполагая, что мы говорим о внутренних целочисленных типах, нет никакого способа, которым один мог бы быть быстрее чем другой. Они явно семантически идентичны. Они оба просят компилятор сделать то же самое. Только ужасно сломанный компилятор может сгенерировать худший код для одного из них.
Если бы была какая-то платформа, где <
был быстрее чем <=
для простых целочисленных типов компилятор всегда должен преобразовывать <=
в <
для постоянных. Любой компилятор, который не был бы просто плохим компилятором (для этой платформы).
Я вижу, что ни один не быстрее. Компилятор генерирует один и тот же машинный код в каждом условии с разным значением.
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
Для кода с плавающей запятой сравнение <= действительно может быть медленнее (на одну инструкцию) даже на современных архитектурах. Вот первая функция:
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
нужно оба оценивать как ложные.
Может быть, автор этой безымянной книги читал, что a > 0
работает быстрее чем a >= 1
и думает, что это правда универсально.
Но это потому, что 0
участвует (потому что CMP
может, в зависимости от архитектуры, заменить, например, на OR
) а не из-за <
,
По крайней мере, если бы это было так, компилятор мог бы тривиально оптимизировать a <= b к!(A> b), и поэтому даже если бы само сравнение было на самом деле медленнее со всеми, кроме самого наивного компилятора, вы бы не заметили разницу,
TL; DR ответ
Для большинства комбинаций архитектуры, компилятора и языка это не будет быстрее.
Полный ответ
Другие ответы были сконцентрированы на архитектуре x86, и я не знаю архитектуру ARM (как, кажется, ваш пример ассемблера) достаточно хорошо, чтобы комментировать конкретно сгенерированный код, но это пример микрооптимизации, которая очень архитектурна конкретный, и с такой же вероятностью будет антиоптимизацией, как и оптимизацией.
Таким образом, я бы предположил, что такого рода микрооптимизация является примером программирования культового груза, а не лучшей практикой разработки программного обеспечения.
Вероятно, есть некоторые архитектуры, где это является оптимизацией, но я знаю, по крайней мере, одну архитектуру, где может быть верно обратное. В почтенной архитектуре Транспутера инструкции машинного кода были только равны и больше или равны, поэтому все сравнения должны были быть построены из этих примитивов.
Даже тогда, почти во всех случаях, компилятор мог упорядочить инструкции оценки таким образом, чтобы на практике ни одно сравнение не имело никакого преимущества перед любым другим. В худшем случае, возможно, потребуется добавить обратную инструкцию (REV), чтобы поменять местами два верхних элемента в стеке операндов. Это была однобайтовая инструкция, для выполнения которой требовался один цикл, поэтому были наименьшие возможные издержки.
Является ли микрооптимизация, подобная этой, оптимизацией или антиоптимизацией, зависит от конкретной архитектуры, которую вы используете, поэтому обычно плохая идея привыкнуть к использованию микрооптимизаций, специфичных для архитектуры, иначе вы можете инстинктивно используйте один, когда это неуместно, и, похоже, именно это и защищает книга, которую вы читаете.
У них одинаковая скорость. Может быть, в какой-то особой архитектуре то, что он / она сказал, правильно, но по крайней мере в семействе x86 я знаю, что они одинаковы. Потому что для этого процессор будет делать вычитание (a - b) и затем проверять флаги регистра флагов. Два бита этого регистра называются ZF (нулевой флаг) и SF (флаг знака), и это делается за один цикл, потому что он будет делать это с одной операцией маски.
Это будет сильно зависеть от базовой архитектуры, в которую компилируется Си. Некоторые процессоры и архитектуры могут иметь явные инструкции для равных или меньших и равных, которые выполняются за разное количество циклов.
Это было бы довольно необычно, поскольку компилятор мог бы обойти это, делая его неактуальным.
Вы не должны быть в состоянии заметить разницу, даже если она есть. Кроме того, на практике вам придется сделать дополнительный a + 1
или же a - 1
чтобы сохранить условие, если вы не собираетесь использовать магические константы, что является очень плохой практикой во всех отношениях.
Когда я писал этот ответ, я смотрел только на заглавный вопрос о a <= 900
, Многие компиляторы всегда уменьшают величину констант путем преобразования между <
а также <=
Например, потому что непосредственный операнд x86 имеет более короткую 1-байтовую кодировку для -128..127.
Для ARM и особенно для AArch64 возможность кодирования как непосредственного зависит от возможности поворота узкого поля в любую позицию в слове. Так cmp w0, #0x00f000
будет кодироваться, в то время как cmp w0, #0x00effff
не может быть. Таким образом, правило "сделай это меньше" для сравнения с константой времени компиляции не всегда применимо к AArch64.
На языке ассемблера на большинстве машин сравнение для <=
имеет ту же стоимость, что и для сравнения <
, Это применимо, независимо от того, ветвитесь ли вы на нем, логизируете его для создания целого числа 0/1 или используете его в качестве предиката для операции выбора без ответвлений (например, CMOV x86). Другие ответы касались только этой части вопроса.
Но этот вопрос касается операторов C++, входных данных для оптимизатора. Обычно они оба одинаково эффективны; совет из книги звучит совершенно фиктивно, потому что компиляторы всегда могут преобразовать сравнение, которое они реализуют в asm. Но есть по крайней мере одно исключение, когда использование <=
может случайно создать что-то, что не может оптимизировать компилятор.
В качестве условия цикла существуют случаи, когда <=
качественно отличается от <
, когда он останавливает компилятор доказать, что цикл не является бесконечным. Это может иметь большое значение, отключая автоматическую векторизацию.
Неподписанное переполнение четко определено как перестановка по основанию 2, в отличие от подписанного переполнения (UB). Счетчики циклов со знаком, как правило, защищены от этого с помощью компиляторов, которые оптимизируют на основе UB со знаком переполнения: ++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
всегда верно. Так что этот цикл бесконечен для size=0
и компилятор должен учитывать это, даже если вы, как программист, вероятно, никогда не намереваетесь передать size=0. Если компилятор может встроить эту функцию в вызывающую функцию, где он может доказать, что size = 0 невозможен, то это здорово, он может оптимизировать, как и для i < size
,
Как if(!size) skip the loop;
do{...}while(--size);
это один обычно эффективный способ оптимизировать for( i<size )
цикл, если фактическое значение i
не требуется внутри цикла ( Почему циклы всегда компилируются в стиле "do...while" (прыжок в хвост)?).
Но это {} пока не может быть бесконечным: если введено с size==0
мы получаем 2^n итераций. ( Итерация по всем целым числам без знака в цикле for C позволяет выразить цикл по всем целым числам без знака, включая ноль, но без флага переноса это непросто, как в asm.)
Учитывая возможность оборачивания счетчика циклов, современные компиляторы часто просто "сдаются" и оптимизируют их не так агрессивно.
Пример: сумма целых чисел от 1 до n
Использование неподписанного 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;
}
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/1 или используете его в качестве предиката для операции выбора без ответвлений (например, CMOV x86). Другие ответы касались только этой части вопроса.
Но этот вопрос касается операторов C++, входных данных для оптимизатора. Обычно они оба одинаково эффективны; совет из книги звучит совершенно фиктивно, потому что компиляторы всегда могут преобразовать сравнение, которое они реализуют в asm. Но есть по крайней мере одно исключение, когда использование <=
может случайно создать что-то, что не может оптимизировать компилятор.
В качестве условия цикла существуют случаи, когда <=
качественно отличается от <
, когда он останавливает компилятор доказать, что цикл не является бесконечным. Это может иметь большое значение, отключая автоматическую векторизацию.
Неподписанное переполнение четко определено как перестановка по основанию 2, в отличие от подписанного переполнения (UB). Счетчики циклов со знаком, как правило, защищены от этого с помощью компиляторов, которые оптимизируют на основе UB со знаком переполнения: ++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
всегда верно. Так что этот цикл бесконечен для size=0
и компилятор должен учитывать это, даже если вы, как программист, вероятно, никогда не намереваетесь передать size=0. Если компилятор может встроить эту функцию в вызывающую функцию, где он может доказать, что size = 0 невозможен, то это здорово, он может оптимизировать, как и для i < size
,
Как if(!size) skip the loop;
do{...}while(--size);
это один обычно эффективный способ оптимизировать for( i<size )
цикл, если фактическое значение i
не требуется внутри цикла ( Почему циклы всегда компилируются в стиле "do...while" (прыжок в хвост)?).
Но это {} пока не может быть бесконечным: если введено с size==0
мы получаем 2^n итераций. ( Итерация по всем целым числам без знака в цикле for C позволяет выразить цикл по всем целым числам без знака, включая ноль, но без флага переноса это непросто, как в asm.)
Учитывая возможность оборачивания счетчика циклов, современные компиляторы часто просто "сдаются" и оптимизируют их не так агрессивно.
Пример: сумма целых чисел от 1 до n
Использование неподписанного 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;
}
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, поэтому он не использует окончательное значение счетчика цикла.
Вы могли бы сказать, что строка является правильной в большинстве языков сценариев, так как дополнительный символ приводит к немного более медленной обработке кода. Однако, как указывалось в самом верхнем ответе, это не должно иметь никакого эффекта в C++, и все, что делается с помощью языка сценариев, вероятно, не имеет отношения к оптимизации.
Только если люди, создавшие компьютеры, плохо разбираются в логической логике. Чего им быть не должно.
Каждое сравнение (>=
<=
>
<
) можно делать с той же скоростью.
Каждое сравнение - это просто вычитание (разница) и определение положительного / отрицательного результата.
(Еслиmsb
установлено, число отрицательное)
Как проверить a >= b
? Suba-b >= 0
Проверить, если a-b
положительный.
Как проверитьa <= b
? Sub0 <= b-a
Проверить, если b-a
положительный.
Как проверитьa < b
? Suba-b < 0
Проверить, если a-b
отрицательный.
Как проверитьa > b
? Sub0 > b-a
Проверить, если b-a
отрицательный.
Проще говоря, компьютер может просто сделать это под капотом для данной операции:
a >= b
== msb(a-b)==0
a <= b
== msb(b-a)==0
a > b
== msb(b-a)==1
a < b
== msb(a-b)==1
и, конечно же, компьютеру не нужно ==0
или ==1
или.
для==0
он мог просто перевернуть msb
из схемы.
Во всяком случае, они бы наверняка не сделали a >= b
рассчитываться как a>b || a==b
лол
В C и C++ важным правилом для компилятора является правило «как если бы»: если выполнение X ведет себя точно так же, как если бы вы выполняли Y, то компилятор свободен выбирать, какое из них использовать.
В вашем случае «a < 901» и «a <= 900» всегда дают один и тот же результат, поэтому компилятор может компилировать любую версию. Если по какой-то причине одна версия была быстрее, то любой качественный компилятор будет создавать код для более быстрой версии. Таким образом, если ваш компилятор не создает исключительно плохой код, обе версии будут работать с одинаковой скоростью.
Теперь, если у вас была ситуация, когда два бита кода всегда будут давать один и тот же результат, но компилятору трудно доказать, и/или компилятору трудно доказать, какая версия быстрее, вы можете получить разный код работает с разной скоростью.
PS Исходный пример может работать на разных скоростях, если процессор поддерживает однобайтовые константы (быстрее) и многобайтовые константы (медленнее), поэтому сравнение с 255 (1 байт) может быть быстрее, чем сравнение с 256 (два байта). Я ожидаю, что компилятор сделает все, что быстрее.
Только если путь вычислений зависит от данных:
a={1,1,1,1,1000,1,1,1,1}
while (i<=4)
{
for(j from 0 to a[i]){ do_work(); }
i++;
}
будет вычислять в 250 раз больше, чем
while(i<4)
Примером из реального мира будет вычисление множества Мандельброта. Если вы включите пиксель, который повторяется 1000000 раз, это вызовет отставание, но совпадение с
<=
вероятность использования слишком мала.
На самом деле они будут иметь одинаковую скорость, потому что на уровне сборки они оба занимают одну строку. Такие как:
jl ax,dx
(прыгает, если AX меньше DX)jle ax,dx
(прыгает, если AX меньше или равен DX)
Так что нет, ни быстрее. Но если вы хотите получить действительно техническую информацию, я думаю, если бы вы проверили ее на уровне электронного тока, он был бы немного быстрее, но не ближе к скорости, которую вы заметили бы.