Любой способ переместить 2 байта в 32-битной x86 с использованием MOV, не вызывая переключения режима или остановки процессора?
Если я хочу переместить 2 неподписанных байта из памяти в 32-разрядный регистр, могу ли я сделать это с помощью MOV
инструкция и нет режима переключения?
Я заметил, что вы можете сделать это с MOVSE
а также MOVZE
инструкции. Например, с MOVSE
кодировка 0F B7
перемещает 16 бит в 32-битный регистр. Это инструкция из 3 циклов.
В качестве альтернативы, я думаю, я мог бы переместить 4 байта в регистр, а затем каким-то образом CMP только два из них как-то.
Какая самая быстрая стратегия для получения и сравнения 16-битных данных на 32-битной x86? Обратите внимание, что я в основном делаю 32-битные операции, поэтому я не могу переключиться в 16-битный режим и оставаться там.
К сведению непосвященных: проблема в том, что 32-разрядные процессоры Intel x86 могут MOV
8-битные данные и 16-битные ИЛИ 32-битные данные в зависимости от того, в каком режиме они находятся. Этот режим называется настройкой "D-бит". Вы можете использовать специальные префиксы 0x66 и 0x67, чтобы использовать режим не по умолчанию. Например, если вы находитесь в 32-разрядном режиме и перед командой указывается 0x66, это приведет к тому, что операнд будет считаться 16-разрядным. Единственная проблема заключается в том, что это приводит к значительному снижению производительности.
2 ответа
movzx
Медленно работает на древней микроархитектуре P5 (оригинальный Pentium), а не в этом столетии. Процессоры под маркой Pentium, основанные на новейших микроархитектурах, таких как Pentium G3258 (Haswell, выпуск оригинального Pentium с 20-й годовщиной), представляют собой совершенно разные звери и работают как эквивалентные i3, но без AVX, BMI2 или гиперпоточности.
Не настраивайте современный код, основанный на рекомендациях / цифрах P5. Однако Knight's Corner (Xeon Phi) основан на модифицированной микроархитектуре P54C, поэтому, возможно, он имеет медленную movzx
также. Ни Agner Fog, ни Instlatx64 не имеют числа пропускной способности / задержки для каждой команды для KNC.
Использование инструкции размера 16-битного операнда не переводит весь конвейер в 16-битный режим и не вызывает большого успеха. См . Pdf-файл Agner Fog для изучения микроархитектуры процессоров x86 (включая такие же старые, как Intel P5 (оригинальный Pentium), о которых вы почему-то говорите).
Запись 16-разрядного регистра, а затем чтение полного 32- и 64-разрядного регистра выполняется медленно на некоторых процессорах (частичная остановка регистра при объединении в семействе Intel P6). В других случаях запись 16-битного регистра сливается со старым значением, поэтому при записи существует ложная зависимость от старого значения полного регистра, даже если вы никогда не читаете полный регистр. (Обратите внимание, что Haswell/Skylake переименовывает только AH отдельно, в отличие от Sandybridge, который (подобно Core2/Nehalem) также переименовывает AL / AX отдельно от RAX, но сливается без остановки.)
Если вы не особенно заботитесь о порядке P5 (или, возможно, Knight's Corner Xeon Phi, на основе того же ядра, но IDK, если movzx
там тоже медленно) ИСПОЛЬЗУЙТЕ ЭТО:
movzx eax, word [src1] ; as efficient as a 32-bit MOV load on most CPUs
cmp ax, word [src2]
Префикс размера операнда для cmp
эффективно декодирует на всех современных процессорах. Чтение 16-битного регистра после записи полного регистра всегда хорошо, и 16-битная загрузка для другого операнда также хороша.
Префикс размера операнда не меняет длину, потому что там нет imm16 / imm32. например cmp word [src2], 0x7F
хорошо (он может использовать расширенный знак imm8), но cmp word [src2], 0x80
нужен imm16 и будет зависать LCP на некоторых процессорах Intel. (Без префикса размера операнда тот же код операции имел бы imm32, то есть остальная часть инструкции была бы другой длины). Вместо этого используйте mov eax, 0x80
/ cmp word [src2], ax
,
Префикс размера адреса может изменяться по длине в 32-битном режиме (disp32 или disp16), но мы не хотим использовать 16-битные режимы адресации для доступа к 16-битным данным. Мы все еще используем [ebx+1234]
(или же rbx
) не [bx+1234]
,
На современных x86: Intel P6 / семейство SnB / Atom / Silvermont, AMD начиная с версии K7, то есть все, что было сделано в этом веке, новее, чем реальный P5 Pentium, movzx
нагрузки очень эффективны.
На многих процессорах порты загрузки напрямую поддерживают movzx
(а иногда и movsx
), поэтому он работает только как load uop, а не как load + ALU.
Данные из таблиц набора команд Agner Fog: обратите внимание, что они могут не охватывать каждый угловой случай, например mov
Числа -load могут быть только для 32/64-битных загрузок. Также обратите внимание, что значения задержки загрузки Agner Fog не являются задержкой загрузки из кэша L1D; они имеют смысл только как часть задержки сохранения / перезагрузки (переадресации магазина), но относительные числа скажут нам, сколько циклов movzx
добавляет поверх mov
(часто без лишних циклов).
- P5 Pentium (в порядке исполнения):
movzx
-load - это 3-тактная инструкция (плюс узкое место декодирования из0F
префикс), противmov
-нагрузки, являющиеся пропускной способностью одного цикла. (Они все еще имеют задержку, хотя). - Intel:
- PPro / Pentium II / III:
movzx
/movsx
работать только на порту загрузки, такой же пропускной способности, как обычныйmov
, - Core2 / Nehalem: то же самое, кроме
movsxd r64, m
на Нехалеме, по-видимому, нужен АЛУ (без микроплавкого предохранителя). Возможно, Core2 такой же, но Агнер этого не проверял. - Семья Sandybridge (SnB через Skylake и позже):
movzx
/movsx
нагрузки однопроцессорные (просто порт загрузки) и выполняются идентичноmov
грузы. - Pentium4 (нетберт):
movzx
работает только на порту загрузки, такой же, какmov
,movsx
нагрузка + ALU, и занимает 1 дополнительный цикл. - Атом (по порядку): таблица Агнера неясна для источника памяти
movzx
/movsx
нужен ALU, но они определенно быстрые. Номер латентности только для рег, рег. - Silvermont: так же, как Atom: быстро, но неясно, нужен ли порт.
KNL (на основе Silvermont): списки Агнера
movzx
/movsx
с источником памяти, использующим IP0 (ALU), но задержка равнаmov r,m
так что нет штрафа. (Давление в исполнительном блоке не является проблемой, потому что декодеры KNL в любом случае едва могут поддерживать свои 2 ALU).AMD:
- Bobcat:
movzx
/movsx
нагрузки 1 за такт, 5 циклов задержки.mov
-нагрузка составляет 4с задержки. - Jaguar:
movzx
/movsx
нагрузки - 1 за такт, задержка 4 цикла.mov
нагрузки равны 1 на такт, задержка 3c для 32/64-битной или 4c дляmov r8/r16, m
(но все же только порт AGU, а не объединение ALU, как это делают Haswell/Skylake). - K7 / K8 / K10:
movzx
/movsx
нагрузки имеют пропускную способность 2 на такт, задержка на 1 цикл выше, чемmov
нагрузки. Они используют AGU и ALU. - Бульдозерная семья: такая же, как К10, но
movsx
-нагрузка имеет 5 циклов задержки.movzx
-нагрузка имеет 4 цикла задержки,mov
-нагрузка имеет 3 цикла задержки. Так что в теории это может быть меньше задержкиmov cx, word [mem]
а потомmovsx eax, cx
(1 цикл), если ложная зависимость от 16-битнойmov
load не требует дополнительного слияния ALU или создания переносимой зависимости для вашего цикла. - Ryzen:
movzx
/movsx
загрузка выполняется только в порту загрузки, с той же задержкой, что иmov
грузы. - С ПОМОЩЬЮ
- Via Nano 2000/3000:
movzx
работает только на порту загрузки, с той же задержкой, что иmov
грузы.movsx
это LD + ALU, с дополнительной задержкой 1с.
Когда я говорю "выполнять одинаково", я имею в виду не считать каких-либо частичных регистров штрафов или разбиений строки кэша от более широкой загрузки. например movzx eax, word [rsi]
избегает слияния штраф против mov ax, word [rsi]
на Skylake, но я все еще скажу, что mov
выполняет идентично movzx
, (Я думаю, я имею в виду, что mov eax, dword [rsi]
без каких-либо разделений строки кэша так же быстро, как movzx eax, word [rsi]
.)
xor
- обнуление полного регистра перед записью 16-разрядного регистра позволяет избежать более поздней остановки слияния частичных регистров в семействе Intel P6, а также нарушить ложные зависимости.
Если вы хотите хорошо работать и на P5, это может быть несколько лучше, но не намного хуже на любых современных процессорах, кроме PPro-PIII, где xor
-zeroing не вызывает прерывания, даже если он все еще распознается как идиома обнуления, делающая EAX эквивалентной AX (нет частичной регистрации регистра при чтении EAX после записи AL или AX).
;; Probably not a good idea, maybe not faster on anything.
;mov eax, 0 ; some code tuned for PIII used *both* this and xor-zeroing.
xor eax, eax ; *not* dep-breaking on early P6 (up to PIII)
mov ax, word [src1]
cmp ax, word [src2]
; safe to read EAX without partial-reg stalls
Префикс размера операнда не идеален для P5, поэтому вы можете рассмотреть возможность использования 32-битной загрузки, если вы уверены, что он не сбоит, пересекает границу строки кэша или вызывает сбой пересылки из-за недавнего 16-битный магазин.
На самом деле, я думаю, что 16-битный mov
На Pentium загрузка может быть медленнее, чем movzx
/ cmp
2 последовательности команд. Похоже, что для работы с 16-битными данными так же эффективно, как и с 32-битными, не существует! (Конечно, кроме упакованного MMX).
См. Руководство Agner Fog для деталей Pentium, но префикс размера операнда занимает дополнительные 2 цикла для декодирования на P1 (оригинал P5) и PMMX, так что эта последовательность может фактически быть хуже, чем movzx
нагрузки. На P1 (но не PMMX) 0F
escape-байт (используется movzx
) также считается префиксом, принимая дополнительный цикл для декодирования.
По-видимому movzx
не в любом случае. Multi-цикл movzx
будет скрывать задержку декодирования cmp ax, [src2]
, так movzx
/ cmp
вероятно, все еще лучший выбор. Или график инструкции, чтобы movzx
сделано раньше и cmp
может может быть что-то в паре с В любом случае, правила планирования довольно сложны для P1/PMMX.
Я рассчитал этот цикл на Core2 (Conroe), чтобы доказать, что обнуление по xor позволяет избежать частичных остановок регистров для 16-битных регистров, а также low-8 (как для setcc al
):
mov ebp, 100000000
ALIGN 32
.loop:
%rep 4
xor eax, eax
; mov eax, 1234 ; just break dep on the old value, not a zeroing idiom
mov ax, cx ; write AX
mov edx, eax ; read EAX
%endrep
dec ebp ; Core2 can't fuse dec / jcc even in 32-bit mode
jg .loop ; but SnB does
perf stat -r4 ./testloop
вывод для этого в статическом двоичном файле, который делает системный вызов sys_exit после:
;; Core2 (Conroe) with XOR eax, eax
469,277,071 cycles # 2.396 GHz
1,400,878,601 instructions # 2.98 insns per cycle
100,156,594 branches # 511.462 M/sec
9,624 branch-misses # 0.01% of all branches
0.196930345 seconds time elapsed ( +- 0.23% )
2,98 инструкций за цикл имеет смысл: 3 порта ALU, все инструкции ALU, и нет макросов, поэтому каждый из них равен 1 моп. Таким образом, мы работаем на 3/4 от входной мощности. Цикл имеет 3*4 + 2
инструкции / упс.
На Core2 все сильно отличается xor
обнуление прокомментировал и используя mov eax, imm32
вместо:
;; Core2 (Conroe) with MOV eax, 1234
1,553,478,677 cycles # 2.392 GHz
1,401,444,906 instructions # 0.90 insns per cycle
100,263,580 branches # 154.364 M/sec
15,769 branch-misses # 0.02% of all branches
0.653634874 seconds time elapsed ( +- 0.19% )
0,9 IPC (по сравнению с 3) соответствует внешнему сбою в течение 2–3 циклов для вставки слияния в каждую mov edx, eax
,
Skylake запускает обе петли одинаково, потому что mov eax,imm32
все еще разрушает зависимость. (Как и большинство инструкций с назначением только для записи, но остерегайтесь ложных зависимостей от popcnt
а также lzcnt
/ tzcnt
).
На самом деле, uops_executed.thread
Счетчик perf показывает разницу: в SnB-семействе обнуление xor не требует выполнения, потому что обрабатывается на этапе выпуска / переименования. (mov edx,eax
также устраняется при переименовании, так что количество мопов на самом деле довольно мало). Количество циклов одинаково с точностью до 1% в любом случае.
;;; Skylake (i7-6700k) with xor-zeroing
Performance counter stats for './testloop' (4 runs):
84.257964 task-clock (msec) # 0.998 CPUs utilized ( +- 0.21% )
0 context-switches # 0.006 K/sec ( +- 57.74% )
0 cpu-migrations # 0.000 K/sec
3 page-faults # 0.036 K/sec
328,337,097 cycles # 3.897 GHz ( +- 0.21% )
100,034,686 branches # 1187.243 M/sec ( +- 0.00% )
1,400,195,109 instructions # 4.26 insn per cycle ( +- 0.00% ) ## dec/jg fuses into 1 uop
1,300,325,848 uops_issued_any # 15432.676 M/sec ( +- 0.00% ) ### fused-domain
500,323,306 uops_executed_thread # 5937.994 M/sec ( +- 0.00% ) ### unfused-domain
0 lsd_uops # 0.000 K/sec
0.084390201 seconds time elapsed ( +- 0.22% )
lsd.uops равен нулю, потому что буфер цикла отключен обновлением микрокода. Это узкие места на входе: uops (fused-domain) / clock = 3.960 (из 4). Этот последний.04 может быть частично перегружен ОС (прерывания и т. Д.), Потому что это только подсчет мопов пользовательского пространства.
Придерживаться 32-битного режима и использовать 16-битные инструкции
mov eax, 0 ; clear the register
mov ax, 10-binary ; do 16 bit stuff
В качестве альтернативы, я думаю, я мог бы переместить 4 байта в регистр, а затем каким-то образом CMP только два из них
mov eax, xxxx ; 32 bit num loaded
mov ebx, xxxx
cmp ax, bx ; 16 bit cmp performed in 32 bit mode