Каков наилучший способ установить регистр в ноль в сборке x86: xor, mov или или?

Все следующие инструкции делают то же самое: установите %eax на ноль. Какой путь оптимален (требуется меньше машинных циклов)?

xorl   %eax, %eax
mov    $0, %eax
andl   $0, %eax

Ответы

Ответ 1

TL; сводка DR: xor same, same - лучший выбор для всех процессоров. Ни один другой метод не имеет никакого преимущества перед ним, и он имеет по крайней мере некоторое преимущество перед любым другим методом. Это официально рекомендовано Intel и AMD. В 64-битном режиме все еще используйте xor r32, r32, потому что записывает 32-битные регистры с нулями в верхние 32. xor r64, r64 - пустая трата байта, потому что ему нужен префикс REX.

Хуже того, Silvermont распознает xor r32,r32 только как разрушающий, а не 64-битный размер операнда. Таким образом, , даже если префикс REX все еще требуется, потому что вы обнуляете r8..r15, используйте xor r10d,r10d, а не xor r10,r10.

Примеры целочисленных GP:

xor   eax, eax       ; RAX = 0.  Including AL=0 etc.
xor   r10d, r10d     ; R10 = 0
xor   edx, edx       ; RDX = 0

; small code-size alternative:    cdq    ; zero RDX if EAX is already zero

; SUB-OPTIMAL
xor   rax,rax       ; waste of a REX prefix, and extra slow on Silvermont
mov   eax, 0        ; does not touch FLAGS, but not faster and takes more bytes

xor   al, al        ; false dep on some CPUs, not a zeroing idiom.
mov   al, 0         ; only 2 bytes, and probably better than xor al,al *if* you need to leave the rest of EAX/RAX unmodified

Обнуление векторного регистра обычно лучше всего сделать с помощью pxor xmm, xmm. Обычно это делает gcc (даже перед использованием с инструкциями FP).

xorps xmm, xmm может иметь смысл. Он на один байт короче, чем pxor, но xorps требуется порт выполнения 5 на Intel Nehalem, тогда как pxor может работать на любом порту (0/1/5). (Задержка задержки обхода Nehalem 2c между целым числом и FP обычно не имеет значения, потому что выполнение вне порядка обычно может скрыть его в начале новой цепочки зависимостей).

На микроархитектурах семейства SnB ни один из вариантов xor -zeroing даже не нуждается в порте исполнения. В AMD и Intel, предшествующих Nehalem P6/Core2, xorps и pxor обрабатываются одинаково (как целочисленные инструкции вектора).

При использовании AVX-версии 128-векторной векторной инструкции также обнуляется верхняя часть регистра, поэтому vpxor xmm, xmm, xmm является хорошим выбором для обнуления YMM (AVX1/AVX2) или ZMM (AVX512) или любого будущего расширения вектора. vpxor ymm, ymm, ymm, однако, не требует дополнительных байтов для кодирования и работает аналогично на Intel, но медленнее на AMD до Zen2 (2 моп). Обнуление AVX512 ZMM потребует дополнительных байтов (для префикса EVEX), поэтому предпочтение следует отдавать обнулению XMM или YMM.

Примеры XMM/YMM/ZMM

#Good:
 xorps   xmm0, xmm0         ; smallest code size (for non-AVX)
 pxor    xmm0, xmm0         ; costs an extra byte, runs on any port on Nehalem.
 xorps   xmm15, xmm15       ; Needs a REX prefix but that unavoidable if you need to use high registers without AVX.  Code-size is the only penalty.

   # with AVX
 vpxor xmm0, xmm0, xmm0    ; zeros X/Y/ZMM0
 vpxor xmm15, xmm0, xmm0   ; zeros X/Y/ZMM15, still only 2-byte VEX prefix

 vpxord xmm30, xmm30, xmm30  ; EVEX is unavoidable when zeroing high 16, but still prefer XMM or YMM for fewer uops on probable future AMD.  May be worth it to avoid needing vzeroupper.
 vpxord zmm30, zmm30, zmm30  ; Without AVX512VL you have to use a 512-bit instruction.

#sub-optimal:
 vpxor   xmm15, xmm15, xmm15   ; 3-byte VEX prefix for high source reg
 vpxord  zmm0, zmm0, zmm0      ; EVEX prefix (4 bytes), and a 512-bit uop.

См. . Является ли vxorps -zeroing на AMD Jaguar/Bulldozer/Zen быстрее с регистрами xmm, чем ymm? и
Какой самый эффективный способ очистить один или несколько регистров ZMM в Knights Landing?

Полусвязанный: Самый быстрый способ установить значение __m256 для всех ОДИН битов и
 Эффективно установите все биты в регистре ЦПУ на 1 также охватывает регистры маски AVX512 k0..7.


