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

Все следующие инструкции делают то же самое: set %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.

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

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

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

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


Некоторые процессоры распознают 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 мопов за такт, даже если им всем нужны порты выполнения.

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

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


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


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

xor будет помечать регистр как обнуленные верхние части, так xor eax, eax / inc al / inc eax избегает обычного штрафа за частичный регистр, который имеют CPU до IvB. Даже без xor, IvB нужен только слияние UOP, когда высокие 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 перед инструкцией по установке флага. например, вы хотите выполнить ветвление с одним условием, а затем установить с помощью ccc другое условие с теми же флагами. например 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. (Даже не рассматривайте sahf / lahf или же pushf / popf). IvB может устранить movzx r32, r8 (т. е. обрабатывать это с помощью переименования регистров без единицы выполнения или задержки, например, обнуление нуля). Haswell и позже только устранить регулярные mov инструкции, так movzx принимает модуль выполнения и имеет ненулевую задержку, делая test /setcc/movzx хуже чем xor/тестовое задание/setcc, но все же, по крайней мере, так же хорошо, как тест /mov r,0/setcc (и намного лучше на старых процессорах).

С помощью setcc / movzx отсутствие нуля в первую очередь плохо для AMD/P4/Silvermont, потому что они не отслеживают deps отдельно для подрегистров. Там будет ложное депо на старое значение регистра. С помощью mov reg, 0/setcc для обнуления / нарушения зависимости, вероятно, является лучшей альтернативой, когда xor/тестовое задание/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) не узнал xor- обнулять как нарушитель зависимостей, только в качестве идиомы обнуления в целях избежания частичных регистров, так что в некоторых случаях стоило использовать оба. (См. Пример Агнера Фога 6.17. В его микроархиве pdf. Он утверждает, что это также относится к P2, P3 и даже (рано?) PM, но я скептически отношусь к этому. Комментарий к сообщению в блоге говорит, что это был только PPro у него был этот недосмотр. Кажется действительно маловероятным, чтобы существовало несколько поколений семейства P6, не признав обнуление ксором как прерыватель депо.)


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

Другие вопросы по тегам