Каков наилучший способ установить регистр в ноль в сборке 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
, хоть.