Что особенного в обнулении идиом, таких как xor, на разных уарчах

Некоторые ЦП распознают sub same,same как идиому обнуления, как xor, но все ЦП, которые распознают любые идиомы обнуления, распознают xor. Просто используйте xor, чтобы вам не приходилось беспокоиться о том, какой процессор распознает идиому обнуления.

xor (в отличие от mov reg, 0 - признанная идиома обнуления) имеет некоторые очевидные и некоторые тонкие преимущества (краткий список, затем я остановлюсь на них):

  • меньший размер кода, чем mov reg,0. (Все процессоры)
  • избегает частичной регистрации штрафов для последующего кода. (Intel P6-семейство и SnB-семейство).
  • не использует исполнительный блок, экономя энергию и освобождая ресурсы выполнения. (Семейство Intel SnB)
  • меньший uop (без непосредственных данных) оставляет место в строке кэша uop для соседних инструкций для заимствования при необходимости. (Семейство Intel SnB).
  • не использует записи в файле физического регистра. (По крайней мере, семейство Intel SnB (и P4), возможно, и AMD, поскольку они используют аналогичную схему PRF вместо сохранения состояния регистра в ROB, как микроархитектуры семейства Intel P6.)

Меньший размер машинного кода (2 байта вместо 5) всегда является преимуществом: более высокая плотность кода приводит к меньшему количеству пропусков кэша команд, а также к лучшему извлечению команд и, возможно, декодированию полосы пропускания.


Преимущество в том, что не используется исполнительный модуль для xor в микроархитектурах семейства Intel SnB, незначительно, но экономит энергию. Скорее всего, это имеет значение для SnB или IvB, которые имеют только 3 исполнительных порта ALU. У Haswell и более поздних версий есть 4 порта выполнения, которые могут обрабатывать целочисленные инструкции ALU, включая mov r32, imm32, поэтому при идеальном принятии решений планировщиком (что не всегда происходит на практике) HSW может поддерживать 4 мопа за такт, даже когда они все нужны порты выполнения ALU.

Смотрите мой ответ на другой вопрос об обнулении регистров для более подробной информации.

Сообщение в блоге Брюса Доусона, на которое ссылался Майкл Петч (в комментарии к вопросу), указывает на то, что xor обрабатывается на этапе переименования регистра без необходимости выполнения модуля (ноль мопов в неиспользованном домене), но пропускает Дело в том, что это еще один моп в слитном домене. Современные процессоры Intel могут выдавать & удалите 4 uops слитых доменов за часы. Вот откуда берутся 4 ноля за такт. Повышенная сложность аппаратного переименования регистров является лишь одной из причин ограничения ширины дизайна до 4. (Брюс написал несколько очень хороших постов в блоге, например, его серии по математике FP и x87/SSE/округлению, что я очень рекомендую).


В процессорах семейства AMD Bulldozermov immediate работает на тех же целочисленных исполнительных портах EX0/EX1, что и xor. mov reg,reg также может работать на AGU0/1, но только для копирования регистров, а не для настройки из немедленных. Так что AFAIK, для AMD единственным преимуществом xor перед mov является более короткое кодирование. Это также может сэкономить ресурсы физического регистра, но я не видел никаких тестов.


Распознаваемые идиомы обнуления позволяют избежать штрафов за частичные регистры на процессорах Intel, которые переименовывают частичные регистры отдельно от полных регистров (семейства P6 и SnB).

xor помечает регистр как обнуленные верхние части, поэтому xor eax, eax/inc al/inc eax избегает обычного штрафа за частичный регистр, который имеют ЦП до IvB. Даже без xor IvB нужен только объединяющийся моп, когда старшие 8 бит (AH) модифицируются, а затем читается весь регистр, и Haswell даже удаляет это.

Из руководства по микроархам Agner Fog, стр. 98 (раздел Pentium M, на который ссылаются более поздние разделы, включая SnB):

Процессор распознает XOR регистра с самим собой в качестве настройки это к нулю. Специальный тег в реестре запоминает, что старшая часть регистра равна нулю, так что EAX = AL. Этот тег запоминается даже в цикле:

    ; Example    7.9. Partial register problem avoided in loop
    xor    eax, eax
    mov    ecx, 100
LL:
    mov    al, [esi]
    mov    [edi], eax    ; No extra uop
    inc    esi
    add    edi, 4
    dec    ecx
    jnz    LL

(из pg82): процессор помнит, что старшие 24 бита EAX равны нулю, пока вы не получаете прерывание, неправильное предсказание или другое событие сериализации.

pg82 этого руководства также подтверждает, что mov reg, 0 не распознается как идиома обнуления, по крайней мере, в ранних проектах P6, таких как PIII или PM. Я был бы очень удивлен, если бы они потратили транзисторы на обнаружение этого на более поздних процессорах.


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

