Советы по игре в гольф в машинном коде x86 / x64


27

Я заметил, что такого вопроса нет, поэтому вот он:

У вас есть общие советы по игре в гольф в машинном коде? Если совет относится только к определенной среде или соглашению о вызовах, укажите это в своем ответе.

Пожалуйста, только один совет за ответ (см. Здесь ).

Ответы:


11

mov-среднее дорого для констант

Это может быть очевидным, но я все равно оставлю это здесь. В общем случае стоит задуматься о представлении числа на битовом уровне, когда вам нужно инициализировать значение.

Инициализация eaxс 0:

b8 00 00 00 00          mov    $0x0,%eax

следует сократить (как для производительности, так и для размера кода ) до

31 c0                   xor    %eax,%eax

Инициализация eaxс -1:

b8 ff ff ff ff          mov    $-1,%eax

можно сократить до

31 c0                   xor    %eax,%eax
48                      dec    %eax

или

83 c8 ff                or     $-1,%eax

Или, в более общем случае, любое 8-битное значение с расширенным знаком может быть создано в 3 байта с push -12(2 байта) / pop %eax(1 байт). Это даже работает для 64-битных регистров без дополнительного префикса REX; push/ popразмер операнда по умолчанию = 64.

6a f3                   pushq  $0xfffffffffffffff3
5d                      pop    %rbp

Или, учитывая известную константу в регистре, вы можете создать другую соседнюю константу, используя lea 123(%eax), %ecx(3 байта). Это удобно, если вам нужен нулевой регистр и константа; xor-ноль (2 байта) + lea-disp8(3 байта).

31 c0                   xor    %eax,%eax
8d 48 0c                lea    0xc(%eax),%ecx

См. Также Установите все биты в регистре процессора на 1 эффективно


Кроме того, чтобы инициализировать регистр небольшим (8-битным) значением, отличным от 0: используйте, например, push 200; pop edx- 3 байта для инициализации.
Анатолий

2
Кстати, чтобы инициализировать регистр -1, используйте dec, например,xor eax, eax; dec eax
anatolyg

