Какие методы могут быть использованы для эффективного увеличения длины инструкции на современном x86?
Представьте, что вы хотите выровнять серию инструкций по сборке x86 по определенным границам. Например, вы можете выровнять циклы по 16 или 32-байтовой границе или упаковать инструкции, чтобы они эффективно помещались в кэш uop или что-то еще.
Простейшим способом достижения этого являются однобайтовые инструкции NOP, за которыми следуют многобайтовые NOP. Хотя последний метод, как правило, более эффективен, ни один из методов не является бесплатным: NOP используют внешние ресурсы выполнения, а также учитывают ваш предел переименования в 4 для всей системы на современном x86.
Другой вариант - как-то удлинить некоторые инструкции, чтобы получить желаемое выравнивание. Если это делается без введения новых киосков, это кажется лучше, чем подход NOP. Как можно эффективнее выполнять инструкции на последних процессорах x86?
В идеальном мире техники удлинения были бы одновременно:
- Применимо к большинству инструкций
- Способен удлинить инструкцию на переменную величину
- Не останавливать или иным образом замедлять работу декодеров
- Быть эффективно представленным в кэше UOP
Маловероятно, что существует один метод, который одновременно удовлетворяет всем вышеперечисленным пунктам, поэтому хорошие ответы, вероятно, помогут решить различные проблемы.
1 Лимит составляет 5 или 6 драмов в Ryzen.
4 ответа
Подумайте об умеренной игре в код, чтобы уменьшить код, а не расширять его, особенно перед циклом. например xor eax,eax
/ cdq
если вам нужны два обнуленных регистра, или mov eax, 1
/ lea ecx, [rax+1]
чтобы установить регистры в 1 и 2 всего в 8 байтах вместо 10. См. Установите все биты в регистре ЦП в 1 эффективно для получения дополнительной информации об этом, и Советы по игре в гольф в машинном коде x86/x64 для более общих идей. Возможно, вы все еще хотите избежать ложных зависимостей.
Или заполните дополнительное пространство, создавая векторную константу на лету вместо загрузки ее из памяти. (Однако добавление большего давления в uop-кеш может быть хуже для более крупного цикла, содержащего ваш цикл setup + внутренний цикл. Но он предотвращает пропуски d-кеша для констант, поэтому у него есть потенциал для компенсации выполнения большего числа мопов.)
Если вы еще не использовали их для загрузки "сжатых" констант, pmovsxbd
, movddup
, или же vpbroadcastd
длиннее чем movaps
, Загрузка трансляций dword / qword бесплатна (нет ALU, просто загрузка).
Если вы вообще беспокоитесь о выравнивании кода, вы, вероятно, беспокоитесь о том, как он находится в кэше L1I или где находятся границы uop-кэша, так что простого подсчета общего количества мопов уже недостаточно, и несколько дополнительных мопов в блокировка перед тем, о ком вы заботитесь, может не быть проблемой вообще.
Но в некоторых ситуациях вы, возможно, захотите оптимизировать пропускную способность декодирования / использование uop-кэша / общее количество uops для инструкций перед выравниванием блока, который вы хотите выровнять.
Инструкции по заполнению, как вопрос, заданный для:
У Агнера Фога есть целый раздел на эту тему: "10.6 Делать инструкции длиннее ради выравнивания" в своем руководстве "Оптимизация подпрограмм на языке ассемблера". (The lea
, push r/m64
и идеи SIB оттуда, и я скопировал предложение / фразу или две, в противном случае этот ответ - моя собственная работа, либо другие идеи, либо написанные до проверки руководства Агнера.)
Это не было обновлено для текущих процессоров, хотя: lea eax, [rbx + dword 0]
имеет больше недостатков, чем раньше mov eax, ebx
потому что вы пропускаете нулевую задержку / нет исполнительного блока mov
, Если это не на критическом пути, пойти на это, хотя. просто lea
имеет довольно хорошую пропускную способность, и LEA с большим режимом адресации (и, возможно, даже с некоторыми префиксами сегментов) может быть лучше для пропускной способности декодирования / выполнения, чем mov
+ nop
,
Используйте общую форму вместо краткой (без ModR/M) инструкций, таких как push reg
или же mov reg,imm
, например, использовать 2 байта push r/m64
за push rbx
, Или используйте эквивалентную инструкцию, которая длиннее, например add dst, 1
вместо inc dst
в тех случаях, когда нет недостатков в inc
так что вы уже использовали inc
,
Используйте SIB-байт. Вы можете заставить NASM сделать это, используя один регистр в качестве индекса, например mov eax, [nosplit rbx*1]
( см. также), но это вредит задержке использования нагрузки по сравнению с простым кодированием mov eax, [rbx]
с байтом SIB. Режимы индексированной адресации имеют и другие недостатки в семействе SnB, например, отсутствие ламинирования и не использование порта 7 для магазинов.
Так что лучше всего кодировать base=rbx + disp0/8/32=0
используя ModR/M + SIB без индекса рег. (Кодировка SIB для "без индекса" - это кодировка, которая в противном случае означала бы idx=RSP). [rsp + x]
режимы адресации уже требуют SIB (base=RSP - код перехода, который означает, что есть SIB), и это все время появляется в сгенерированном компилятором коде. Таким образом, есть очень веская причина ожидать, что это будет полностью эффективно для декодирования и выполнения (даже для базовых регистров, отличных от RSP) сейчас и в будущем. Синтаксис NASM не может выразить это, поэтому вам придется кодировать вручную. GNU газовый синтаксис Intel от objdump -d
говорится 8b 04 23 mov eax,DWORD PTR [rbx+riz*1]
для примера Агнера Фога 10.20. (riz
является вымышленной нотацией индекса с нулем, которая означает, что есть SIB без индекса). Я не проверял, принимает ли ГАЗ это как ввод.
Используйте imm32
и / или disp32
форма инструкции, которая нужна только imm8
или же disp0/disp32
, Испытание Агнером Фогом кэша uop Сэндибриджа ( таблица 9.1 руководства по микроархам) показывает, что значение имеет фактическое значение немедленного / смещения, а не количество байтов, используемых в кодировке команд. У меня нет никакой информации о тайнике Ризена.
Итак, NASM imul eax, [dword 4 + rdi], strict dword 13
(10 байт: код операции + modrm + disp32 + imm32) будет использовать категорию 32small, 32small и займет 1 запись в кэше UOP, в отличие от того, если в атрибуте direct или disp32 действительно имеется более 16 значащих бит. (Тогда потребуется 2 записи, а загрузка из кэша UOP потребует дополнительного цикла.)
Согласно таблице Агнера, 8/16/32 малых всегда эквивалентны для SnB. И режимы адресации с регистром одинаковы, независимо от того, нет ли смещения, или он маленький. mov dword [dword 0 + rdi], 123456
занимает 2 записи, так же, как mov dword [rdi], 123456789
, Я не поняла [rdi]
+ full imm32 занял 2 записи, но, видимо, так и есть на SnB.
использование jmp / jcc rel32
вместо rel8
, В идеале старайтесь расширять инструкции в местах, которые не требуют более длинных кодировок перехода за пределы области, которую вы расширяете. Пэд после целей прыжка для более ранних прыжков вперед, пэд до целей прыжка для более поздних прыжков назад, если они близки к необходимости в rel32 где-то еще. т.е. старайтесь избегать заполнения между веткой и ее целью, если только вы не хотите, чтобы эта ветка все равно использовала rel32.
Вы можете испытать желание закодировать mov eax, [symbol]
как 6 байт a32 mov eax, [abs symbol]
в 64-битном коде, используя префикс размера адреса, чтобы использовать 32-битный абсолютный адрес. Но это приводит к задержке префикса изменения длины при декодировании на процессорах Intel. К счастью, ни один из NASM/YASM / gas / clang не выполняет эту оптимизацию размера кода по умолчанию, если вы не укажете явно 32-битный размер адреса, вместо этого используйте 7-байтовый mov r32, r/m32
с режимом абсолютной адресации ModR/M+SIB+disp32 для mov eax, [abs symbol]
,
В 64-битном позиционно-зависимом коде абсолютная адресация - это дешевый способ использовать 1 дополнительный байт по сравнению с RIP-относительным. Но обратите внимание, что 32-битный абсолютный + немедленный требует 2 цикла для извлечения из кэша UOP, в отличие от RIP-относительного + imm8/16/32, который занимает только 1 цикл, даже если он все еще использует 2 записи для инструкции. (например, для mov
-магазин или cmp
). Так cmp [abs symbol], 123
медленнее извлекать из кэша UOP, чем cmp [rel symbol], 123
хотя оба принимают по 2 записи. Без немедленного, нет никаких дополнительных затрат для
Обратите внимание, что исполняемые файлы PIE допускают ASLR даже для исполняемого файла, и они используются по умолчанию во многих дистрибутивах Linux, поэтому, если вы можете сохранить код PIC без каких-либо недостатков, тогда это предпочтительнее.
Используйте префикс REX, когда он вам не нужен, например db 0x40
/ add eax, ecx
,
Обычно небезопасно добавлять префиксы, такие как rep, которые игнорируются текущими процессорами, потому что они могут означать что-то другое в будущих расширениях ISA.
Повторение того же префикса иногда возможно (но не с REX, хотя). Например, db 0x66, 0x66
/ add ax, bx
дает инструкции 3 префикса размера операнда, которые, я думаю, всегда строго эквивалентны одной копии префикса. До 3 префиксов является пределом для эффективного декодирования на некоторых процессорах. Но это работает, только если у вас есть префикс, который вы можете использовать в первую очередь; вы обычно не используете 16-битный размер операнда и, как правило, не хотите 32-битный размер адреса (хотя это безопасно для доступа к статическим данным в позиционно-зависимом коде).
ds
или же ss
префикс инструкции, обращающейся к памяти, запрещен и, вероятно, не вызывает замедления на каких-либо текущих процессорах. (@prl предложил это в комментариях).
На самом деле, микроархив Agner Fog использует ds
префикс на movq
[esi+ecx],mm0
в примере 7.1. Организация блоков IFETCH для настройки цикла для PII/PIII (без буфера цикла или кэша UOP), ускоряя его с 3 итераций за такт до 2.
Некоторые процессоры (например, AMD) декодируются медленно, когда инструкции имеют более 3 префиксов. На некоторых процессорах это включает обязательные префиксы в SSE2 и особенно инструкции SSSE3 / SSE4.1. В Сильвермонте даже побег байта 0F считается.
Инструкции AVX могут использовать 2 или 3-байтовый префикс VEX. Для некоторых инструкций требуется 3-байтовый префикс VEX (2-й источник - x/ymm8-15 или обязательные префиксы для SSSE3 или более поздней версии). Но инструкция, которая могла бы использовать 2-байтовый префикс, всегда может быть закодирована 3-байтовым VEX. NASM или ГАЗ {vex3} vxorps xmm0,xmm0
, Если AVX512 доступен, вы также можете использовать 4-байтовый EVEX.
Используйте 64-битный размер операнда для mov
например, даже если вам это не нужно mov rax, strict dword 1
принудительно применяет 7-байтовую кодировку extended-imm32 в NASM, которая обычно оптимизирует ее до 5-байтовой mov eax, 1
,
mov eax, 1 ; 5 bytes to encode (B8 imm32)
mov rax, strict dword 1 ; 7 bytes: REX mov r/m64, sign-extended-imm32.
mov rax, strict qword 1 ; 10 bytes to encode (REX B8 imm64). movabs mnemonic for AT&T.
Вы могли бы даже использовать mov reg, 0
вместо xor reg,reg
,
mov r64, imm64
эффективно помещается в кэш uop, когда константа действительно мала (подходит для 32-разрядного расширенного знака.) 1 запись в uop-кэше, а время загрузки = 1, то же, что и для mov r32, imm32
, Декодирование гигантской инструкции означает, что в 16-байтовом блоке декодирования, вероятно, нет места для 3 других инструкций, чтобы декодировать в том же цикле, если они не все 2-байтовые. Возможно, немного удлинить несколько других инструкций может быть лучше, чем иметь одну длинную инструкцию.
Расшифровка штрафов за дополнительные префиксы:
- P5: префиксы предотвращают сопряжение, за исключением адреса / размера операнда только в PMMX.
- Относительно PIII: всегда есть штраф, если инструкция имеет более одного префикса. Этот штраф обычно составляет один такт на дополнительный префикс. (Руководство по микроарху Агнера, конец раздела 6.3)
- Silvermont: это, вероятно, самое жесткое ограничение на то, какие префиксы вы можете использовать, если вам это нужно. Декодирование останавливается на более чем 3 префиксах, считая обязательные префиксы + 0F escape-байт. Инструкции SSSE3 и SSE4 уже имеют 3 префикса, поэтому даже REX замедляет их декодирование.
- немного AMD: возможно, ограничение с 3 префиксами, не включая escape-байты, и, возможно, не включая обязательные префиксы для инструкций SSE.
... TODO: закончите этот раздел. До этого обратитесь к руководству по микроарху Агнера Фога.
После ручного кодирования всегда разбирайте ваш двоичный файл, чтобы убедиться, что вы все правильно поняли. К сожалению, NASM и другие ассемблеры не имеют лучшей поддержки для выбора дешевого заполнения для области инструкций, чтобы достичь заданной границы выравнивания.
Синтаксис ассемблера
NASM имеет некоторый синтаксис переопределения кодировки: {vex3}
а также {evex}
префиксы, NOSPLIT
, а также strict byte / dword
и принудительное отображение disp8/disp32 в режимах адресации. Обратите внимание, что [rdi + byte 0]
не допускается, byte
Ключевое слово должно стоять первым. [byte rdi + 0]
разрешено, но я думаю, что это выглядит странно.
Листинг от nasm -l/dev/stdout -felf64 padding.asm
line addr machine-code bytes source line
num
4 00000000 0F57C0 xorps xmm0,xmm0 ; SSE1 *ps instructions are 1-byte shorter
5 00000003 660FEFC0 pxor xmm0,xmm0
6
7 00000007 C5F058DA vaddps xmm3, xmm1,xmm2
8 0000000B C4E17058DA {vex3} vaddps xmm3, xmm1,xmm2
9 00000010 62F1740858DA {evex} vaddps xmm3, xmm1,xmm2
10
11
12 00000016 FFC0 inc eax
13 00000018 83C001 add eax, 1
14 0000001B 4883C001 add rax, 1
15 0000001F 678D4001 lea eax, [eax+1] ; runs on fewer ports and doesn't set flags
16 00000023 67488D4001 lea rax, [eax+1] ; address-size and REX.W
17 00000028 0501000000 add eax, strict dword 1 ; using the EAX-only encoding with no ModR/M
18 0000002D 81C001000000 db 0x81, 0xC0, 1,0,0,0 ; add eax,0x1 using the ModR/M imm32 encoding
19 00000033 81C101000000 add ecx, strict dword 1 ; non-eax must use the ModR/M encoding
20 00000039 4881C101000000 add rcx, strict qword 1 ; YASM requires strict dword for the immediate, because it's still 32b
21 00000040 67488D8001000000 lea rax, [dword eax+1]
22
23
24 00000048 8B07 mov eax, [rdi]
25 0000004A 8B4700 mov eax, [byte 0 + rdi]
26 0000004D 3E8B4700 mov eax, [ds: byte 0 + rdi]
26 ****************** warning: ds segment base generated, but will be ignored in 64-bit mode
27 00000051 8B8700000000 mov eax, [dword 0 + rdi]
28 00000057 8B043D00000000 mov eax, [NOSPLIT dword 0 + rdi*1] ; 1c extra latency on SnB-family for non-simple addressing mode
GAS имеет псевдопрефиксы переопределения кодировки {vex3}
, {evex}
, {disp8}
, а также {disp32}
Они заменяют устаревшие .s
, .d8
а также .d32
суффиксы
У ГАЗА нет переопределения к немедленному размеру, только смещения.
GAS позволяет вам добавить явный ds
префикс, с ds mov src,dst
gcc -g -c padding.S && objdump -drwC padding.o -S
с ручным редактированием:
# no CPUs have separate ps vs. pd domains, so there's no penalty for mixing ps and pd loads/shuffles
0: 0f 28 07 movaps (%rdi),%xmm0
3: 66 0f 28 07 movapd (%rdi),%xmm0
7: 0f 58 c8 addps %xmm0,%xmm1 # not equivalent for SSE/AVX transitions, but sometimes safe to mix with AVX-128
a: c5 e8 58 d9 vaddps %xmm1,%xmm2, %xmm3 # default {vex2}
e: c4 e1 68 58 d9 {vex3} vaddps %xmm1,%xmm2, %xmm3
13: 62 f1 6c 08 58 d9 {evex} vaddps %xmm1,%xmm2, %xmm3
19: ff c0 inc %eax
1b: 83 c0 01 add $0x1,%eax
1e: 48 83 c0 01 add $0x1,%rax
22: 67 8d 40 01 lea 1(%eax), %eax # runs on fewer ports and doesn't set flags
26: 67 48 8d 40 01 lea 1(%eax), %rax # address-size and REX
# no equivalent for add eax, strict dword 1 # no-ModR/M
.byte 0x81, 0xC0; .long 1 # add eax,0x1 using the ModR/M imm32 encoding
2b: 81 c0 01 00 00 00 add $0x1,%eax # manually encoded
31: 81 c1 d2 04 00 00 add $0x4d2,%ecx # large immediate, can't get GAS to encode this way with $1 other than doing it manually
37: 67 8d 80 01 00 00 00 {disp32} lea 1(%eax), %eax
3e: 67 48 8d 80 01 00 00 00 {disp32} lea 1(%eax), %rax
mov 0(%rdi), %eax # the 0 optimizes away
46: 8b 07 mov (%rdi),%eax
{disp8} mov (%rdi), %eax # adds a disp8 even if you omit the 0
48: 8b 47 00 mov 0x0(%rdi),%eax
{disp8} ds mov (%rdi), %eax # with a DS prefix
4b: 3e 8b 47 00 mov %ds:0x0(%rdi),%eax
{disp32} mov (%rdi), %eax
4f: 8b 87 00 00 00 00 mov 0x0(%rdi),%eax
{disp32} mov 0(,%rdi,1), %eax # 1c extra latency on SnB-family for non-simple addressing mode
55: 8b 04 3d 00 00 00 00 mov 0x0(,%rdi,1),%eax
GAS строго менее мощен, чем NASM, для выражения кодировок, превышающих необходимые.
Давайте посмотрим на конкретный кусок кода:
cmp ebx,123456
mov al,0xFF
je .foo
Для этого кода ни одна из инструкций не может быть заменена чем-либо другим, поэтому единственными вариантами являются избыточные префиксы и NOP.
Однако что делать, если вы измените порядок команд?
Вы можете преобразовать код в это:
mov al,0xFF
cmp ebx,123456
je .foo
После повторного заказа инструкции; mov al,0xFF
можно заменить на or eax,0x000000FF
или же or ax,0x00FF
,
Для первого порядка команд существует только одна возможность, а для второго порядка команд есть 3 варианта; таким образом, есть всего 4 возможных перестановки на выбор без использования каких-либо избыточных префиксов или NOP.
Для каждой из этих 4 перестановок вы можете добавить варианты с разным количеством избыточных префиксов, а также одно- и многобайтовых NOP, чтобы завершить определенное выравнивание (я). Я слишком ленив, чтобы заняться математикой, поэтому давайте предположим, что, возможно, он расширится до 100 возможных перестановок.
Что делать, если вы дали каждой из этих 100 перестановок оценку (основываясь на таких вещах, как, сколько времени потребуется, чтобы выполнить, насколько хорошо она выравнивает инструкцию после этой части, если размер или скорость имеют значение, ...). Это может включать микроархитектурный таргетинг (например, возможно, для некоторых процессоров исходная перестановка нарушает микрооперацию и ухудшает код).
Вы можете сгенерировать все возможные перестановки и дать им оценку, а также выбрать перестановку с наилучшей оценкой. Обратите внимание, что это может быть не перестановка с лучшим выравниванием (если выравнивание менее важно, чем другие факторы, и только ухудшает производительность).
Конечно, вы можете разбить большие программы на множество небольших групп линейных команд, разделенных изменениями потока управления; а затем выполните этот "исчерпывающий поиск перестановки с лучшим счетом" для каждой небольшой группы линейных инструкций.
Проблема в том, что порядок команд и выбор команд взаимозависимы.
Для примера выше, вы не можете заменить mov al,0xFF
до тех пор, пока мы не переупорядочим инструкции; и легко найти случаи, когда вы не можете переупорядочить инструкции, пока вы не заменили (некоторые) инструкции. Это затрудняет тщательный поиск лучшего решения для любого определения "лучший", даже если вы заботитесь только о выравнивании и вообще не заботитесь о производительности.
Я могу думать о четырех способах от макушки головы:
Первое: используйте альтернативные кодировки для инструкций (Питер Кордес упомянул нечто подобное). Например, существует множество способов вызова операции ADD, и некоторые из них занимают больше байтов:
http://www.felixcloutier.com/x86/ADD.html
Обычно ассемблер пытается выбрать "лучшую" кодировку для ситуации, оптимизирует ли она скорость или длину, но вы всегда можете использовать другую и получить тот же результат.
Второе: используйте другие инструкции, которые означают одно и то же и имеют разную длину. Я уверен, что вы можете подумать о бесчисленных примерах, где вы могли бы вставить одну инструкцию в код, чтобы заменить существующую и получить те же результаты. Люди, которые вручную оптимизируют код, делают это постоянно:
shl 1
add eax, eax
mul 2
etc etc
Третье: используйте множество доступных NOP, чтобы выделить дополнительное пространство:
nop
and eax, eax
sub eax, 0
etc etc
В идеальном мире вам, вероятно, придется использовать все эти приемы, чтобы получить код, который будет иметь ту длину, которую вы хотите.
В-четвертых: измените свой алгоритм, чтобы получить больше опций, используя вышеуказанные методы.
И последнее замечание: очевидно, что ориентация на более современные процессоры даст вам лучшие результаты из-за количества и сложности инструкций. Доступ к инструкциям MMX, XMM, SSE, SSE2, с плавающей запятой и т. Д. Может облегчить вашу работу.
Зависит от характера кода.
Тяжелый код с плавающей точкой
AVX префикс
Для большей части инструкций SSE можно прибегнуть к более длинному префиксу AVX. Обратите внимание, что при переключении между SSE и AVX на процессорах Intel существует фиксированный штраф [1] [2]. Для этого требуется vzeroupper, который можно интерпретировать как еще один NOP для кода SSE или кода AVX, для которого не требуются старшие 128 бит.
SSE/AVX NOPS
Типичные NOP, о которых я могу думать:
- XORPS тот же регистр, используйте вариации SSE/AVX для целых чисел этих
- ANDPS тот же регистр, используйте вариации SSE/AVX для целых чисел этих