Является ли if( a < 901 )
быстрееif( a <= 900 )
.
Не совсем так, как в этом простом примере, но есть небольшие изменения производительности сложного кода цикла. Я полагаю, это связано с созданным машинным кодом на случай, если это правда.
c++
performance
assembly
relational-operators
выслеживающий
источник
источник
<
в два раза быстрее, чем набор текста<=
.Ответы:
Нет, это не будет быстрее на большинстве архитектур. Вы не указали, но на x86 все интегральные сравнения обычно будут реализованы в двух машинных инструкциях:
test
илиcmp
инструкция, которая устанавливаетEFLAGS
Jcc
(переход) инструкция , в зависимости от типа сравнения (и макета кода):jne
- прыгать, если не равно ->ZF = 0
jz
- Перейти, если ноль (равно) ->ZF = 1
jg
- Прыгай, если больше ->ZF = 0 and SF = OF
Пример (отредактировано для краткости) Скомпилировано с
$ gcc -m32 -S -masm=intel test.c
Компилируется в:
А также
Компилируется в:
Таким образом, единственная разница между ними -
jg
этоjge
инструкция. Оба займут одинаковое количество времени.Я хотел бы обратиться к комментарию, что ничто не указывает на то, что различные инструкции перехода занимают одинаковое количество времени. На этот вопрос немного сложно ответить, но вот что я могу дать: в Справочнике по набору инструкций Intel все они сгруппированы по одной общей инструкции
Jcc
(переход, если выполнено условие). Такая же группировка выполняется вместе в Справочном руководстве по оптимизации , в Приложении C. Задержка и пропускная способность.Значения для
Jcc
:со следующей сноской
Jcc
:Таким образом, ничто в документах Intel никогда не рассматривает одну
Jcc
инструкцию в отличие от других.Если задуматься о фактической схеме, используемой для реализации инструкций, можно предположить, что в разных битах будут простые вентили И / ИЛИ
EFLAGS
, чтобы определить, выполняются ли условия. Таким образом, нет никаких причин, по которым инструкция, проверяющая два бита, должна занимать больше или меньше времени, чем одна проверка только одного (Игнорирование задержки распространения затвора, которая намного меньше, чем тактовый период).Изменить: с плавающей точкой
Это справедливо и для x87 с плавающей точкой: (Практически тот же код, что и выше, но с
double
вместоint
.)источник
jg
иjnle
те же инструкции7F
:-)Исторически (мы говорим о 1980-х и начале 1990-х), было несколько архитектур, в которых это было правдой. Основная проблема заключается в том, что целочисленное сравнение по своей сути реализуется посредством целочисленных вычитаний. Это приводит к следующим случаям.
Теперь, когда
A < B
вычитание должно занимать старший бит для правильного вычитания, точно так же, как вы берете и заимствуете при сложении и вычитании вручную. Этот «заимствованный» бит обычно упоминается как бит переноса и может проверяться инструкцией ветвления. Второй бит, называемый нулевым битом , будет установлен, если вычитание будет идентично нулю, что подразумевает равенство.Обычно было как минимум две команды условного перехода: одна для перехода на бит переноса, а другая на нулевой бит.
Теперь, чтобы понять суть дела, давайте расширим предыдущую таблицу, включив в нее результаты переноса и нулевые биты.
Таким образом, реализация ветви
A < B
может быть выполнена в одной инструкции, потому что бит переноса очищен только в этом случае, то естьНо, если мы хотим сделать сравнение меньше или равно, нам нужно сделать дополнительную проверку нулевого флага, чтобы поймать случай равенства.
Таким образом, на некоторых машинах использование сравнения «меньше» может сохранить одну машинную инструкцию . Это было актуально в эпоху скорости процессора ниже мегагерца и отношения процессора к памяти 1: 1, но сегодня это практически не имеет значения.
источник
jge
тестирование флагов «ноль» и «знак / перенос».<=
тест может быть реализован в одной инструкции с поменяв операнды и тестирование наnot <
(эквивалент>=
) Это желательно<=
с обмениваемых операндами:cmp B,A; bcs addr
. Вот почему этот тест был опущен Intel, они сочли его избыточным, и вы не могли позволить себе лишние инструкции в то время :-)Предполагая, что мы говорим о внутренних целочисленных типах, нет никакого способа, которым один мог бы быть быстрее чем другой. Они явно семантически идентичны. Они оба просят компилятор сделать то же самое. Только ужасно сломанный компилятор может сгенерировать худший код для одного из них.
Если была какая-то платформа, которая
<
была быстрее, чем<=
для простых целочисленных типов, компилятор всегда должен преобразовывать<=
в<
константы. Любой компилятор, который не был бы просто плохим компилятором (для этой платформы).источник
<
ни<=
компилятор не решат, какую скорость они будут иметь. Это очень простая оптимизация для компиляторов, если учесть, что они, как правило, уже выполняют оптимизацию мертвого кода, оптимизацию хвостовых вызовов, поднятие циклов (иногда развертывание), автоматическое распараллеливание различных циклов и т. Д. Зачем тратить время на обдумывание преждевременной оптимизации ? Запустите прототип, профилируйте его, чтобы определить, где лежат наиболее существенные оптимизации, выполните эти оптимизации в порядке значимости и снова профилируйте в процессе измерения прогресса ...(a < C)
в(a <= C-1)
(для некоторой константыC
)C
затрудняет кодирование в наборе команд. Например, набор команд может быть способен представлять константы со знаком от -127 до 128 в компактной форме в сравнениях, но константы вне этого диапазона должны загружаться с использованием либо более длинной, более медленной кодировки, либо полностью другой инструкции. Таким образом, подобное сравнение(a < -127)
может не иметь прямого преобразования.a > 127
сa > 128
тем, что у вас там нет выбора, вы используете тот, который вам нужен. Мы сравниваемa > 127
с темa >= 128
, что не может требовать другой кодировки или разных инструкций, потому что они имеют одну и ту же таблицу истинности. Любое кодирование одного равно кодированию другого.<=
в<
константы». Насколько я знаю, это преобразование предполагает изменение константы. Например,a <= 42
компилируется какa < 43
потому что<
быстрее. В некоторых крайних случаях такое преобразование не будет плодотворным, поскольку новая константа может потребовать большего или меньшего количества инструкций. Конечно ,a > 127
иa >= 128
эквивалентны и компилятор должен кодировать обе формы в (же) самый быстрый способ, но это не противоречит тому , что я сказал.Я вижу, что ни один не быстрее. Компилятор генерирует один и тот же машинный код в каждом условии с различным значением.
Мой пример
if
из GCC на платформе x86_64 в Linux.Авторы компиляторов - довольно умные люди, и они думают об этих вещах, и многие другие считают нас само собой разумеющимся.
Я заметил, что если это не константа, то в обоих случаях генерируется один и тот же машинный код.
источник
if(a <=900)
чтобы продемонстрировать, что он генерирует точно такой же asm :)Для кода с плавающей запятой сравнение <= действительно может быть медленнее (на одну инструкцию) даже на современных архитектурах. Вот первая функция:
В PowerPC сначала выполняется сравнение с плавающей запятой (которое обновляет
cr
регистр условий), затем перемещается регистр условий в GPR, сдвигается бит «сравнено меньше чем», а затем возвращается. Требуется четыре инструкции.Теперь рассмотрим эту функцию вместо:
Это требует той же работы, что и
compare_strict
выше, но теперь есть два бита: «было меньше» и «было равно». Это требует дополнительной инструкции (cror
- регистр условия поразрядно ИЛИ), чтобы объединить эти два бита в один. Так чтоcompare_loose
требуется пять инструкций, аcompare_strict
требуется четыре.Вы можете подумать, что компилятор может оптимизировать вторую функцию следующим образом:
Однако это будет неправильно обрабатывать NaN.
NaN1 <= NaN2
иNaN1 > NaN2
нужно оба оценить как ложное.источник
fucomip
устанавливает ZF и CF.cr
является эквивалентом флагов , какZF
иCF
на x86. (Хотя CR более гибок.) О чем говорит автор плаката, так это о переносе результата в GPR: для этого требуется две инструкции на PowerPC, но в x86 есть инструкция условного перемещения.Может быть, автор этой безымянной книги прочитал, что она
a > 0
работает быстрее,a >= 1
и считает, что это правда во всем мире.Но это потому, что
0
участвует (потому чтоCMP
может, в зависимости от архитектуры, заменить, например, наOR
), а не из-за<
.источник
(a >= 1)
запуска медленнее потребуется плохой компилятор(a > 0)
, поскольку первый может быть тривиально преобразован во второй с помощью оптимизатора.По крайней мере, если бы это было так, компилятор мог бы тривиально оптимизировать a <= b к! (A> b), и поэтому даже если бы само сравнение было на самом деле медленнее со всеми, кроме самого наивного компилятора, вы бы не заметили разницу ,
источник
NOT
только что сделан по другой инструкции (je
противjne
)У них одинаковая скорость. Возможно, в какой-то особой архитектуре то, что он / она сказал, правильно, но, по крайней мере, в семействе x86 я знаю, что они одинаковы. Потому что для этого ЦП выполнит вычитание (a - b), а затем проверит флаги регистра флага. Два бита этого регистра называются ZF (нулевой флаг) и SF (знаковый флаг), и это делается за один цикл, потому что он будет делать это с одной операцией маски.
источник
Это будет сильно зависеть от базовой архитектуры, в которую компилируется Си. Некоторые процессоры и архитектуры могут иметь явные инструкции для равных или меньших и равных, которые выполняются за разное количество циклов.
Это было бы довольно необычно, поскольку компилятор мог бы обойти это, делая его неуместным.
источник
TL; DR ответ
Для большинства комбинаций архитектуры, компилятора и языка это не будет быстрее.
Полный ответ
Другие ответы были сосредоточены на x86 архитектуре, и я не знаю ARM архитектуры (что ваш пример на ассемблер , кажется,) достаточно хорошо , чтобы конкретно прокомментировать сгенерированный код, но это пример микро-оптимизацию , которая является самой архитектурой специфический, и с такой же вероятностью будет антиоптимизацией, как и оптимизацией .
Таким образом, я бы предположил, что такого рода микрооптимизация является примером программирования культового груза, а не лучшей практикой разработки программного обеспечения.
Есть, вероятно, некоторые архитектуры, где это оптимизация, но я знаю, по крайней мере, одну архитектуру, где может быть и обратное. В почтенной архитектуре Транспутера инструкции машинного кода были только равны и больше или равны , поэтому все сравнения должны были быть построены из этих примитивов.
Даже тогда, почти во всех случаях, компилятор мог упорядочить инструкции оценки таким образом, чтобы на практике ни одно сравнение не имело никакого преимущества перед любым другим. В худшем случае, возможно, потребуется добавить обратную инструкцию (REV), чтобы поменять местами два верхних элемента в стеке операндов . Это была однобайтовая инструкция, для выполнения которой потребовался один цикл, поэтому были наименьшие возможные издержки.
Является ли микрооптимизация, подобная этой, оптимизацией или антиоптимизацией, зависит от конкретной архитектуры, которую вы используете, поэтому обычно плохая идея привыкнуть к использованию микрооптимизаций, специфичных для архитектуры, иначе вы можете инстинктивно используйте один, когда это неуместно, и похоже, что именно эту книгу вы защищаете.
источник
Вы не должны быть в состоянии заметить разницу, даже если она есть. Кроме того, на практике вам придется выполнить дополнительное условие
a + 1
илиa - 1
выполнить условие, если вы не собираетесь использовать магические константы, что во всех отношениях является очень плохой практикой.источник
Вы могли бы сказать, что строка является правильной в большинстве языков сценариев, так как дополнительный символ приводит к немного более медленной обработке кода. Тем не менее, как указывалось в самом верхнем ответе, это не должно иметь никакого эффекта в C ++, и все, что делается с помощью языка сценариев, вероятно, не касается оптимизации.
источник
Когда я писал этот ответ, я смотрел только на титульный вопрос о <против <= в целом, а не конкретный пример постоянного
a < 901
VS.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
всегда в конечном итоге становятся ложными. ( Что должен знать каждый программист на C о неопределенном поведении )Компиляторы могут оптимизировать только таким образом, чтобы сохранить (определенное и юридически наблюдаемое) поведение источника C ++ для всех возможных входных значений , кроме тех, которые приводят к неопределенному поведению.
(Простое
i <= size
тоже создало бы проблему, но я подумал, что вычисление верхней границы было более реалистичным примером случайного введения возможности бесконечного цикла для ввода, который вас не волнует, но который должен учитывать компилятор.)В этом случае
size=0
приводит кupper_bound=UINT_MAX
иi <= UINT_MAX
всегда верно. Так что этот цикл бесконеченsize=0
, и компилятор должен учитывать это, даже если вы, как программист, вероятно, никогда не намереваетесь передать size = 0. Если компилятор может встроить эту функцию в вызывающую функцию, где он может доказать, что size = 0 невозможен, то это здорово, он может оптимизировать так, как мог быi < size
.Asm like
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
формулы Гаусса .x86-64 asm из clang7.0 и gcc8.2 в проводнике компилятора Godbolt
Но для наивной версии мы просто получаем тупую петлю от лязга.
GCC в любом случае не использует замкнутую форму, поэтому выбор условия цикла на самом деле не повредит ; он автоматически векторизуется с добавлением целых чисел SIMD, выполняя 4
i
значения параллельно в элементах регистра XMM.У этого также есть простой скалярный цикл, который я думаю, что он использует для очень маленького
n
, и / или для случая бесконечного цикла.Кстати, оба этих цикла тратят впустую инструкцию (и моп на процессорах семейства Sandybridge) на издержки цикла.
sub eax,1
/jnz
вместоadd eax,1
/ cmp / jcc будет более эффективным. 1 моп вместо 2 (после макро-слияния sub / jcc или cmp / jcc). Код после обоих циклов безоговорочно записывает EAX, поэтому он не использует окончательное значение счетчика цикла.источник
<
или<=
. Но, конечно,test ecx,ecx
/bt eax, 3
/jbe
будет прыгать, если ZF установлен (ecx == 0), или если CF установлен (бит 3 EAX == 1), вызывая частичный останов флага в большинстве процессоров, потому что читаемые флаги не все исходить из последней инструкции, чтобы написать любые флаги. На семействе Сэндибридж он не останавливается, просто нужно вставить объединяющийся моп.cmp
/test
записать все флаги, ноbt
оставляет ZF без изменений. felixcloutier.com/x86/btТолько если люди, которые создали компьютеры, плохо с логической логикой. Которого они не должны быть.
Каждое сравнение (
>=
<=
>
<
) может быть сделано с той же скоростью.То, что каждое сравнение, это просто вычитание (разница) и проверка, является ли оно положительным / отрицательным.
(Если
msb
установлено, число является отрицательным)Как проверить
a >= b
? Suba-b >= 0
Проверьте, еслиa-b
положительный.Как проверить
a <= b
? Sub0 <= b-a
Проверьте, еслиb-a
положительный.Как проверить
a < b
? Suba-b < 0
Check, еслиa-b
отрицательный.Как проверить
a > b
? Sub0 > b-a
Check, если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
LOLисточник