Подтвердить что ты не робот

Какие методы можно использовать для эффективного расширения длины инструкции на современных x86?

Представьте, что вы хотите выровнять серию инструкций по сборке x86 до определенных границ. Например, вы можете захотеть выровнять петли с 16 или 32-байтной границей или передать инструкции, чтобы они были эффективно размещены в кэше uop или что-то еще.

Самый простой способ добиться этого - однобайтовые инструкции NOP, за которыми следуют многобайтовые NOP. Хотя последний, как правило, более эффективен, ни один из методов не является бесплатным: NOP используют ресурсы для запуска переднего плана, а также учитывают ваш перекрестный предел ширины 1 4 на современном x86.

Другой вариант - как-то удлинить некоторые инструкции, чтобы получить нужное вам выравнивание. Если это делается без введения новых киосков, это кажется лучше, чем подход NOP. Как инструкции могут быть эффективно доработаны на последних процессорах x86?

В идеальном мире методы удлинения будут одновременно:

  • Применимо к большинству инструкций
  • Возможность удлинения команды переменной величиной
  • Не останавливать или замедлять декодеры.
  • Эффективно отображаться в кэше uop

Не исключено, что существует единственный метод, который одновременно удовлетворяет всем вышеперечисленным точкам, поэтому хорошие ответы, вероятно, будут касаться различных компромиссов.


1 Предел составляет 5 или 6 на AMD Ryzen.

4b9b3361

Ответ 1

Подумайте об умеренном коде -g, который позволит сжать код, а не расширять его, особенно перед циклом. например, xor eax,eax/cdq если вам нужны два обнуленных регистра, или mov eax, 1/lea ecx, [rax+1] чтобы установить регистры в 1 и 2 всего в 8 байтах вместо 10. См. Установка всех битов в CPU зарегистрируйтесь на 1, чтобы узнать больше об этом, и Советы по игре в гольф в машинном коде x86/x64 для более общих идей. Возможно, вы все еще хотите избежать ложных зависимостей.

Или заполните дополнительное пространство, создавая векторную константу на лету вместо загрузки ее из памяти. (Однако добавление большего давления в uop-кеш может быть хуже для более крупного цикла, содержащего ваш цикл setup + внутренний цикл. Но он позволяет избежать пропусков d-кеша для констант, поэтому у него есть потенциал, чтобы компенсировать выполнение большего числа мопов.)

Если вы еще не использовали их для загрузки "сжатых" констант, pmovsxbd, movddup или vpbroadcastd длиннее, чем movaps. Загрузка трансляций dword/qword бесплатна (нет ALU, просто загрузка).

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

Но в некоторых ситуациях вы, возможно, захотите оптимизировать пропускную способность декодирования/использование uop-кэша/общее количество uops для инструкций перед выравниванием блока, который вы хотите выровнять.


Инструкции по заполнению, как вопрос, заданный для:

У Агнера Фога есть целый раздел на эту тему: "10.6 Делать инструкции длиннее ради выравнивания" в его руководстве "Оптимизация подпрограмм на языке ассемблера". (Идеи lea, push r/m64 и SIB взяты оттуда, и я скопировал предложение/фразу или две, иначе этот ответ - моя собственная работа, либо другие идеи, либо написанные до проверки руководства Агнера.)

Он не был обновлен для текущих процессоров, однако: lea eax, [rbx + dword 0] имеет больше недостатков, чем раньше, чем против mov eax, ebx, потому что вы пропускаете нулевую -l интенсивность/отсутствие исполнительного модуля 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, такие как процесс отмены -l и не использование порта 7 для хранилищ.

Поэтому лучше всего кодировать base=rbx + disp0/8/32=0 используя ModR/M + SIB без индекса reg. (Кодировка SIB для "без индекса" - это кодировка, которая в противном случае означала бы idx = RSP). Режимы адресации [rsp + x] требуют SIB (base = RSP - это управляющий код, который означает, что есть SIB), и это все время появляется в сгенерированном коде компилятора -g. Таким образом, есть очень веская причина ожидать, что это будет полностью эффективно для декодирования и выполнения (даже для базовых регистров, отличных от RSP) сейчас и в будущем. Синтаксис NASM не может выразить это, поэтому вам придется кодировать вручную. GNU gas Синтаксис Intel от objdump -d говорит 8b 04 23 mov eax,DWORD PTR [rbx+riz*1] для примера Agner Fog 10.20. (riz - вымышленная нотация индекса с нулем, которая означает, что есть SIB без индекса). Я не проверял, принимает ли ГАЗ это как ввод.

Используйте форму imm32 и/или disp32 для которой требуется только imm8 или disp0/disp32. Проверка Agner Fog кэша uy Sandybridge (таблица 9.1 руководства по микроархитектору) показывает, что значение имеет фактическое значение немедленного/смещения, а не количество байтов, используемых в кодировке команд. У меня нет никакой информации о тайнике Райзена.

Таким образом, NASM imul eax, [dword 4 + rdi], strict dword 13 (10 байт: код операции + modrm + disp32 + imm32) будет использовать категорию 32small, 32small и принимать 1 запись в кэше uop, в отличие от непосредственного или 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.


