Инструкция INC против ADD 1: Это имеет значение?

В основном, я держусь подальше от INC и DEC, потому что они частично обновления кода состояния, и это может вызвать забавные киоски в трубопровода и ADD/SUB нет. Поэтому, когда это не имеет значения (большинство мест), Я использую ADD/SUB, чтобы избежать ларьков. Я использую INC/DEC только при сохранении малое значение кода, например, установка в строке кэша, где размер одна или две инструкции делают достаточную разницу в материи. Это возможно бессмысленный nano [буквально!] - оптимизация, но я довольно старой школы в моих привычках кодирования.

автор: @Ира Бакстер

Вышеприведенный фрагмент из почему инструкции INC и DEC не влияют на флаг переноса?

И я хотел бы спросить, почему это может вызвать киоски в конвейере, а add - нет? В конце концов, как add, так и inc обновляет регистры флагов. Единственное различие заключается в том, что inc не обновляет CF. Но почему это важно?

Ответ 1

На современных процессорах add никогда не медленнее, чем inc (за исключением косвенных эффектов размера/декодирования кода), но обычно это не так быстро, поэтому вам следует предпочесть inc для размера кода причины. Особенно, если этот выбор повторяется много раз в одном двоичном файле (например, если вы являетесь компилятором-писателем).

inc сохраняет 1 байт (64-разрядный режим) или 2 байта (opcodes 0x40..F inc r32/dec r32 короткая форма в 32-битном режиме, переопределенная как префикс REX для x86- 64). Это делает небольшую процентную разницу в общем размере кода. Это помогает снизить количество ошибок в кеш-кеш, скорость атаки iTLB и количество страниц, которые необходимо загрузить с диска.

Преимущества inc:

  • размер кода напрямую
  • Невозможно использовать немедленный эффект uop-cache для семейства Sandybridge, что может компенсировать лучшее микро-слияние add. (См. Таблица Agner Fog 9.1 в разделе Sandybridge его руководства по микроархитексу.) Счетчики Perf могут легко измерить эмиссионные уровни, но сложнее измерить как вещи упаковываются в кеш-память uop и uop-cache считывают эффекты полосы пропускания.
  • Выход из немодифицированного CF является преимуществом в некоторых случаях на процессорах, где вы можете читать CF после inc без остановки. (Не на Нехалеме и раньше.)

Существует одно исключение среди современных процессоров: Silvermont/Goldmont/Knight Landing эффективно декодирует inc/dec как 1 uop, но расширяется до 2 в allocate/rename (aka issue) сцена. Дополнительный uop объединяет частичные флаги. inc пропускная способность - только 1 за такт, против 0,5 c (или 0,33 c Goldmont) для независимого add r32, imm8 из-за созданной цепочки dep путем слияния флагов.

В отличие от P4, в реестре нет флагов-отпечатков на флажках (см. ниже), поэтому выполнение вне очереди принимает слияние флагов с критическим путем задержек, когда ничто не использует результат флага. (Но окно ООО намного меньше, чем обычные процессоры, такие как Хасуэлл или Рызень.) Выполнение inc в качестве двух отдельных процессоров, вероятно, является победой для Silvermont в большинстве случаев; большинство инструкций x86 записывают все флаги, не читая их, нарушая эти цепочки зависимостей флага.

SMont/KNL имеет очередь между декодированием и распределением/переименованием (см. Руководство по оптимизации Intel, рисунок 16-2), поэтому он расширяется до 2-х часов во время выпуска могут заполнять пузырьки из декодированных ларьков (по инструкциям типа один-операнд mul или pshufb), которые производят более 1 мкА от декодера и вызывают 3-7 циклов для микрокода). Или на Silvermont, просто инструкция с более чем тремя префиксами (включая escape-байты и обязательные префиксы), например. 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 over 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, поэтому, возможно, все еще необходимо использовать "универсальную" настройку для использования inc/dec в большинстве кодов, особенно когда результат флага не является частью критического пути.


Помимо Silvermont, это, в основном, устаревшая рекомендация по оптимизации от Pentium4. На современных процессорах существует только проблема, если вы действительно прочитали флаг, который не был написан последним insn, который писал какие-либо флаги. например. в циклах BigInteger adc.(И в этом случае вам нужно сохранить CF, поэтому использование add приведет к поломке вашего кода.)

