Мне любопытно, как много способов установить регистр в ноль в сборке x86. Использование одной инструкции. Кто-то сказал мне, что ему удалось найти по меньшей мере 10 способов сделать это.
Ответ 2
См. этот ответ для наилучшего способа для нулевых регистров: xor eax,eax
(преимущества производительности и меньшая кодировка).
Я рассмотрю только то, как одна команда может обнулить регистр. Есть слишком много способов, если вы разрешаете загружать нуль из памяти, поэтому мы будем главным образом исключать инструкции, которые загружаются из памяти.
Я нашел 10 разных одиночных инструкций, которые обнулили 32-битный регистр (и, следовательно, полный 64-битный регистр в длинном режиме), без предварительных условий или нагрузок из любой другой памяти. Это не считая разных кодировок одного и того же insn или разных форм mov
. Если вы подсчитываете загрузку из памяти, которая, как известно, содержит нуль или регистры сегментов или что-то еще, есть лодка. Существует также миллион способов для нулевых векторных регистров.
Для большинства из них версии eax и rax представляют собой отдельные кодировки для одной и той же функциональности, обе обнуляющие полные 64-разрядные регистры, неинвертирующую верхнюю половину или явно записывая полный регистр с префиксом REX.W.
Целочисленные регистры:
# Works on any reg unless noted, usually of any size. eax/ax/al as placeholders
and eax, 0 ; three encodings: imm8, imm32, and eax-only imm32
andn eax, eax,eax ; BMI1 instruction set: dest = ~s1 & s2
imul eax, any,0 ; eax = something * 0. two encodings: imm8, imm32
lea eax, [0] ; absolute encoding (disp32 with no base or index). Use [abs 0] in NASM if you used DEFAULT REL
lea eax, [rel 0] ; YASM supports this, but NASM doesn't: use a RIP-relative encoding to address a specific absolute address, making position-dependent code
mov eax, 0 ; 5 bytes to encode (B8 imm32)
mov rax, strict dword 0 ; 7 bytes: REX mov r/m64, sign-extended-imm32. NASM optimizes mov rax,0 to the 5B version, but dword or strict dword stops it for some reason
mov rax, strict qword 0 ; 10 bytes to encode (REX B8 imm64). movabs mnemonic for AT&T. normally assemblers choose smaller encodings if the operand fits, but strict qword forces the imm64.
sub eax, eax ; recognized as a zeroing idiom on some but maybe not all CPUs
xor eax, eax ; Preferred idiom: recognized on all CPUs
@movzx:
movzx eax, byte ptr[@movzx + 6] //Because the last byte of this instruction is 0. neat hack from GJ. answer
.l: loop .l ; clears e/rcx... eventually. from I. J. Kennedy answer. To operate on only ECX, use an address-size prefix.
; rep lodsb ; not counted because it not safe (potential segfaults), but also zeros ecx
"Сдвиг всех битов на один конец" невозможно для регистров GP обычного размера, только частичные регистры. shl
и shr
значения сдвига маскируются: count &= 31;
, что эквивалентно count %= 32;
. (Но 286 и более ранние - только 16 бит, поэтому ax
является "полным" регистром. Форма shr r/m16, imm8
переменной-счетчика команды была добавлена 286, поэтому были процессоры, в которых сдвиг может нулевать полный целочисленный регистр. )
Также обратите внимание, что сдвиговые значения для векторов насыщаются вместо обертывания.
# Zeroing methods that only work on 16bit or 8bit regs:
shl ax, 16 ; shift count is still masked to 0x1F for any operand size less than 64b. i.e. count %= 32
shr al, 16 ; so 8b and 16b shifts can zero registers.
# zeroing ah/bh/ch/dh: Low byte of the reg = whatever garbage was in the high16 reg
movxz eax, ah ; From Jerry Coffin answer
В зависимости от других существующих условий (кроме нуля в другом регистре):
bextr eax, any, eax ; if al >= 32, or ah = 0. BMI1
BLSR eax, src ; if src only has one set bit
CDQ ; edx = sign-extend(eax)
sbb eax, eax ; if CF=0. (Only recognized on AMD CPUs as dependent only on flags (not eax))
setcc al ; with a condition that will produce a zero based on known state of flags
PSHUFB xmm0, all-ones ; xmm0 bytes are cleared when the mask bytes have their high bit set
vector regs:
Некоторые из этих SSE2-целых инструкций могут также использоваться для регистров MMX (mm0
- mm7
). Опять же, лучшим выбором является некоторая форма xor. Либо PXOR
/VPXOR
, либо XORPS
/VXORPS
.
AVX vxorps xmm0,xmm0,xmm0
нули полный ymm0/zmm0 и лучше, чем vxorps ymm0,ymm0,ymm0
для процессоров AMD. Эти команды обнуления имеют три кодировки: устаревшие SSE, AVX (префикс VEX) и AVX512 (префикс EVEX), хотя версия SSE только нули нижней 128, что не является полным регистром на процессорах, поддерживающих AVX или AVX512. Во всяком случае, в зависимости от того, как вы считаете, каждая запись может быть трех разных инструкций (такой же код операции, хотя и разные префиксы). За исключением vzeroall
, который AVX512 не изменился (и не равен нулю zmm16-31).
ANDNPD xmm0, xmm0
ANDNPS xmm0, xmm0
PANDN xmm0, xmm0 ; dest = ~dest & src
PCMPGTB xmm0, xmm0 ; n > n is always false.
PCMPGTW xmm0, xmm0 ; similarly, pcmpeqd is a good way to do _mm_set1_epi32(-1)
PCMPGTD xmm0, xmm0
PCMPGTQ xmm0, xmm0 ; SSE4.2, and slower than byte/word/dword
PSADBW xmm0, xmm0 ; sum of absolute differences
MPSADBW xmm0, xmm0, 0 ; SSE4.1. sum of absolute differences, register against itself with no offset. (imm8=0: same as PSADBW)
; shift-counts saturate and zero the reg, unlike for GP-register shifts
PSLLDQ xmm0, 16 ; left-shift the bytes in xmm0
PSRLDQ xmm0, 16 ; right-shift the bytes in xmm0
PSLLW xmm0, 16 ; left-shift the bits in each word
PSLLD xmm0, 32 ; double-word
PSLLQ xmm0, 64 ; quad-word
PSRLW/PSRLD/PSRLQ ; same but right shift
PSUBB/W/D/Q xmm0, xmm0 ; subtract packed elements, byte/word/dword/qword
PSUBSB/W xmm0, xmm0 ; sub with signed saturation
PSUBUSB/W xmm0, xmm0 ; sub with unsigned saturation
PXOR xmm0, xmm0
XORPD xmm0, xmm0
XORPS xmm0, xmm0
VZEROALL
# Can raise an exception on SNaN, so only usable if you know exceptions are masked
CMPLTPD xmm0, xmm0 # exception on QNaN or SNaN, or denormal
VCMPLT_OQPD xmm0, xmm0,xmm0 # exception only on SNaN or denormal
CMPLT_OQPS ditto
VCMPFALSE_OQPD xmm0, xmm0, xmm0 # This is really just another imm8 predicate value fro the same VCMPPD xmm,xmm,xmm, imm8 instruction. Same exception behaviour as LT_OQ.
SUBPS xmm0, xmm0
и подобное не будет работать, потому что NaN-NaN = NaN, а не ноль.
Кроме того, инструкции FP могут вызывать исключения из аргументов NaN, поэтому даже CMPPS/PD безопасен, если вы знаете, что исключения маскируются, и вам не нужно устанавливать биты исключений в MXCSR. Даже версия AVX с расширенным выбором предикатов поднимет #IA
на SNaN. "Тихие" предикаты только подавляют #IA
для QNaN. CMPPS/PD также может вызвать исключение Denormal.
(см. таблицу в запись insn set ref для CMPPD или, предпочтительно, в оригинальном PDF файле Intel, так как выдержка HTML управляет этой таблицей. )
AVX512:
Здесь, вероятно, есть несколько вариантов, но сейчас я не достаточно любопытен, чтобы перебирать список наборов инструкций, ища их всех.
Есть один интересный, который стоит упомянуть: VPTERNLOGD/Q может установить регистр all-ones вместо, с imm8 = 0xFF. (Но имеет ложную зависимость от старого значения, от текущих реализаций). Поскольку инструкции сравнения все сравниваются с маской, VPTERNLOGD, по-видимому, является лучшим способом установить вектор для всех-на Skylake-AVX512 в моем тестировании, хотя это не случайный случай imm8 = 0xFF, чтобы избежать ложной зависимости.
VPTERNLOGD zmm0, zmm0,zmm0, 0 ; inputs can be any registers you like.
x87 FP:
Только один выбор (потому что sub не работает, если старое значение было бесконечным или NaN).
FLDZ ; push +0.0
Ответ 3
Еще пара возможностей:
sub ax, ax
movxz, eax, ah
Изменить: я должен отметить, что movzx
не нулевое все eax
- он просто равен нулю ah
(плюс верхние 16 бит, которые не доступны как сами регистры).
Что касается самого быстрого, если память поддерживает sub
и xor
, то они эквивалентны. Они быстрее, чем другие (другие), потому что они достаточно распространены, что разработчики процессоров добавили для них специальную оптимизацию. В частности, при нормальном sub
или xor
результат зависит от предыдущего значения в регистре. CPU распознает xor-with-self и вычитает из себя, поэтому он знает, что цепочка зависимостей там сломана. Любые инструкции после этого не будут зависеть от предыдущего значения, поэтому он может выполнять предыдущие и последующие инструкции параллельно с помощью регистров переименования.
Особенно на более старых процессорах мы ожидаем, что "mov reg, 0" будет медленнее просто потому, что у него есть дополнительные 16 бит данных, а большинство ранних процессоров (особенно 8088) были ограничены в первую очередь их способностью загружать поток из памяти - на самом деле, на 8088 вы можете довольно точно оценить время выполнения с любыми ссылочными листами и просто обратить внимание на количество задействованных байтов. Это нарушает инструкции div
и idiv
, но об этом. OTOH, я должен, вероятно, заткнуться, так как 8088 действительно мало интересует кого-либо (в течение по крайней мере десятилетия).