Возможно, вы a32 mov eax, [abs symbol] закодировать mov eax, [symbol] как 6-байтовый a32 mov eax, [abs symbol] в 64-битном коде, используя префикс ize адреса -S, чтобы использовать 32-битный абсолютный адрес. Но это приводит к задержке префикса изменения длины при декодировании на процессорах Intel. К счастью, ни один из NASM/YASM/gas/clang не делает этот код -S по умолчанию, если вы не указываете 32-битный адрес -S, вместо этого используйте 7-байтовый mov r32, r/m32 с режим абсолютной адресации ModR/M + SIB + disp32 для mov eax, [abs symbol].

В 64-битном позиционном -d зависимом коде абсолютная адресация является дешевым способом использования 1 дополнительного байта по сравнению с RIP-относительным. Но обратите внимание, что 32-битный абсолютный + немедленный требует 2 цикла для извлечения из кэша UOP, в отличие от RIP-относительного + imm8/16/32, который занимает всего 1 цикл, даже если он все еще использует 2 записи для инструкции. (например, для mov -store или 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 -S, который, я думаю, всегда строго эквивалентен одной копии префикса. До 3 префиксов является пределом для эффективного декодирования на некоторых процессорах. Но это работает, только если у вас есть префикс, который вы можете использовать в первую очередь; Вы обычно не используете 16-битный операнд -S ize и, как правило, не хотите, чтобы 32-битный адрес -S ize (хотя это безопасно для доступа к статическим данным в зависимом от позиции -d коде).

Префикс 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 или GAS {vex3} vxorps xmm0,xmm0. Если AVX512 доступен, вы также можете использовать 4-байтовый EVEX.


Используйте 64-битный операнд -S ize для mov даже когда он вам не нужен, например mov rax, strict dword 1 7-байтовую кодировку extended-imm32 в NASM, которая обычно оптимизирует его до 5- byte 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 эффективно помещается в кэш mov r64, imm64 когда константа действительно мала (подходит для 32-разрядного расширенного знака.) 1 запись в uop-cache и load-time = 1, так же, как для mov r32, imm32. Декодирование гигантской инструкции означает, что в 16-байтовом блоке декодирования, вероятно, нет места для 3 других инструкций, которые нужно декодировать в том же цикле, если только они не все 2-байтовые. Возможно, немного удлинить несколько других инструкций лучше, чем иметь одну длинную инструкцию.


Расшифровка штрафов за дополнительные префиксы:

  • P5: префиксы препятствуют сопряжению, за исключением адреса/операнда -S, только на PMMX.
  • Относительно PIII: всегда есть штраф, если инструкция имеет более одного префикса. Этот штраф обычно составляет один такт на дополнительный префикс. (Руководство по микроарху Agner, конец раздела 6.3)
  • Silvermont: это, пожалуй, самое жесткое ограничение на то, какие префиксы вы можете использовать, если вы заботитесь об этом. Декодирование останавливается на более чем 3 префиксах, считая обязательные префиксы + 0F escape-байт. Инструкции SSSE3 и SSE4 уже имеют 3 префикса, поэтому даже REX замедляет их декодирование.
  • некоторые AMD: возможно, ограничение с 3 префиксами, не включая escape-байты, и, возможно, не включая обязательные префиксы для инструкций SSE.

... TODO: закончите этот раздел. До этого обратитесь к руководству по микроарху Agner Fog.


После ручного кодирования всегда разбирайте ваш двоичный файл, чтобы убедиться, что вы все правильно поняли. К сожалению, NASM и другие ассемблеры не имеют лучшей поддержки для выбора дешевого заполнения в области команд для достижения заданной границы выравнивания.


Синтаксис ассемблера

NASM имеет некоторый синтаксис переопределения кодировки: {vex3} и {evex}, NOSPLIT и strict byte/dword, а также принудительное использование disp8/disp32 в режимах адресации. Обратите внимание, что [rdi + byte 0] не допускается, ключевое слово byte должно стоять на первом месте. [byte rdi + 0] разрешено, но я думаю, что это выглядит странно.

nasm -l/dev/stdout -felf64 padding.asm из 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 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} Они заменяют уже существующие -d суффиксы .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 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, для выражения кодировок, превышающих необходимые.

Ответ 2

Я могу думать о четырех способах с головы:

Сначала: Используйте альтернативные кодировки для инструкций (Питер Кордес упомянул что-то подобное). Существует много способов вызвать операцию 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, с плавающей запятой и т.д. Может облегчить вашу работу.

Ответ 3

Давайте посмотрим на конкретный кусок кода:

    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 пока мы не переупорядочим инструкции; и легко найти случаи, когда вы не можете переупорядочить инструкции, пока вы не заменили (некоторые) инструкции. Это затрудняет тщательный поиск лучшего решения для любого определения "лучший", даже если вы заботитесь только о выравнивании и вообще не заботитесь о производительности.

Ответ 4

Зависит от характера кода.

Тяжелый код с плавающей точкой

AVX префикс

Можно использовать более длинный префикс AVX для большинства инструкций SSE. Обратите внимание, что при переключении между SSE и AVX на процессорах Intel существует фиксированный штраф [1] [2]. Для этого требуется vzeroupper, который можно интерпретировать как еще один NOP для кода SSE или кода AVX, для которого не требуются старшие 128 бит.

SSE/AVX NOPS

Типичные НОПы, о которых я могу думать:

  • XORPS тот же регистр, используйте вариации SSE/AVX для целых чисел этих
  • ANDPS тот же регистр, используйте вариации SSE/AVX для целых чисел этих