Было бы неплохо, если бы x86-64 переназначил один из удаленных кодов операций (например, AAM) для 16/32/64 бита setcc r/m, с предикатом, закодированным в 3-битном поле исходного регистра регистра r/m (как некоторые другие однооперационные инструкции используют их как биты кода операции). Но они этого не сделали, и это все равно не помогло бы x86-32.

В идеале вы должны использовать xor/установить флаги/setcc/читать полный регистр:

...
call  some_func
xor     ecx,ecx    ; zero *before* the test
test    eax,eax
setnz   cl         ; cl = (some_func() != 0)
add     ebx, ecx   ; no partial-register penalty here

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

Все становится сложнее, когда вы не хотите делать xor перед инструкцией по установке флага. например Вы хотите выполнить ветвление с одним условием, а затем установитьcc с другим условием с теми же флагами. например cmp/jle, sete, и у вас либо нет запасного регистра, либо вы хотите полностью исключить xor из пути неиспользованного кода.

Нет признанных идиом, которые не влияют на флаги, поэтому лучший выбор зависит от целевой микроархитектуры. На Core2 вставка объединяющего Uop может вызвать 2 или 3 цикла остановки. Похоже, что на SnB дешевле, но я не тратил много времени, пытаясь измерить. Использование mov reg, 0/setcc приведет к значительным потерям на старых процессорах Intel, и все же будет несколько хуже для новых Intel.

Использование setcc/movzx r32, r8, вероятно, является лучшей альтернативой для Intel P6 & Семейства SnB, если вы не можете выполнить xor-zero перед инструкцией по установке флага. Это должно быть лучше, чем повторять тест после xor -zeroing. (Даже не рассматривайте sahf/lahf или pushf/popf). IvB может устранить movzx r32, r8 (то есть обработать его с помощью переименования регистров без единицы выполнения или задержки, как xor -zeroing). Haswell и более поздние версии исключают только обычные инструкции mov, поэтому movzx принимает единицу выполнения и имеет ненулевую задержку, что делает test/setcc/movzx хуже, чем xor/test/setcc, но все же по крайней мере так же хорошо, как test/mov r,0/setcc (и намного лучше на старых процессорах).

Использование setcc/movzx без обнуления в первую очередь плохо для AMD/P4/Silvermont, потому что они не отслеживают deps отдельно для подрегистров. Там будет ложное депо на старое значение регистра. Использование mov reg, 0/setcc для обнуления/устранения зависимостей, вероятно, является наилучшей альтернативой, когда xor/test/setcc не подходит.

Конечно, если вам не нужен вывод setcc шире 8 бит, вам не нужно ничего обнулять. Однако остерегайтесь ложных зависимостей на процессорах, отличных от P6/SnB, если вы выбрали регистр, который недавно был частью длинной цепочки зависимостей. (И остерегайтесь частичного перезапуска или дополнительного запуска, если вы вызываете функцию, которая может сохранить/восстановить регистр, часть которого вы используете.)


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

См. http://agner.org/optimize/ для документации по микроарху, включая то, что обнуление идиом распознается как нарушение зависимости (например, sub same,same на некоторых, но не на всех процессорах, тогда как xor same,same распознается на всех.) mov действительно разрывает цепочку зависимостей на старое значение регистра (независимо от исходного значения, ноль или нет, потому что так работает mov). xor разрывает цепочки зависимостей только в особом случае, когда src и dest - это один и тот же регистр, поэтому mov исключен из списка специально распознаваемых прерывателей зависимостей. (Кроме того, потому что это не признано как идиома обнуления, с другими преимуществами, которые несет.)

Интересно, что самый старый дизайн P6 (PPro через Pentium III) не признавал xor -zeroing в качестве прерывателя зависимости, а только в качестве идиомы обнуления в целях избежания частичных регистров, поэтому в некоторых случаях это стоило используя mov и затем xor -zeroing в этом порядке, чтобы разбить dep и затем снова обнулить + установить внутренний бит тега, чтобы старшие биты были равны нулю, поэтому EAX = AX = AL. (См. пример Agner Fog 6.17 в своем микроархиве pdf. Он говорит, что это также относится к P2, P3 и даже (рано?) PM. Комментарий к связанному сообщению в блоге говорит, что только PPro имел это упущение, но я тестировал на Katmai PIII, а @Fanael тестировал на Pentium M, и мы оба обнаружили, что он не нарушает зависимость для цепочки с задержкой imul. Это, к сожалению, подтверждает результаты Agner Fog.)


Если это действительно делает ваш код более приятным или сохраняет инструкции, то, конечно, обнуляйте с помощью mov, чтобы не касаться флагов, если вы не представляете проблему с производительностью, отличную от размера кода. Тем не менее, избегание затуманивания флагов - единственная разумная причина не использовать xor.