Инструкция INC против ADD 1: имеет ли это значение?
От Иры Бакстер ответьте, почему инструкции INC и DEC не влияют на флаг переноса (CF)?
В основном я держусь подальше от
INC
а такжеDEC
теперь, потому что они делают частичные обновления кода условия, и это может вызвать смешные остановки в конвейере, иADD
/SUB
нет. Так что, где это не имеет значения (большинство мест), я используюADD
/SUB
чтобы избежать киосков. я используюINC
/DEC
только при небольшом содержании кода, например, при размещении в строке кэша, где размер одной или двух команд имеет достаточную разницу для значения. Вероятно, это бессмысленная нано [в буквальном смысле!] Оптимизация, но я довольно старомоден в своих привычках кодирования.
И я хотел бы спросить, почему это может привести к остановке в конвейере, а add - нет? Ведь оба ADD
а также INC
обновляет флаг регистров. Единственная разница в том, что INC
не обновляет CF
, Но почему это важно?
2 ответа
На современных процессорах, add
никогда не медленнее, чем inc
(за исключением косвенных эффектов размера / декодирования кода), но обычно это тоже не быстрее, поэтому вы должны предпочесть inc
по причинам размера кода. Особенно, если этот выбор повторяется много раз в одном и том же двоичном файле (например, если вы пишете компилятор).
inc
сохраняет 1 байт (64-битный режим) или 2 байта (коды операций 0x40..F inc r32
/ dec r32
краткая форма в 32-битном режиме, переопределенная как префикс REX для x86-64). Это делает небольшую процентную разницу в общем размере кода. Это помогает увеличить частоту обращений к кешу инструкций, частоту обращений к iTLB и количество страниц, которые необходимо загрузить с диска.
Преимущества inc
:
- размер кода напрямую
- Неиспользование немедленного может иметь эффект Uop-кэша для семейства Sandybridge, что может компенсировать лучшее микроплавление
add
,(См . Таблицу Агнера Фога 9.1 в разделе "Sandybridge" его руководства по микроархитектуре.) Счетчики Perf могут легко измерить количество операций на этапе выпуска, но сложнее измерить, как вещи упаковывают в кэш uop и эффекты пропускной способности чтения uop-cache. - Оставление CF без изменений является преимуществом в некоторых случаях для процессоров, где вы можете прочитать CF после
inc
без ларька. (Не на Нехалеме, а раньше.)
Среди современных процессоров есть одно исключение: декодирование Silvermont/Goldmont/Knight's Landing inc
/ dec
эффективно как 1 моп, но расширяется до 2 на этапе выделения / переименования (иначе проблема). Extra Uop объединяет частичные флаги. inc
пропускная способность составляет всего 1 за такт, по сравнению с 0,5c (или 0,33c Goldmont) для независимых add r32, imm8
из-за цепочки dep, созданной мопами слияния флагов.
В отличие от P4, результат регистра не имеет false-dep для флагов (см. Ниже), поэтому выполнение не по порядку убирает слияние флагов с критического пути задержки, когда ничто не использует результат флага. (Но окно OOO намного меньше, чем основные процессоры, такие как Haswell или Ryzen.) inc
так как 2 отдельных мопа - это, вероятно, победа Сильвермонта в большинстве случаев; большинство инструкций x86 записывают все флаги, не читая их, нарушая эти цепочки зависимостей флагов.
SMont / KNL имеет очередь между декодированием и выделением / переименованием (см . Руководство по оптимизации Intel, рис. 16-2), поэтому расширение до 2 моп во время выпуска может заполнить пузырьки из киосков декодирования (в таких инструкциях, как один операнд mul
, или же pshufb
, которые производят более 1 моп из декодера и вызывают остановку цикла 3-7 для микрокода). Или в Silvermont - просто инструкция с более чем 3 префиксами (включая управляющие байты и обязательные префиксы), например, REX + любая инструкция SSSE3 или SSE4. Но обратите внимание, что существует буфер цикла ~28 моп, поэтому небольшие циклы не страдают от этих остановок декодирования.
inc
/ dec
не единственные инструкции, которые декодируют как 1, но выдают как 2: push
/ pop
, call
/ ret
, а также lea
с 3 компонентами сделайте это тоже. Так что KNL AVX512 собирать инструкции. Источник: руководство по оптимизации Intel, 17.1.2. Механизм выхода из строя (KNL). Это всего лишь небольшой штраф за пропускную способность (и иногда даже не тот, который, если что-то еще, является большим узким местом), так что обычно все еще можно использовать inc
для "универсального" тюнинга.
Руководство по оптимизации Intel по-прежнему рекомендует add 1
над inc
в общем, чтобы избежать рисков частичного срыва флагов. Но поскольку компилятор Intel не делает этого по умолчанию, маловероятно, что будущие процессоры inc
медленный во всех случаях, как P4.
Clang 5.0 и Intel ICC 17 (на Godbolt) действительно используют inc
при оптимизации по скорости (-O3
), а не только по размеру. -mtune=pentium4
заставляет их избегать inc
/ dec
, но по умолчанию -mtune=generic
не придает большого значения P4.
ICC17 -xMIC-AVX512
(эквивалентно GCC -march=knl
) избегает inc
Это, вероятно, хорошая ставка в целом для Silvermont / KNL. Но это, как правило, не является катастрофой производительности, чтобы использовать inc
так что, вероятно, все еще уместно использовать "универсальную" настройку inc
/ dec
в большинстве кода, особенно когда результат флага не является частью критического пути.
Помимо Silvermont, это в основном устаревший совет по оптимизации, оставшийся от Pentium4. На современных процессорах проблема возникает только в том случае, если вы на самом деле читаете флаг, который не был написан последним insn, который написал какие-либо флаги. например, в BigInteger adc
петли. (И в этом случае вам нужно сохранить CF, так что add
сломал бы ваш код.)
add
записывает все биты флага условия в регистр EFLAGS. Переименование регистра упрощает выполнение записи только по порядку: см. Опасности записи после записи и записи после чтения. add eax, 1
а также add ecx, 1
могут выполняться параллельно, потому что они полностью независимы друг от друга. (Даже Pentium4 переименовывает биты флага условия отдельно от остальных EFLAGS, так как даже add
оставляет включенные прерывания и многие другие биты без изменений.)
На Р4, inc
а также dec
зависит от предыдущего значения всех флагов, поэтому они не могут выполняться параллельно друг с другом или предыдущими инструкциями по установке флагов. (например add eax, [mem]
/ inc ecx
делает inc
ждать, пока после add
, даже если загрузка аддона отсутствует в кеше.) Это называется ложной зависимостью. Partial-flag пишет работу, читая старое значение флагов, обновляя биты, отличные от CF, затем записывая полные флаги.
Все остальные вышедшие из строя процессоры x86 (включая AMD) переименовывают разные части флагов по отдельности, поэтому внутренне они выполняют обновление только для записи для всех флагов, кроме CF. (источник: руководство по микроархитектуре Агнера Фога). Только несколько инструкций, как adc
или же cmc
, действительно прочитайте, а затем напишите флаги. Но также shl r, cl
(увидеть ниже).
Случаи, где add dest, 1
предпочтительнее inc dest
по крайней мере для семейства Intel P6/SnB uarch:
Память-назначение:
add [rdi], 1
может объединить хранилище и нагрузку + добавить на Intel Core2 и SnB-семействе, так что это 2 uops с слитым доменом / 4 uops-unops -domain.inc [rdi]
может только микро-предохранитель магазина, так что это 3F / 4U.
Согласно таблицам Агнера Фога, AMD и Silvermont работают с памятьюinc
а такжеadd
так же, как одиночный макрооператор / моп.Но остерегайтесь эффектов uop-кеша с
add [label], 1
которому нужен 32-битный адрес и 8-битный немедленный для того же мопа.Перед смещением / поворотом счетчика переменных, чтобы сломать зависимость от флагов и избежать частичного слияния флагов:
shl reg, cl
имеет входную зависимость от флагов из-за неудачной истории CISC: он должен оставить их неизмененными, если число сдвигов равно 0.В семействе Intel SnB смещение с переменным счетом составляет 3 мопа (по сравнению с 1 на Core2/Nehalem). AFAICT, два из флагов чтения и записи мопов и независимое чтение мопов
reg
а такжеcl
и пишетreg
, Это странный случай лучшей задержки (1c + неизбежные конфликты ресурсов), чем пропускная способность (1.5c), и возможность достижения максимальной пропускной способности только в том случае, если он смешан с инструкциями, которые нарушают зависимости от флагов. ( Я написал об этом на форуме Агнера Фога). Используйте BMI2shlx
когда возможно; это 1 моп, и количество может быть в любом регистре.Тем не мение,
inc
(написание флагов, но уходCF
без изменений) перед подсчетом переменныхshl
оставляет его с ложной зависимостью от того, что написал CF последним, а для SnB/IvB может потребоваться дополнительный uop для объединения флагов.Core2 / Nehalem удается избежать даже ложного удаления флагов: Merom запускает цикл из 6 независимых
shl reg,cl
инструкции почти в две смены за такт, то же самое исполнение с cl=0 или cl=13. Все, что лучше, чем 1 за такт, доказывает отсутствие зависимости от входных данных для флагов.Я пробовал петли с
shl edx, 2
а такжеshl edx, 0
(мгновенный сдвиг), но не видел разницы в скоростиdec
а такжеsub
на Core2, HSW или SKL. Я не знаю о AMD.
Обновление: Хорошая производительность сдвига в семействе Intel P6 достигается ценой большой производительности, которую вам следует избегать: когда инструкция зависит от флага-результата команды сдвига: внешний интерфейс останавливается до тех пор, пока инструкция не будет удалена. (Источник: руководство по оптимизации Intel,(раздел 3.5.2.6: Регистрация регистров частичного флага)). Так shr eax, 2
/ jnz
Я думаю, это довольно катастрофично для производительности на Intel pre-Sandybridge! использование shr eax, 2
/ test eax,eax
/ jnz
если вы заботитесь о Nehalem и ранее. Примеры Intel показывают, что это относится к сменам немедленного счета, а не только к счету = cl
,
В процессорах, основанных на микроархитектуре Intel Core [это означает Core 2 и более поздние версии], немедленное смещение на 1 обрабатывается специальным оборудованием, так что оно не испытывает частичное блокирование флага.
Intel на самом деле означает специальный код операции без немедленного, который сдвигается неявным 1
, Я думаю, что есть разница в производительности между двумя способами кодирования shr eax,1
с коротким кодированием (используя оригинальный код операции 8086 D1 /5
) создание результата флага частичной записи только для записи, но более длинное кодирование (C1 /5, imm8
с немедленным 1
) не проверять его немедленно на 0 до времени выполнения, но без отслеживания вывода флага в механизме с нарушением порядка.
Поскольку зацикливание на битах является обычным явлением, но зацикливание на каждом 2-м бите (или любом другом шаге) очень редко, это кажется разумным выбором дизайна. Это объясняет, почему компиляторы любят test
результат сдвига вместо непосредственного использования результатов флага из shr
,
Обновление: для переменных смещений в семействе SnB руководство по оптимизации Intel гласит:
3.5.1.6 Переменное число битов Вращение и сдвиг
В микроархитектуре Intel под кодовым названием Sandy Bridge инструкция "ROL/ROR/SHL/SHR reg, cl" имеет три микрооперации. Когда результат пометки не требуется, один из этих микроопераций может быть отброшен, обеспечивая лучшую производительность во многих общих случаях. Когда эти инструкции обновляют результаты частичного флага, которые впоследствии используются, весь поток из трех микроопераций должен проходить через конвейер выполнения и вывода из эксплуатации, что приводит к снижению производительности. В микроархитектуре Intel с кодовым именем Ivy Bridge выполнение полного потока трех микроопераций для использования обновленного результата частичного флага имеет дополнительную задержку.
Рассмотрим зацикленную последовательность ниже:
loop: shl eax, cl add ebx, eax dec edx ; DEC does not update carry, causing SHL to execute slower three micro-ops flow jnz loop
Инструкция DEC не изменяет флаг переноса. Следовательно, инструкция SHL EAX, CL должна выполнить поток трех микроопераций в последующих итерациях. Инструкция SUB обновит все флаги. Так что замена
DEC
сSUB
позволитSHL EAX, CL
выполнить поток двух микроопераций.
терминология
Частичные остановки флагов происходят при чтении флагов, если они вообще происходят. У P4 никогда не бывает киосков с частичным флагом, потому что их никогда не нужно объединять. Вместо этого он имеет ложные зависимости.
Несколько ответов / комментариев смешивают терминологию. Они описывают ложную зависимость, но затем называют ее остановкой частичного флага. Это замедление происходит из-за записи только некоторых флагов, но термин " остановка частичного флага" - это то, что происходит на оборудовании Intel до SnB, когда записи частичного флага должны быть объединены. Процессоры семейства Intel SnB вставляют дополнительный UOP для объединения флагов без остановки. Nehalem и более ранняя остановка для ~7 циклов. Я не уверен, насколько велик штраф на процессорах AMD.
(Обратите внимание, что штрафы за частичный регистр не всегда совпадают с частичными флагами, см. Ниже).
### Partial flag stall on Intel P6-family CPUs:
bigint_loop:
adc eax, [array_end + rcx*4] # partial-flag stall when adc reads CF
inc rcx # rcx counts up from negative values towards zero
# test rcx,rcx # eliminate partial-flag stalls by writing all flags, or better use add rcx,1
jnz
# this loop doesn't do anything useful; it's not normally useful to loop the carry-out back to the carry-in for the same accumulator.
# Note that `test` will change the input to the next adc, and so would replacing inc with add 1
В других случаях, например, частичная запись флага с последующей полной записью флага или чтение только флагов, записанных inc
, Это хорошо. На процессорах семейства SnB, inc/dec
может даже макро-предохранитель с jcc
, такой же как add/sub
,
После P4 Intel в основном перестала пытаться заставить людей перекомпилировать -mtune=pentium4
или измените рукописный ассм как можно больше, чтобы избежать серьезных узких мест. (Настройка для конкретной микроархитектуры всегда будет чем-то особенным, но P4 был необычным в устаревшем множестве вещей, которые раньше были быстрыми на предыдущих процессорах, и, таким образом, были обычными в существующих двоичных файлах.) P4 хотел, чтобы люди использовали RISC-подобное подмножество x86, а также имел подсказки предсказания ветвления в качестве префиксов для инструкций JCC. (У него также были другие серьезные проблемы, такие как кэш трассировки, который просто не был достаточно хорош, и слабые декодеры, которые приводили к плохой производительности при промахах кеша трассировки. Не говоря уже о философии тактирования очень высокого уровня, которая врезалась в стену плотности мощности.)
Когда Intel отказалась от P4 (netburst uarch), они вернулись к проектам семейства P6 (Pentium-M / Core2 / Nehalem), которые унаследовали обработку их частичных флагов / частичных регистров от более ранних процессоров семейства P6 (PPro-PIII), которые предварительно от неудачного шага. (Не все в P4 было изначально плохим, и некоторые идеи вновь появились в Sandybridge, но в целом NetBurst считается ошибкой.) Некоторые инструкции с очень CISC все еще медленнее, чем альтернативы с несколькими командами, например enter
, loop
, или же bt [mem], reg
(потому что значение reg влияет на то, какой адрес памяти используется), но все они были медленными в старых процессорах, поэтому компиляторы их уже избегали.
В Pentium-M даже улучшена аппаратная поддержка частичных регистров (снижены штрафы за слияние). В Sandybridge Intel сохранила частичное переименование и частичное переименование и сделала его намного более эффективным, когда необходимо объединение (объединение UOP вставлено без или с минимальным срывом). SnB внес значительные внутренние изменения и считается новым семейством уархов, хотя он многое наследует от Nehalem и некоторые идеи от P4. (Но обратите внимание, что кэш декодированного UB SnB не является кешем трассировки, так что это совсем другое решение проблемы пропускной способности / мощности декодера, которую пытался решить кеш трассировки netburst.)
Например, inc al
а также inc ah
может работать параллельно на процессорах семейства P6 / SnB, но чтение eax
впоследствии требует слияния.
PPro / PIII срыв на 5-6 циклов при чтении полного рег. Core2/Nehalem останавливаются только на 2 или 3 цикла при вставке объединяющего мопа для частичных регистров, но частичные флаги все еще более длинные.
SnB вставляет объединяющий элемент без сбоев, как для флагов. Руководство по оптимизации Intel гласит, что для объединения AH/BH/CH/DH в более широкий регистр вставка объединяющего мопа занимает полный цикл выпуска / переименования, в течение которого никакие другие мопы не могут быть выделены. Но для low8/low16 объединяющая операция является "частью потока", поэтому она, очевидно, не вызывает дополнительных штрафов пропускной способности внешнего интерфейса, кроме того, что она занимает один из 4 слотов в цикле выпуска / переименования.
В IvyBridge (или, по крайней мере, в Haswell) Intel отказалась от частичного переименования регистров для регистров low8 и low16, сохраняя его только для регистров high8 (AH/BH/CH/DH). Чтение регистров high8 имеет дополнительную задержку. Также, setcc al
имеет ложную зависимость от старого значения rax, в отличие от Nehalem и более ранних (и, вероятно, Sandybridge). Подробности смотрите в разделе " Вопросы и ответы по частичным регистрам HSW/SKL".
(Ранее я утверждал, что Haswell может объединить AH без uop, но это не так, и это не то, о чем говорит руководство Агнера Фога. Я слишком быстро снялся и, к сожалению, повторил свое неправильное понимание во многих комментариях и других постах.)
Процессоры AMD и Intel Silvermont не переименовывают частичные регистры (кроме флагов), поэтому mov al, [mem]
имеет ложную зависимость от старого значения eax. (Достоинством является отсутствие замедления слияния с частичным регистром при чтении полного регистра позже.)
Как правило, единственный раз add
вместо inc
сделает ваш код быстрее на AMD или основной Intel, когда ваш код на самом деле зависит от поведения не-touch-CF inc
, т.е. обычно add
помогает только тогда, когда это сломает ваш код, но обратите внимание на shl
вышеупомянутый случай, когда инструкция читает флаги, но обычно ваш код не заботится об этом, поэтому это ложная зависимость.
Если вы на самом деле хотите оставить CF неизмененным, то у CPU до семейства SnB есть серьезные проблемы с остановками частичных флагов, но в SnB-семействе издержки на объединение процессоров с частичными флагами очень малы, поэтому лучше сохранить с помощью inc
или же dec
как часть условия цикла при нацеливании на эти CPU, с некоторым развертыванием. (Подробнее см. BigInteger adc
Q & A я связал ранее). Это может быть полезно для использования lea
выполнять арифметику, не затрагивая флаги вообще, если вам не нужно переходить на результат.
В зависимости от реализации инструкций процессором, частичное обновление регистра может вызвать остановку. Согласно руководству по оптимизации Agner Fog, стр. 62,
По историческим причинам
INC
а такжеDEC
инструкции оставляют флаг переноса без изменений, в то время как другие арифметические флаги записываются в. Это вызывает ложную зависимость от предыдущего значения флагов и стоит лишний моп. Чтобы избежать этих проблем, рекомендуется всегда использоватьADD
а такжеSUB
вместоINC
а такжеDEC
, Например,INC EAX
должен быть замененADD EAX,1
,
См. Также стр. 83 в разделе "Частичные флажки" и стр. 100 в разделе "Частичные флажки".