@anatolyg: 200 - плохой пример, он не подходит для sign-extended-imm8. Но да, push imm8/pop reg составляет 3 байта и отлично подходит для 64-битных констант на x86-64, где dec/ incсоставляет 2 байта. И push r64/ pop 64(2 байта) может даже заменить 3 байта mov r64, r64(3 байта на REX). См. Также Установка всех битов в регистре ЦП на 1 для таких вещей, как lea eax, [rcx-1]заданное известное значение в eax(например, если нужен нулевой регистр и другая константа, просто используйте LEA вместо push / pop
Peter Cordes

10

Во многих случаях инструкции на основе аккумулятора (то есть те, которые принимают (R|E)AXв качестве операнда назначения) на 1 байт короче, чем инструкции общего случая; увидеть этот вопрос на StackOverflow.


Обычно наиболее полезными являются al, imm8особые случаи, такие как or al, 0x20/ sub al, 'a'/ cmp al, 'z'-'a'/ по ja .non_alphabetic2 байта каждый вместо 3. Использование alдля символьных данных также позволяет lodsbи / или stosb. Или используйте alдля проверки чего-либо о младшем байте EAX, например, lodsd/ test al, 1/ setnz clделает cl = 1 или 0 для нечетного / четного. Но в редком случае, когда вам нужен 32-битный немедленный, тогда, конечно op eax, imm32, как в моем ответе хроматический ключ
Питер Кордес

8

Выберите соглашение о вызовах, чтобы поставить аргументы там, где вы хотите.

Язык вашего ответа - asm (на самом деле машинный код), поэтому рассматривайте его как часть программы, написанной на asm, а не на C-compiled-for-x86. Ваша функция не должна легко вызываться из C с любым стандартным соглашением о вызовах. Это хороший бонус, если он не будет стоить вам лишних байтов.

В чистой программе asm некоторые вспомогательные функции обычно используют соглашение о вызовах, которое удобно для них и для их вызывающей стороны. Такие функции документируют свое соглашение о вызовах (входы / выходы / сгустки) с комментариями

В реальной жизни даже программы asm (я думаю), как правило, используют согласованные соглашения о вызовах для большинства функций (особенно для разных исходных файлов), но любая важная функция может делать что-то особенное. В Code-Golf вы оптимизируете дерьмо из одной функции, так что, очевидно, это важно / особенное.


Чтобы протестировать вашу функцию из C-программы, можете написать оболочку, которая помещает аргументы в нужных местах, сохраняет / восстанавливает любые дополнительные регистры, которые вы закрываете, и помещает возвращаемое значение, e/raxесли его там еще не было.


Пределы того, что разумно: все, что не налагает чрезмерное бремя на звонящего:

  • ESP / RSP должен быть сохранен при вызове; другие целочисленные регистры являются честной игрой. (RBP и RBX обычно сохраняются при обычном соглашении, но вы можете использовать оба этих метода).
  • Любой аргумент в любом регистре (кроме RSP) является разумным, но запрос вызывающего абонента скопировать один и тот же аргумент в несколько регистров - нет.
  • Требующий DF (флаг направления строки для lods / stos/ и т. Д.) Был очищен (вверх) при вызове / повторении, это нормально. Позволить ему быть неопределенным на call / ret было бы хорошо. Требование очистки или установки при входе, но затем изменение его при возвращении было бы странным.

  • Возвращать значения FP в x87 st0разумно, но возвращать вst3 с мусором в другом регистре x87 - нет. Звонящий должен будет очистить стек x87. Даже возвращение st0с непустыми регистрами старшего стека также будет сомнительным (если только вы не возвращаете несколько значений).

  • Ваша функция будет вызываться с помощью call, так же [rsp]как и ваш обратный адрес. Вы можете избежать call/ retна x86, используя регистрацию ссылок вроде lea rbx, [ret_addr]/ jmp functionи вернуться с помощью jmp rbx, но это не «разумно». Это не так эффективно, как call / ret, так что это не то, что вы правдоподобно найдете в реальном коде.
  • Засорение неограниченной памяти над RSP нецелесообразно, но в обычных соглашениях о вызовах допускается забивание аргументов вашей функции в стеке. В x64 Windows требуется 32 байта теневого пространства над адресом возврата, в то время как x86-64 System V предоставляет красную зону на 128 байт ниже RSP, поэтому любой из них является разумным. (Или даже гораздо большая красная зона, особенно в автономной программе, а не в функции.)

Пограничные случаи: напишите функцию, которая создает последовательность в массиве, учитывая первые 2 элемента как аргументы функции . Я выбрал, чтобы вызывающая сторона сохраняла начало последовательности в массиве и просто передавала указатель на массив. Это определенно изгибает требования вопроса. Я подумал о том, чтобы взять упакованные аргументы xmm0для for movlps [rdi], xmm0, что также было бы странным соглашением о вызовах.


Вернуть логическое значение во FLAGS (коды условий)

Системные вызовы OS X делают это ( CF=0означает отсутствие ошибок): считается ли плохой практикой использование регистра флагов в качестве логического возвращаемого значения? ,

Любое условие, которое можно проверить с помощью одного JCC, вполне разумно, особенно если вы можете выбрать условие, имеющее семантическое отношение к проблеме. (например, функция сравнения может установить флаги такjne будут приняты, если они не были равны).


Требуются узкие арги (вроде char ) были знаком или нулем, расширенным до 32 или 64 бит.

Это не лишено смысла; использование movzxили movsx избежание частичного замедления регистрации является нормальным явлением в современной архитектуре x86. Фактически, clang / LLVM уже создает код, который зависит от недокументированного расширения соглашения о вызовах System V в x86-64: аргументы, которые меньше 32 бит, являются знаком или нулем, расширяемым вызывающей стороной до 32 бит .

Вы можете задокументировать / описать расширение до 64 бит, написав uint64_tили int64_tв своем прототипе, если хотите. Например, вы можете использовать loopинструкцию, которая использует все 64 бита RCX, если только вы не используете префикс размера адреса, чтобы переопределить размер до 32-битного ECX (да, действительно, размер адреса не размер операнда).

Обратите внимание, что longэто только 32-битный тип в 64-битном ABI Windows и Linux x32 ABI ; uint64_tявляется однозначным и короче, чем тип unsigned long long.


Существующие соглашения о вызовах:

  • Windows 32-битная __fastcall, уже предложенная другим ответом : целочисленные аргументы в ecxи edx.

  • x86-64 System V : передает много аргументов в регистрах и имеет много регистров с закрытыми вызовами, которые вы можете использовать без префиксов REX. Что еще более важно, это было фактически выбрано, чтобы позволить компиляторам встроить memcpyили memset так же rep movsbлегко: первые 6 аргументов целого числа / указателя передаются в RDI, RSI, RDX, RCX, R8, R9.

    Если ваша функция использует lodsd/ stosdвнутри цикла, который выполняется rcxраз (с loopинструкцией), вы можете сказать «вызывается из C, как int foo(int *rdi, const int *rsi, int dummy, uint64_t len)в соглашении о вызовах System V в x86-64». Пример: рирпроекции .

  • 32-битный GCC regparm: целочисленные аргументы в EAX , ECX, EDX, возврат в EAX (или EDX: EAX). Наличие первого аргумента в том же регистре, что и возвращаемое значение, позволяет провести некоторые оптимизации, как в этом случае с вызывающим примером и прототипом с атрибутом функции . И, конечно, AL / EAX специально для некоторых инструкций.

  • Linux x32 ABI использует 32-разрядные указатели в длинном режиме, так что вы можете сохранить префикс REX при изменении указателя ( пример использования ). Вы по-прежнему можете использовать 64-битный размер адреса, если только у вас нет 32-битного отрицательного целого, расширенного нулями в регистре (так что это будет большое значение без знака, если вы[rdi + rdx] ).

    Обратите внимание, что push rsp/ pop raxсоставляет 2 байта и эквивалентно mov rax,rsp, так что вы все равно можете копировать полные 64-битные регистры в 2 байта.


Когда вызовы требуют вернуть массив, считаете ли вы разумным возвращение в стек? Я думаю, это то, что будут делать компиляторы при возврате структуры по значению.
QWR

@qwr: нет, обычные соглашения о вызовах передают скрытый указатель на возвращаемое значение. (Некоторые соглашения пропускают / возвращают небольшие структуры в регистрах). C / C ++ возвращает структуру по значению изнутри , и смотрите конец Как объекты работают в x86 на уровне сборки? , Обратите внимание, что при передаче массивов (внутри структур) они копируются в стек для x86-64 SysV: какой тип данных C11 является массивом в соответствии с AMD64 ABI , но Windows x64 передает неконстантный указатель.
Питер Кордес

так что вы думаете о разумном или нет? Считаете ли вы x86 по этому правилу codegolf.meta.stackexchange.com/a/8507/17360
qwr

1
@qwr: x86 не является языком, основанным на стеке. x86 - это машина регистрации с ОЗУ , а не машина стека . Машина стека похожа на запись с обратной полировкой, как регистры x87. Fld / Fld / Faddp. стек вызовов x86 не подходит для этой модели: все обычные соглашения о вызовах оставляют RSP неизменным или выдвигают аргументы ret 16; они не выталкивают адрес возврата, выдвигают массив, затем push rcx/ ret. Вызывающая сторона должна знать размер массива или сохранить RSP где-нибудь за пределами стека, чтобы найти себя.
Питер Кордес

Вызов push push адрес инструкции после вызова в стеке jmp для вызова функции; ret выскакивают адрес из стека и jmp на этот адрес
RosLuP

7

Используйте краткие формы для специальных случаев для AL / AX / EAX, а также другие короткие формы и однобайтовые инструкции

Примеры предполагают 32/64-битный режим, где размер операнда по умолчанию составляет 32 бита. Префикс размера операнда меняет инструкцию на AX вместо EAX (или наоборот в 16-битном режиме).

  • inc/decрегистр (кроме 8-битного): inc eax/ dec ebp. (Не x86-64: 0x4xбайты кода операции были переназначены как префиксы REX, поэтому inc r/m32это единственная кодировка.)

    8-разрядный inc bl2 байта, используя inc r/m8опкод + ModR / M операнд , кодирующий . Так что используйте inc ebxдля увеличения bl, если это безопасно. (например, если вам не нужен результат ZF в случаях, когда старшие байты могут быть ненулевыми).

  • scasd: e/rdi+=4, требует, чтобы регистр указывал на читаемую память. Иногда полезно, даже если вас не волнует результат FLAGS (например, cmp eax,[rdi]/ rdi+=4). А в 64-битном режиме scasbможет работать как 1 байтinc rdi , если lodsb или stosb бесполезны.

  • xchg eax, r32: Это где 0x90 NOP пришли: xchg eax,eax. Пример: переупорядочить 3 регистра с двумя xchgинструкциями в цикле cdq/ для GCD в 8 байтов, где большинство инструкций являются однобайтовыми, включая злоупотребление / вместо /idivinc ecxlooptest ecx,ecxjnz

  • cdq: расширение знака EAX в EDX: EAX, то есть копирование старшего бита EAX во все биты EDX. Чтобы создать ноль с известным неотрицательным, или получить 0 / -1 для добавления / sub или маски с. Урок истории x86: cltqпротивmovslq , а также AT & T против мнемоники Intel для этого и связанных с ним cdqe.

  • lodsb / d : как mov eax, [rsi]/ rsi += 4без заглушающих флагов. (Предполагая, что DF ясен, какие стандартные соглашения о вызовах требуются при входе в функцию.) Также stosb / d, иногда scas и реже movs / cmps.

  • push/ pop reg. например, в 64-битном режиме push rsp/ pop rdiсоставляет 2 байта, но mov rdi, rspтребует префикса REX и составляет 3 байта.

xlatbсуществует, но редко бывает полезным. Большой справочной таблицы - это то, чего следует избегать. Я также никогда не находил применения для AAA / DAA или других инструкций, упакованных BCD или 2-ASCII-цифрами.

1 байт lahf/ sahfредко используются. Вы могли бы lahf / and ah, 1в качестве альтернативыsetc ah , но это, как правило, бесполезно.

А для CF, в частности, sbb eax,eaxнужно получить 0 / -1 или даже недокументированный, но универсально поддерживаемый 1-байт salc(установите AL из Carry), что эффективно не sbb al,alвлияет на флаги. (Удалено в x86-64). Я использовал SALC в конкурсе « Оценка пользователей № 1: Деннис» .

1-байт cmc/ clc/ stc(flip («дополнение»), очистить или установить CF) редко используются, хотя я нашел применение дляcmc сложения с расширенной точностью с базовыми 10 ^ 9 кусками. Чтобы безоговорочно установить / очистить CF, обычно организуйте, чтобы это происходило как часть другой инструкции, например, xor eax,eaxочищает CF, а также EAX. Не существует эквивалентных инструкций для других флагов условий, только DF (направление строки) и IF (прерывания). Флаг переноса специально для множества инструкций; сдвиги устанавливают его, adc al, 0могут добавить его в AL в 2 байта, и я упоминал ранее недокументированный SALC.

std/ cldредко, кажется, стоит . Особенно в 32-битном коде лучше просто использовать decуказатель и movоперанд или источник памяти для инструкции ALU вместо установки DF so lodsb/ stosbgo вниз, а не вверх. Обычно, если вам нужен нисходящий поток, у вас все еще есть еще один указатель, поэтому вам нужно больше, чем один stdи cldво всей функции, чтобы использовать lods/ stosдля обоих. Вместо этого просто используйте строковые инструкции для направления вверх. (Стандартные соглашения о вызовах гарантируют DF = 0 при входе в функцию, поэтому вы можете предположить, что это бесплатно без использования cld.)


История 8086 года: почему существуют эти кодировки

В оригинальных 8086, AX было очень особенным: инструкции нравятся lodsb/ stosb, cbw, mul/ divи другие используют его неявно. Это все еще так, конечно; В текущем x86 не пропал ни один из 8080-х операционных кодов (по крайней мере, ни один из официально документированных). Но позже процессоры добавили новые инструкции, которые давали лучшие / более эффективные способы выполнения действий без предварительного копирования или замены их в AX. (Или в EAX в 32-битном режиме.)

например, в 8086 отсутствовали более поздние дополнения, такие как movsx/ movzxдля загрузки или перемещения + знак-удлинение, или 2-х и 3-х операнды, imul cx, bx, 1234которые не дают результата с половиной и не имеют никаких неявных операндов.

Кроме того, основным узким местом 8086 была выборка инструкций, поэтому оптимизация под размер кода была важна для производительности в то время . Дизайнер ISA 8086 (Стивен Морс) потратил много места для кодирования кода операции в особых случаях для AX / AL, включая специальные (E) коды операции AX / AL-destination для всех основных инструкций ALU- непосредственного кода, просто код операции + немедленный без байта ModR / M. 2-байтовый add/sub/and/or/xor/cmp/test/... AL,imm8или AX,imm16или (в 32-битном режиме)EAX,imm32 .

Но для этого нет особого случая EAX,imm8, поэтому обычное кодирование ModR / M add eax,4короче.

Предполагается, что если вы собираетесь работать с некоторыми данными, вы захотите использовать их в AX / AL, поэтому вам, возможно, захочется заменить регистр на AX , возможно, даже чаще, чем копировать регистр в AX с помощью mov,

Все, что касается кодирования инструкций 8086, поддерживает эту парадигму: от инструкций, подобных lodsb/wвсем кодировкам для особых случаев, для немедленных с EAX до неявного использования даже для умножения / деления.


Не увлекайся; обменять все на EAX не всегда автоматически, особенно если вам нужно использовать немедленные операции с 32-разрядными регистрами вместо 8-разрядных. Или если вам нужно чередовать операции с несколькими переменными в регистрах одновременно. Или, если вы используете инструкции с 2 регистрами, не сразу.

Но всегда имейте в виду: я делаю что-нибудь, что было бы короче в EAX / AL? Могу ли я переставить так, чтобы у меня было это в AL, или я в настоящее время пользуюсь преимуществом AL с тем, для чего я уже его использую.

Свободно смешивайте 8-битные и 32-битные операции, чтобы воспользоваться преимуществами, когда это безопасно (вам не нужно выносить данные в полный регистр или что-то в этом роде).


cdqэто полезно для divчего нулю edxво многих случаях.
сентября

1
@qwr: верно, вы можете злоупотреблять cdqперед беззнаковыми, divесли знаете, что ваш дивиденд ниже 2 ^ 31 (то есть неотрицательный, когда рассматривается как подписанный), или если вы используете его перед установкой eaxпотенциально большого значения. Обычно (вне code-golf) вы бы использовали его cdqкак настройку idiv, так и xor edx,edxраньшеdiv
Peter Cordes

5

Используйте fastcallсоглашения

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

При использовании fastcallна 32-битных платформах 2 первых параметра обычно передаются в ecxи edx. Если ваша функция имеет 3 параметра, вы можете рассмотреть возможность ее реализации на 64-битной платформе.

Прототипы функций C для fastcallсоглашения (взяты из этого примера ответа ):

extern int __fastcall SwapParity(int value);                 // MSVC
extern int __attribute__((fastcall)) SwapParity(int value);  // GNU   

Или используйте полностью настраиваемое соглашение о вызовах , потому что вы пишете в чистом asm, а не обязательно пишете код, вызываемый из C. Возвращение логических значений во FLAGS часто удобно.
Питер Кордес

5

Вычтите -128 вместо добавления 128

0100 81C38000      ADD     BX,0080
0104 83EB80        SUB     BX,-80

Точно так же, добавить -128 вместо вычитать 128


1
Это также работает в другом направлении, конечно: добавить -128 вместо суб 128. Интересный факт: составители знают эту оптимизацию, а также сделать соответствующую оптимизацию превращения < 128в <= 127уменьшить величину немедленного операнда cmp, или GCC всегда предпочитает переставляя сравнивает, чтобы уменьшить величину, даже если это не -129 против -128.
Питер Кордес

4

Создайте 3 нуля с помощью mul(затем inc/, decчтобы получить +1 / -1, а также ноль)

Вы можете обнулить eax и edx, умножив на ноль в третьем регистре.

xor   ebx, ebx      ; 2B  ebx = 0
mul   ebx           ; 2B  eax=edx = 0

inc   ebx           ; 1B  ebx=1

в результате EAX, EDX и EBX будут равны нулю всего за четыре байта. Вы можете обнулить EAX и EDX в трех байтах:

xor eax, eax
cdq

Но с этой начальной точки вы не можете получить 3-й регистр с нулем в еще одном байте или регистр +1 или -1 в еще 2 байта. Вместо этого используйте технику мул.

Пример использования: объединение чисел Фибоначчи в двоичном виде .

Обратите внимание, что после LOOPзавершения цикла ECX будет нулевым и может использоваться для обнуления EDX и EAX; Вы не всегда должны создавать первый ноль с xor.


1
Это немного сбивает с толку. Не могли бы вы расширить?
NoOneIsHere

@NoOneIsHere Я полагаю, что он хочет установить три регистра в 0, включая EAX и EDX.
NieDzejkob

4

Регистры и флаги процессора находятся в известных состояниях запуска

Можно предположить, что процессор находится в известном и задокументированном состоянии по умолчанию в зависимости от платформы и ОС.

Например:

DOS http://www.fysnet.net/yourhelp.htm

Linux x86 ELF http://asm.sourceforge.net/articles/startup.html


1
Правила Code Golf гласят, что ваш код должен работать хотя бы в одной реализации. Linux выбирает для обнуления все регистры (кроме RSP) и стека перед входом в новый процесс пользовательского пространства, хотя в документах ABI i386 и x86-64 System V говорится, что они «не определены» при входе в _start. Так что да, это справедливо, если вы пишете программу вместо функции. Я сделал это в Экстрим Фибоначчи . (В динамически выполняемом файле, ld.so бежит перед прыжком к вашему _start, и делает отпуск мусор в регистрах, а статический только ваш код.)
Питер Кордес

3

Чтобы сложить или вычесть 1, используйте один байт incили decинструкции, которые меньше, чем многобайтовые инструкции сложения и подчинения.


Обратите внимание, что 32-битный режим имеет 1 байт inc/dec r32с номером регистра, закодированным в коде операции. Таким образом, inc ebxэто 1 байт, но inc blравен 2. Еще меньше, чем, add bl, 1конечно, для регистров, кроме al. Также обратите внимание, что inc/ decоставьте CF без изменений, но обновите другие флаги.
Питер Кордес,

1
2 для +2 и -2 в x86
l4m2

3

lea для математики

Это, наверное, одна из первых вещей, которые мы узнаем о x86, но я оставляю это здесь как напоминание. leaможет использоваться для умножения на 2, 3, 4, 5, 8 или 9 и добавления смещения.

Например, для вычисления ebx = 9*eax + 3в одной инструкции (в 32-битном режиме):

8d 5c c0 03             lea    0x3(%eax,%eax,8),%ebx

Вот это без смещения:

8d 1c c0                lea    (%eax,%eax,8),%ebx

Вот Это Да! Конечно, leaможно использовать и математику какebx = edx + 8*eax + 3 для расчета индексации массива.


1
Возможно, стоит упомянуть, что lea eax, [rcx + 13]это версия без дополнительных префиксов для 64-битного режима. 32-битный размер операнда (для результата) и 64-битный размер адреса (для входов).
Питер Кордес

3

Инструкции цикла и строки меньше, чем альтернативные последовательности команд. Наиболее полезным является то, loop <label>что меньше, чем две последовательности команд dec ECXи jnz <label>, и lodsbменьше, чем mov al,[esi]и inc si.


2

mov маленький сразу в нижние регистры, когда это применимо

Если вы уже знаете, что верхние биты регистра равны 0, вы можете использовать более короткую инструкцию для немедленного перемещения в нижние регистры.

b8 0a 00 00 00          mov    $0xa,%eax

против

b0 0a                   mov    $0xa,%al

Используйте push/ popдля imm8, чтобы обнулить старшие биты

Благодарю Питера Кордеса. xor/ mov4 байта, но push/ popтолько 3!

6a 0a                   push   $0xa
58                      pop    %eax

mov al, 0xaхорошо, если вам не нужно, чтобы он был расширен до нуля. Но если вы это сделаете, xor / mov будет 4 байта против 3 для push imm8 / pop или leaдругой известной константы. Это может быть полезно в сочетании с mulобнулением 3 регистров в 4 байта или cdq, если вам нужно много констант.
Питер Кордес,

Другой вариант использования был бы для констант из [0x80..0xFF], которые не могут быть представлены как расширенный знак imm8. Или, если вы уже знаете старшие байты, например, mov cl, 0x10после loopинструкции, потому что единственный способ loopне перейти - это когда она выполнена rcx=0. (Я думаю, вы сказали это, но ваш пример использует xor). Вы даже можете использовать младший байт регистра для чего-то другого, пока что-то еще возвращает его к нулю (или как угодно), когда вы закончите. Например, моя программа Фибоначчи хранится -1024в ebx и использует bl.
Питер Кордес

@PeterCordes Я добавил вашу технику push / pop
qwr

Вероятно, стоит перейти к существующему ответу о константах, где Анатолий уже предложил это в комментарии . Я отредактирую этот ответ. IMO, вы должны переработать этот, чтобы предложить использовать 8-битный размер операнда для большего количества вещей (кроме xchg eax, r32), например, mov bl, 10/ dec bl/ jnzчтобы ваш код не заботился о старших байтах RBX.
Питер Кордес

@PeterCordes хм. Я до сих пор не знаю, когда использовать 8-битные операнды, поэтому я не уверен, что вставить в этот ответ.
Qwr

2

В ФЛАГИ устанавливаются после многих инструкций

После многих арифметических инструкций флаг переноса (без знака) и флаг переполнения (со знаком) устанавливаются автоматически ( дополнительная информация ). Флаг знака и флаг нуля устанавливаются после многих арифметических и логических операций. Это можно использовать для условного ветвления.

Пример:

d1 f8                   sar    %eax

ZF устанавливается этой инструкцией, поэтому мы можем использовать ее для условного ветвления.


Когда вы когда-нибудь использовали флаг паритета? Вы знаете, что это горизонтальная xor младших 8 битов результата, верно? (Независимо от размера операнда PF устанавливается только из младших 8 битов ; см. Также ). Не четное число / нечетное число; для этой проверки ZF после test al,1; Вы обычно не получаете это бесплатно. (Или and al,1создать целое число 0/1 в зависимости от нечетного / четного.)
Питер Кордес

В любом случае, если в этом ответе сказано «используйте флаги, уже установленные другими инструкциями, чтобы избежать test/ cmp», то это будет довольно простой для новичка x86, но все же стоит воздержаться.
Питер Кордес

@PeterCordes Да, я, кажется, неправильно понял флаг паритета. Я все еще работаю над другим ответом. Я отредактирую ответ. И, как вы, вероятно, можете сказать, я новичок, поэтому основные советы помогут.
18:00

2

Используйте циклы do-while вместо циклов while

Это не специфично для x86, но широко применимо для начинающих. Если вы знаете, что цикл while будет запускаться хотя бы один раз, переписывание цикла как цикла do-while с проверкой состояния цикла в конце часто сохраняет 2-байтовую инструкцию перехода. В особом случае вы можете даже использовать loop.


2
Связанный: почему циклы всегда компилируются так? объясняет, почему do{}while()при сборке используется естественная цикличность (особенно для эффективности). Также обратите внимание, что 2-байтовый jecxz/ jrcxzперед циклом работает очень хорошо, loopчтобы обрабатывать регистр «необходимо запустить нулевое время» «эффективно» (на редких процессорах, где loopне медленно). jecxzтакже можно использовать внутри цикла для реализацииwhile(ecx){} , с jmpнижней.
Питер Кордес

@PeterCordes, это очень хорошо написанный ответ. Я хотел бы найти применение для прыжка в середину цикла в программе для игры в код.
августа

Используйте goto jmp и отступ ... Loop follow
RosLuP

2

Используйте любые удобные соглашения о вызовах

System V x86 использует стек и System V x86-64 использует rdi, rsi, rdx, rcxи т.д. для входных параметров, а также в raxкачестве возвращаемого значения, но это вполне разумно использовать свое собственное соглашение о вызовах. __fastcall использует ecxи в edxкачестве входных параметров, а другие компиляторы / ОС используют свои собственные соглашения . Используйте стек и все, что записывается как ввод / вывод, когда это удобно.

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

Meta: запись ввода в регистры , запись вывода в регистры

Другие ресурсы: заметки Агнера Фога о соглашениях о вызовах


1
Я наконец-то нашел время, чтобы опубликовать свой собственный ответ на этот вопрос о составлении соглашений о вызовах, а также о том, что является разумным против неразумного.
Питер Кордес

@PeterCordes не имеет отношения, как лучше всего печатать в x86? До сих пор я избегал проблем, которые требуют печати. DOS, похоже, имеет полезные прерывания для ввода / вывода, но я только планирую писать 32/64 битные ответы. Единственный способ, который я знаю, - это то, int 0x80что требуется куча настроек.
QWR

Да, int 0x80в 32-битном коде или syscallв 64-битном коде sys_write- единственный хороший способ. Это то, что я использовал для Extreme Fibonacci . В 64-битном коде __NR_write = 1 = STDOUT_FILENO, так что вы можете mov eax, edi. Или, если старшие байты EAX равны нулю, mov al, 4в 32-битном коде. Вы также можете call printfили puts, я думаю, написать ответ «x86 asm для Linux + glibc». Я думаю, что не стоит считать пространство ввода PLT или GOT или сам код библиотеки.
Питер Кордес

1
Я был бы более склонен к тому, чтобы вызывающий передавал и создавал char*bufстроку в ней с ручным форматированием. например, как это (неловко оптимизировано для скорости) asm FizzBuzz , где я получил строковые данные в регистр, а затем сохранил их сmov , потому что строки были короткими и фиксированной длины.
Питер Кордес

1

Используйте условные ходы CMOVccи наборыSETcc

Это скорее напоминание для меня, но инструкции по условному набору существуют и инструкции по условному перемещению существуют на процессорах P6 (Pentium Pro) или новее. Существует много инструкций, основанных на одном или нескольких флагах, установленных в EFLAGS.


1
Я обнаружил, что ветвление обычно меньше. Есть некоторые случаи, когда это естественно, но cmovимеет 2-байтовый код операции ( 0F 4x +ModR/M), так что это минимум 3 байта. Но источником является r / m32, поэтому вы можете условно загрузить его в 3 байта. Помимо ветвления, setccполезно в большем количестве случаев, чем cmovcc. Тем не менее, рассмотрим весь набор инструкций, а не только базовые 386 инструкций. (Хотя инструкции SSE2 и BMI / BMI2 настолько велики, что они редко бывают полезными. Их длина rorx eax, ecx, 32составляет 6 байт, они длиннее, чем mov + ror. Отличная производительность, а не игра в гольф, если только POPCNT или PDEP не спасут много иснов)
Peter Cordes

@PeterCordes спасибо, я добавил setcc.
Qwr

1

Экономьте на jmpбайтах, упорядочивая if / then, а не if / then / else

Это, конечно, очень просто, просто подумал, что я опубликую это как то, о чем нужно подумать, играя в гольф. В качестве примера рассмотрим следующий простой код для декодирования шестнадцатеричного символа:

    cmp $'A', %al
    jae .Lletter
    sub $'0', %al
    jmp .Lprocess
.Lletter:
    sub $('A'-10), %al
.Lprocess:
    movzbl %al, %eax
    ...

Это может быть сокращено на два байта, позволяя падежу «then» попасть в регистр «else»:

    cmp $'A', %al
    jb .digit
    sub $('A'-'0'-10), %eax
.digit:
    sub $'0', %eax
    movzbl %al, %eax
    ...

Вы часто делаете это обычно при оптимизации производительности, особенно когда дополнительная subзадержка на критическом пути для одного случая не является частью цепочки зависимостей, переносимых циклом (как здесь, где каждая входная цифра независима до слияния 4-битных блоков ). Но я думаю, +1 в любом случае. Кстати, в вашем примере есть отдельная пропущенная оптимизация: если вам все movzxравно понадобится конец, то используйте sub $imm, %alnot EAX, чтобы воспользоваться 2-байтовым кодированием no-modrm op $imm, %al.
Питер Кордес

Кроме того, вы можете устранить cmp, делая sub $'A'-10, %al; jae .was_alpha;add $('A'-10)-'0', (Я думаю, что я понял логику правильно). Обратите внимание, что 'A'-10 > '9'так нет никакой двусмысленности. Вычитая исправление для буквы, мы обернем десятичную цифру. Так что это безопасно, если мы предполагаем, что наши входные данные являются действительными шестнадцатеричными, как и ваши.
Питер Кордес

0

Вы можете извлекать последовательные объекты из стека, задав для esi значение esp и выполнив последовательность lodsd / xchg reg, eax.


Почему это лучше, чем pop eax/ pop edx/ ...? Если вам нужно оставить их в стеке, вы можете pushвернуть их обратно для восстановления ESP, по-прежнему 2 байта на объект без необходимости mov esi,esp. Или вы имели в виду для 4-байтовых объектов в 64-битном коде, где popбы получить 8 байт? Кстати, вы даже можете использовать popдля зацикливания буфера с более высокой производительностью, чем lodsd, например, для сложения с повышенной точностью в Extreme Fibonacci
Peter Cordes

это более правильно использовать после "lea esi, [esp + size ret address]", что исключает использование pop, если у вас нет запасного регистра.
Питер Ферри

О, для функции args? Довольно редко вам нужно больше аргументов, чем регистров, или вы хотите, чтобы вызывающая сторона оставляла один в памяти вместо передачи их всех в регистрах. (У меня есть неполный ответ об использовании пользовательских соглашений о вызовах, в случае, если одно из стандартных соглашений о вызовах в реестре не подходит идеально.)
Питер Кордес

Cdecl вместо fastcall оставит параметры в стеке, и очень легко иметь много параметров. См. Например, github.com/peterferrie/tinycrypt.
Питер Ферри

0

Для Codegolf и ASM: используйте инструкции, используйте только регистры, нажмите всплывающее окно, минимизируйте память регистров или память немедленно


0

Чтобы скопировать 64-битный регистр, используйте push rcx; pop rdxвместо 3-х байт mov.
Размер операнда по умолчанию для push / pop - 64-битный без префикса REX.

  51                      push   rcx
  5a                      pop    rdx
                vs.
  48 89 ca                mov    rdx,rcx

(Префикс размера операнда может переопределить размер push / pop до 16-битного, но 32-битный размер операнда / pop не может быть закодирован в 64-битном режиме даже если REX.W = 0.)

Если один или оба регистра являются r8.. r15, используйтеmov потому что для push и / или pop потребуется префикс REX. В худшем случае это на самом деле проигрывает, если обоим нужны префиксы REX. Очевидно, что вы все равно должны избегать r8..r15 в кодовом гольфе.


Во время разработки с этим макросом NASM вы можете сделать свой источник более читабельным . Просто помните, что он идет на 8 байтов ниже RSP. (В красной зоне в x86-64 System V). Но в нормальных условиях это замена для 64-битной mov r64,r64илиmov r64, -128..127

    ; mov  %1, %2       ; use this macro to copy 64-bit registers in 2 bytes (no REX prefix)
%macro MOVE 2
    push  %2
    pop   %1
%endmacro

Примеры:

   MOVE  rax, rsi            ; 2 bytes  (push + pop)
   MOVE  rbp, rdx            ; 2 bytes  (push + pop)
   mov   ecx, edi            ; 2 bytes.  32-bit operand size doesn't need REX prefixes

   MOVE  r8, r10             ; 4 bytes, don't use
   mov   r8, r10             ; 3 bytes, REX prefix has W=1 and the bits for reg and r/m being high

   xchg  eax, edi            ; 1 byte  (special xchg-with-accumulator opcodes)
   xchg  rax, rdi            ; 2 bytes (REX.W + that)

   xchg  ecx, edx            ; 2 bytes (normal xchg + modrm)
   xchg  rcx, rdx            ; 3 bytes (normal REX + xchg + modrm)

xchgЧасть примера потому , что иногда вам нужно получить значение в EAX или RAX и не заботятся о сохранении старой копии. Однако push / pop не помогает вам обмениваться.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.