add записывает все биты состояния в регистр EFLAGS. Реестр-переименование упрощает запись только для выполнения вне очереди: см. опасности записи после записи и записи после чтения. add eax, 1 и add ecx, 1 могут выполняться параллельно, потому что они полностью независимы друг от друга. (Даже Pentium4 переименовывает бит флага условия отдельно от остальной части EFLAGS, так как даже add оставляет прерывания и многие другие биты немодифицированы.)

На P4, inc и dec зависят от предыдущего значения всех флагов, поэтому они не могут выполняться параллельно друг другу или перед инструкциями по установке флага. (например, add eax, [mem]/inc ecx заставляет inc ждать до тех пор, пока не будет add, даже если загрузка будет отсутствовать в кеше.) Это называется ложной зависимостью. Partial-flag пишет работу, читая старое значение флагов, обновляя биты, отличные от CF, а затем записывая полные флаги.

Все остальные процессоры x86 x86 (включая AMD), переименовывают разные части флагов отдельно, поэтому внутренне они делают обновление только для записи ко всем флагам, кроме CF. (источник: Руководство по микроархитектуре Agner Fog). Только несколько инструкций, например adc или cmc, действительно читают, а затем записывают флаги. Но также shl r, cl (см. Ниже).


Случаи, где add dest, 1 предпочтительнее inc dest, по крайней мере для семейств uarch Intel P6/SnB:

  • Память-назначения: add [rdi], 1 может микрозахват магазина и загрузка + добавление в семейство Intel Core2 и SnB, так что это 2 домена fops-домена uops/4 unused-domain.
    inc [rdi] может только микро-предохранитель хранить, так что 3F/4U.
    Согласно таблицам Agner Fog, AMD и Silvermont запускают memory-dest inc и add то же самое, что и один макро-op/uop.

    Но будьте осторожны с эффектами uop-cache с add [label], 1, которым нужен 32-разрядный адрес и 8-битный немедленный для того же самого uop.

  • Перед сменой/поворотным счетчиком переменных, чтобы разбить зависимость от флагов и избежать слияния с частичным флагом: shl reg, cl имеет входную зависимость от флагов из-за неудачной истории CISC: он должен оставить их немодифицированными, если количество сдвигов равно 0.

    В семействе Intel SnB сдвиги с переменным числом - 3 раза (по сравнению с 1 на Core2/Nehalem). AFAICT, два из флагов чтения/записи uops и независимый uop читает reg и cl, и записывает reg. Это странный случай улучшения латентности (1c + неизбежные конфликты ресурсов), чем пропускная способность (1.5c), и только возможность достижения максимальной пропускной способности, если они смешиваются с инструкциями, которые нарушают зависимости от флагов. (Я опубликовал больше об этом на форуме Agner Fog). Используйте BMI2 shlx, когда это возможно; это 1 uop, и счет может быть в любом регистре.

    В любом случае 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 происходит за счет большой производительности, которую вам нужно избегать: когда команда зависит от результата флага: * strong > Передние стойки до тех пор, пока инструкция не будет удалена. (Источник: Руководство по оптимизации Intel (раздел 3.5.2.6: регистраторы с частичным флагом)). Так что shr eax, 2/jnz довольно катастрофично для производительности на Intel Pre-Sandybridge, я думаю! Используйте shr eax, 2/test eax,eax/jnz, если вас беспокоит Nehalem и ранее. Примеры Intel дают понять, что это относится к смещениям немедленного счета, а не только count = cl.

В процессорах на базе микроархитектуры Intel Core [это означает, что Core 2 и более поздние версии], немедленное смещение на 1 обрабатывается специальным оборудованием, так что оно не сталкивается с остановкой частичного флага.

Intel на самом деле означает специальный код операции без немедленного, который сдвигается неявным 1. Я думаю, что существует разница в производительности между двумя способами кодирования shr eax,1, с коротким кодированием (с использованием исходного кода 8086 D1 /5) создавая результат флага только для записи (неполный), но более длинное кодирование (C1 /5, imm8 с немедленным 1), не имеющее немедленного контроля на 0 до времени выполнения, но без отслеживания вывода флага в неуправляемом порядке техника.

Поскольку циклирование по битам является обычным явлением, но чередование каждого второго бита (или любого другого шага) очень необычно, это кажется разумным выбором дизайна. Это объясняет, почему компиляторы предпочитают 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 никогда не имеет парковочных мест с частичным флагом, потому что их никогда не нужно объединять. Вместо этого он имеет ложные зависимости.

Несколько ответов/комментариев смешивают терминологию. Они описывают ложную зависимость, но затем называют ее парком с флагом с частичным флагом. Это замедление, которое происходит из-за написания только некоторых флагов, но термин "partial-flag stall" - это то, что происходит на оборудовании pre-SnB Intel, когда записи с частичным флагом должны быть объединены. Процессоры 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 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 или изменить рукописный asm как можно больше, чтобы избежать серьезных узких мест. (Настройка для конкретной микроархитектуры всегда будет чем-то вроде этого, но 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 даже улучшил аппаратную поддержку для частичных regs (более низкие слияния штрафов). В Sandybridge Intel сохранила переименование частичного флага и частично-reg и сделала его намного более эффективным, когда требуется слияние (слияние uop, вставленное без или минимальное срыв). SnB сделал большие внутренние изменения и считается новым семейством uarch, хотя он много наследует от Nehalem и некоторые идеи от P4. (Но обратите внимание, что кэш SnB decoded-uop не является кэшем трассировки, тем не менее, это очень другое решение проблемы пропускной способности/мощности декодера, которую пытался решить кеш трассировки Netburst.)


Например, inc al и inc ah могут работать параллельно на процессорах семейства P6/SnB, но чтение eax впоследствии требует слияния.

PPro/PIII срыв на 5-6 циклов при чтении полной записи. Core2/Nehalem останавливается всего на 2 или 3 цикла при вставке слияния uop для частичных regs, но частичные флаги все еще более длинны.

SnB вставляет слияние uop без остановки, как и для флагов. Руководство по оптимизации Intel говорит, что для слияния AH/BH/CH/DH в более широкий рег, вставка слияния uop требует целого цикла "проблема/переименование", в течение которого не может быть выделено никаких других uops. Но для low8/low16 объединенный uop является "частью потока", поэтому он, по-видимому, не вызывает дополнительных штрафов за пропускную способность перед заходом на один из 4 слотов в цикле вопросов/переименований.

В IvyBridge (или, по крайней мере, Haswell) Intel отказалась от переименования частичных регистров для регистров low8 и low16, сохраняя их только для регистров high8 (AH/BH/CH/DH). Чтение регистров high8 имеет дополнительную задержку. Кроме того, setcc al имеет ложную зависимость от старого значения rax, в отличие от Nehalem и ранее (и, вероятно, Sandybridge). См. эту характеристику частичного регистра HSW/SKL Q & A для деталей.

(Я ранее утверждал, что Хасуэлл мог объединить AH без uop, но это не так, а не то, что говорит Agner Fog. Я слишком быстро пересказывал и, к сожалению, повторил свое неправильное понимание в большом количестве комментариев и других сообщений.)

Процессоры AMD и Intel Silvermont не переименовывают частичные регистры (кроме флагов), поэтому mov al, [mem] имеет ложную зависимость от старого значения eax. (Потенциал роста не является замедлением замещения частичного списания при чтении полного регистра позже.)


Как правило, единственное время add вместо inc сделает ваш код быстрее в AMD или основной Intel, когда ваш код действительно зависит от поведения без tt-touch-CF inc. т.е. обычно add помогает только тогда, когда он сломает ваш код, но обратите внимание на упомянутый выше случай shl, где инструкция считывает флаги, но обычно ваш код не заботится об этом, поэтому он ложная зависимость.

Если вы действительно хотите оставить CF немодифицированным, процессоры pre SnB-семейства имеют серьезные проблемы с ларьками с частичным флагом, но в семействе SnB накладные расходы на то, что слияние центрального процессора с частичными флагами очень низкое, это может быть лучше использовать inc или dec как часть условия цикла при таргетинге на этот ЦП с некоторой разворачиванием. (Подробнее см. BigInteger adc Q & A, я связал ранее). Полезно использовать lea для выполнения арифметики без влияния на флаги вообще, если вам не нужно вставлять результат.

Ответ 2

В зависимости от реализации команд ЦПУ обновление частичного регистра может привести к остановке. Согласно Руководство по оптимизации Agner Fog, стр. 62,

По историческим причинам команды INC и DEC оставляют флаг переноса неизменным, в то время как другие арифметические флаги записываются в. Это приводит к ложной зависимости от предыдущего значения флагов и стоимости дополнительного μop. Чтобы избежать этих проблем, рекомендуется использовать ADD и SUB вместо INC и DEC. Например, INC EAX следует заменить на ADD EAX,1.

См. также стр. 83 в разделе "Партийные стойки с флагом" и стр. 100 в разделе "Столбцы с частичными флагами".