x86 / x64マシンコードでのゴルフのヒント


27

私はそのような質問がないことに気づいたので、ここにあります:

マシンコードでゴルフをするための一般的なヒントはありますか?ヒントが特定の環境または呼び出し規約にのみ適用される場合は、回答でそれを指定してください。

回答ごとに1つのヒントのみを入力してください(こちらを参照)。

回答:


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ビットの符号拡張値は、push -12(2バイト)/ pop %eax(1バイト)で3バイトで作成できます。 これは、追加のREXプレフィックスのない64ビットレジスタでも機能します。push/ popデフォルトのオペランドサイズ= 64。

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

または、レジスタに既知の定数を指定すると、lea 123(%eax), %ecx(3バイト)を使用して近くに別の定数を作成できます。これは、ゼロ化されたレジスタ定数が必要な場合に便利です。xor-zero(2バイト)+ lea-disp8(3バイト)。

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

CPUレジスタのすべてのビットを1に効率的に設定するも参照してください。


また、0以外の小さな(8ビット)値でレジスタを初期化するには、初期化にpush 200; pop edx-3バイトなどを使用します。
アナトリグ

2
ところで-1にレジスタを初期化するために、使用dec、例えばxor eax, eax; dec eax
anatolyg

@anatolyg:200は貧弱な例であり、sign-extended-imm8には適合しません。しかし、はい、push imm8/ pop reg3バイトで、x86-64で、上の64ビットの定数のための素晴らしいですdec/ inc2バイトです。そしてpush r64/ pop 64(2バイト)は3バイトmov r64, r64(REXで3バイト)を置き換えることさえできます。参照して効率的に1にCPUレジスタのすべてのビットを設定してくださいのようなもののためlea eax, [rcx-1]にある与えられた既知の値eax(例えば必要性ゼロ化されたレジスタの場合および /ポップ別の定数、代わりにプッシュだけ使用LEA
ピーター・コルド

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(実際はマシンコード)です。したがって、C-compiled-for-x86ではなくasmで記述されたプログラムの一部として扱います。 関数は、標準の呼び出し規約を使用してCから簡単に呼び出すことができる必要はありません。ただし、余分なバイト数がかからない場合、これは素晴らしいボーナスです。

純粋なasmプログラムでは、一部のヘルパー関数は、それらと呼び出し元にとって便利な呼び出し規約を使用するのが普通です。このような関数は、呼び出し規約(入力/出力/クラッバー)をコメントで文書化します。

実際には、asmプログラムでも(ほとんどのソースファイル間で)ほとんどの関数で一貫した呼び出し規則を使用する傾向がありますが、重要な関数は何か特別なことができます。code-golfでは、1つの関数のがらくたを最適化するので、明らかにそれは重要/特別です。


Cプログラムから関数をテストするには、引数を適切な場所に配置し、クローバーする余分なレジスタを保存/復元し、戻り値e/raxがまだない場合はその中に戻り値を入れるラッパー作成できます。


合理的な範囲:呼び出し元に不当な負担をかけないもの:

  • ESP / RSPはコール保存する必要があります。他の整数regは公平なゲームです。(RBPとRBXは通常、通常の規則では通話が保持されます、両方を上書きすることもできます。)
  • 任意のレジスタ(RSPを除く)内の任意の引数は妥当ですが、同じ引数を複数のレジスタにコピーするように呼び出し側に要求することは適切ではありません。
  • DF(lods/ stos/などの文字列方向フラグ)を呼び出し/ retでクリア(上方向)にすることは正常です。call / retで未定義にするのは問題ありません。エントリでクリアまたは設定する必要がありますが、戻ったときに変更したままにしておくのは奇妙です。

  • x87でFP値を返すことst0は妥当ですがst3、他のx87レジスタでゴミを入れて返すことはできません。呼び出し元はx87スタックをクリーンアップする必要があります。st0空ではないより高いスタックレジスタで戻ることも疑問です(複数の値を返す場合を除く)。

  • 関数はで呼び出されるcallため[rsp]、返信先アドレスも呼び出されます。/ などのリンクレジスタを使用してx86で/ を回避し、で返すことができます、それは「合理的」ではありません。それはcall / retほど効率的ではないので、実際のコードでもっともらしく見つけることはできません。callretlea rbx, [ret_addr]jmp functionjmp rbx
  • RSPを超える無制限のメモリを上書きすることは合理的ではありませんが、通常の呼び出し規約では、スタック上の関数引数を上書きすることは許可されています。x64 Windowsは戻りアドレスの上に32バイトのシャドウスペースを必要としますが、x86-64 System VはRSPの下に128バイトのレッドゾーンを提供するため、どちらも妥当です。(または、特に機能ではなくスタンドアロンプ​​ログラムでは、はるかに大きなレッドゾーンです。)

境界線の場合:最初の2つの要素を関数argsとして指定して、配列にシーケンスを生成する関数を作成します。 私が選んだの配列に、発信者の店にシーケンスの開始を持っているだけで、配列へのポインタを渡します。これは間違いなく質問の要件を曲げています。xmm0for movlps [rdi], xmm0にパックされた引数を取ることを検討しましたが、これも奇妙な呼び出し規約になります。


FLAGSでブール値を返します(条件コード)

OS Xシステムコールはこれを行います(CF=0エラーがないことを意味します)。フラグレジスタをブール値の戻り値として使用するのは悪い習慣と見なされますか

1つのJCCでチェックできる条件は完全に合理的です。特に問題に意味的な関連性がある条件を選択できる場合はそうです。(たとえば、比較関数はフラグを設定する可能性があるためjne、それらが等しくなかった場合に取得されます)。


狭い引数(のようなchar)を符号または32ビットまたは64ビットにゼロ拡張する必要があります。

これは不合理ではありません。部分的なレジスタのスローダウンを使用しmovzxたりmovsx 、回避したりすることは、最新のx86 asmでは普通です。:実際の打ち鳴らすに/ LLVMは既にx86-64のシステムV呼び出し規約に文書化されていない拡張に依存するコードを行う32ビットより狭い引数は、呼び出し側によって32ビットに符号拡張またはゼロです

必要に応じて、プロトタイプを作成するuint64_tint64_t、作成することにより、64ビットへの拡張機能を文書化/記述できます。たとえば、loop命令を使用できます。命令は、アドレスサイズプレフィックスを使用してサイズを32ビットECXまでオーバーライドしない限り、RCXの64ビット全体を使用します(実際には、オペランドサイズではなくアドレスサイズ)。

これlongは、Windows 64ビットABIおよびLinux x32 ABIでは 32ビットタイプのみであることに注意してください。uint64_tは明確であり、入力よりも短いunsigned long long


既存の呼び出し規約:

  • Windows 32ビット__fastcallすでに別の答えによって提案された整数の引数で:ecxedx

  • x86-64 System V:多くの引数をレジスタに渡し、REXプレフィックスなしで使用できるコールクローバーレジスタをたくさん持っています。さらに重要なことは、実際にコンパイラーがインラインmemcpyまたはmemsetをrep movsb簡単にインライン化できるように選択されたことです。最初の6つの整数/ポインター引数は、RDI、RSI、RDX、RCX、R8、R9で渡されます。

    関数が(命令を使用して)時間を実行するループ内でlodsd/ stosdを使用する場合、「x86-64 System V呼び出し規約と同様にCから呼び出し可能」と言うことができます。 例:chromakeyrcxloopint foo(int *rdi, const int *rsi, int dummy, uint64_t len)

  • 32ビットGCC regparmEAX、ECX、EDXの整数引数は、EAX(またはEDX:EAX)で返されます。戻り値と同じレジスタに最初の引数があると、この例のような呼び出し元の例と関数属性のプロトタイプのように、いくつかの最適化が可能になります。そしてもちろん、AL / EAXはいくつかの命令にとって特別です。

  • Linux x32 ABIはロングモードで32ビットポインターを使用するため、ポインターを変更するときにREXプレフィックスを保存できます(use-caseの例)。レジスタにゼロ拡張された32ビットの負の整数がない限り、64ビットのアドレスサイズを引き続き使用できます(そうすると、大きな符号なしの値になります[rdi + rdx])。

    push rsp/ pop raxは2バイトで、と同等であるためmov rax,rsp、2バイトで完全な64ビットレジスタをコピーできることに注意してください。


課題が配列を返すように要求するとき、スタックに戻ることは合理的だと思いますか?値で構造体を返すときにコンパイラが行うことだと思います。
qwr

@qwr:いいえ、主流の呼び出し規約は戻り値に隠しポインターを渡します。(いくつかの規則は、レジスター内の小さな構造体を渡す/返す)。 C / C ++がフードの下にある値によって構造体を返し、アセンブリレベルでx86でオブジェクトどのように機能するかを参照してください。配列(構造体内部)を渡すと、x86-64 SysVのスタックにコピーされることに注意してください。AMD64ABI による配列はどのようなC11データ型ですが、 Windows x64は非constポインターを渡します。
ピーターコーデス

だからあなたは合理的かどうかについてどう思いますか?この規則の下でx86をカウントしますcodegolf.meta.stackexchange.com/a/8507/17360
qwr

1
@qwr:x86は「スタックベースの言語」ではありません。x86はスタックマシンではなく、RAMを備えたレジスタマシンです。スタックマシンは、x87レジスタのような逆ポーランド記法に似ています。fld / fld / faddp。x86の呼び出しスタックはそのモデルに適合しません。通常の呼び出し規約はすべてRSPを変更しないままにするか、引数をret 16;でポップします。リターンアドレスをポップせず、配列をプッシュしてから、push rcx/をプッシュしretます。呼び出し元は、配列サイズを知っているか、スタックの外側のどこかにRSPを保存して自分自身を見つける必要があります。
ピーターコーデス

呼び出しjj スタックからアドレスをポップし、そのアドレスにjmpをポップします
RosLuP

7

AL / AX / EAX、およびその他の短い形式とシングルバイト命令に特別な場合の短い形式のエンコードを使用する

例では、32/64ビットモードを想定しています。デフォルトのオペランドサイズは32ビットです。オペランドサイズのプレフィックスは、命令をEAXではなくAXに変更します(または16ビットモードでは逆になります)。

  • inc/decレジスタ(8ビット以外):inc eax/ dec ebp。(x86-64ではありません:0x4xオペコードバイトはREXプレフィックスとして再利用されたためinc r/m32、唯一のエンコーディングです。)

    8ビットinc blは2バイトで、inc r/m8オペコード+ ModR / Mオペランドエンコーディングを使用します。したがって、安全であれば、を使用inc ebxしてインクリメントしますbl(たとえば、上位バイトがゼロ以外の場合にZFの結果が必要ない場合)。

  • scasde/rdi+=4、レジスタが読み取り可能なメモリを指す必要があります。FLAGSの結果を気にしなくても役立つ場合があります(cmp eax,[rdi]/などrdi+=4)。また、64ビットモードでscasbinc rdi、lodsbまたはstosbが役に立たない場合は1バイトとして機能します

  • xchg eax, r32:これは、0x90 NOPの由来ですxchg eax,eax。例:GCDの / ループxchg内の2つの命令で3つのレジスタを8バイトで再配置します。ほとんどの命令はシングルバイトで、/の代わりに/の不正使用を含みますcdqidivinc ecxlooptest ecx,ecxjnz

  • cdq:EAXをEDX:EAXに符号拡張します。つまり、EAXの上位ビットをEDXのすべてのビットにコピーします。既知の非負のゼロを作成するか、追加/サブまたはマスクする0 / -1を取得します。 x86ヒストリーレッスン:cltqvsmovslq .およびAT&T vs. 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アスキー数字命令の使用法を見つけたことがありません。

1バイトlahf/ sahfはめったに役立ちません。/ の代わりに/ を使用でき ます、通常は役に立ちません。lahfand ah, 1setc ah

また、特にCFについてsbb eax,eaxは、0 / -1を取得するか、ドキュメント化されていないが普遍的にサポートされている1バイトsalc(キャリーからALを設定)を取得する必要がありsbb al,alます。(x86-64で削除)。ユーザー感謝チャレンジ#1:デニス♦で SALCを使用しました。

1バイトのcmc/ clc/ stc(flip( "complement")、clear、またはset CF)はほとんど役に立たないが、ベース10 ^ 9チャンクでの拡張精度加算での使用をcmc見つけ。CFを無条件に設定/クリアするには、通常、それを別の命令の一部として実行するように調整しxor eax,eaxます。たとえば、CFとEAXをクリアします。他の条件フラグに相当する命令はなく、DF(文字列方向)とIF(割り込み)のみがあります。キャリーフラグは、多くの命令にとって特別です。シフトはそれを設定し、adc al, 02バイトでALに追加できます。また、以前に文書化されていないSALCについて言及しました。

std/ cldめったに価値がないと思われる。特に32ビットコードでは、より良いだけ使用へだdecポインタ上及びmovまたはメモリソースオペランドALU命令に代えてDFを設定するようにlodsb/ stosb代わりに最大の下方へ行きます。あなたがすべてで下方必要がある場合は、1個以下の必要があると思いますので、通常、あなたはまだ、上がって別のポインタを持っているstdし、cld使用する関数全体でlods/ stosの両方のために。代わりに、上方向の文字列命令を使用してください。(標準の呼び出し規約では、関数のエントリでDF = 0が保証されているため、を使用せずに無料で想定できますcld。)


8086履歴:これらのエンコーディングが存在する理由

同様の手順:オリジナル8086では、AXは非常に特別だったlodsb/ stosbcbwmul/ divなどが暗黙のうちにそれを使用しています。もちろん、それはもちろんです。現在のx86は8086のオペコード(少なくとも公式に文書化されたオペコードのどれも)をドロップしていません。しかし、後にCPUが新しい命令を追加して、最初にAXにコピーまたはスワップすることなく、より良い/より効率的な方法を実現しました。(または32ビットモードでEAXに。)

例えば、8086には、ロードまたは移動+符号拡張、または上位半分の結果を生成せず、暗黙のオペランドを持たない2および3オペランドのmovsx/のような後の追加がありませんでした。movzximul cx, bx, 1234

また、8086の主なボトルネックは命令フェッチであったため、当時のパフォーマンスにとってコードサイズの最適化は重要でした。8086 のISAデザイナー(Stephen Morse)は、すべての基本的なimmediate-src ALU-命令の特別な(E)AX / AL-destinationオペコードを含む、AX / ALの特殊なケースに、オペコード+イミディエートだけで多くのオペコードコーディングスペースを費やしました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

lodsb/wEAXでのイミディエートのすべての特殊なケースエンコーディングから乗算/除算までの暗黙的な使用まで、8086命令エンコーディングに関するすべてがこのパラダイムをサポートしています。


夢中にならないでください。特に8ビットではなく32ビットのレジスタでイミディエイトを使用する必要がある場合は、すべてをEAXにスワップすることは自動的に勝利ではありません。または、レジスタ内の複数の変数に対する操作を一度にインターリーブする必要がある場合。または、イミディエイトではなく、2つのレジスターを持つ命令を使用している場合。

ただし、常に念頭に置いてください。EAX/ ALで短くなることは何ですか?ALでこれができるように再配置できますか、それとも既に使用しているALをより有効に活用していますか。

8ビット操作と32ビット操作を自由に組み合わせて、安全に実行できる場合はいつでも利用できます(フルレジスタなどにキャリーアウトする必要はありません)。


cdq多くの場合、divゼロ化が必要な場合に便利ですedx
qwr

1
@qwr:正しい。配当が2 ^ 31未満(cdq符号付きdivとして扱われる場合に負でない)であることがわかっている場合、またはeax潜在的に大きな値に設定する前に使用する場合、符号なしの前に悪用できます。通常(外側のコードゴルフ)あなたがしたい使用cdqするための設定としてidiv、そしてxor edx,edxdiv
ピーター・コルド

5

使用fastcall規則

x86プラットフォームには多くの呼び出し規約があります。レジスタでパラメータを渡すものを使用する必要があります。x86_64では、最初のいくつかのパラメーターがレジスタで渡されるため、問題はありません。32ビットプラットフォームでは、デフォルトの呼び出し規則(cdecl)がパラメーターをスタックに渡しますが、これはゴルフには適していません。スタック上のパラメーターにアクセスするには長い命令が必要です。

使用する場合fastcall、32ビットプラットフォーム上で、2つの最初のパラメータは、通常に渡されるecxedx。関数に3つのパラメーターがある場合、64ビットプラットフォームに実装することを検討できます。

fastcallコンベンション用のC関数プロトタイプ(この回答例から取得):

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

mul(その後inc/でdec+1 / -1とゼロを取得する)で3つのゼロを作成します

3番目のレジスタでゼロを乗算することにより、eaxとedxをゼロにできます。

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

inc   ebx           ; 1B  ebx=1

EAX、EDX、およびEBXがすべて4バイトでゼロになります。3バイトでEAXとEDXをゼロにすることができます。

xor eax, eax
cdq

しかし、その開始点から、もう1バイトで3番目にゼロ化されたレジスターを取得することも、別の2バイトで+1または-1レジスターを取得することもできません。代わりに、mulテクニックを使用してください。

ユースケースの例:フィボナッチ数をバイナリに連結します。

LOOPループが終了すると、ECXはゼロになり、EDXとEAXをゼロにするために使用できることに注意してください。で最初のゼロを作成する必要はありませんxor


1
これは少しわかりにくいです。拡大してもらえますか?
-NoOneIsHere

@NoOneIsHere彼は、EAXとEDXを含む3つのレジスタを0に設定したいと考えています。
-NieDzejkob

4

CPUレジスタとフラグが既知の起動状態にある

CPUは、プラットフォームとOSに基づいて、既知の文書化されたデフォルト状態にあると想定できます。

例えば:

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

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


1
コードゴルフのルールでは、コードは少なくとも1つの実装で動作する必要があるとされています。Linuxは、i386およびx86-64 System V ABIのドキュメントでがに入場すると「未定義」であると言っていても、すべてのreg(RSPを除く)とスタックをゼロにしてから新しいユーザー空間プロセスに入り_startます。そのため、関数ではなくプログラムを作成している場合、それを利用するのは公正なゲームです。私はエクストリームフィボナッチでそうしました。(動的にリンクされた実行可能ファイルで、実行ld.soジャンプの前にあなたに_start、そしてないレジスタの休暇のゴミを、しかし、静的なだけであなたのコードである。)
ピーター・コルド

3

1を加算または減算するには、マルチバイトの加算およびサブ命令よりも小さい1バイトincまたはdec命令を使用します。


32ビットモードにはinc/dec r32、レジスタ番号がオペコードでエンコードされた1バイトがあることに注意してください。したがってinc ebx、1バイトですがinc bl、2です。add bl, 1もちろん、それ以外のレジスタの場合は、それよりも小さくなりalます。また、inc/ decはCFを変更せずに残しますが、他のフラグは更新します。
ピーター

1
x86
l4m2

3

lea 数学用

これはおそらくx86について最初に学ぶことの1つですが、ここでは念のために残しておきます。lea2、3、4、5、8、または9で乗算を行い、オフセットを追加するために使用できます。

たとえばebx = 9*eax + 3、1つの命令で計算するには(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>2つの命令シーケンスよりも小さいdec ECXjnz <label>、そしてlodsbよりも小さくなっているmov al,[esi]inc si


2

mov 該当する場合、下位レジスタへの小さな即値

レジスタの上位ビットが0であることが既にわかっている場合は、短い命令を使用して、即値を下位レジスタに移動できます。

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

b0 0a                   mov    $0xa,%al

imm8にpush/ popを使用して上位ビットをゼロにする

Peter Cordesの功績。xor/ movは4バイトですが、push/ popは3だけです!

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

mov al, 0xa完全なregにゼロ拡張する必要がない場合に適しています。しかし、そうすると、xor / movは4バイトであるのに対して、プッシュimm8 / popまたはlea他の既知の定数からは3バイトです。これは、組み合わせに有用であり得るmulゼロ3 4バイトのレジスタへ、またはcdqかかわらず、あなたは定数の多くを必要とする場合は、。
ピーター

もう1つのユースケースは[0x80..0xFF]、からの定数で、これは符号拡張されたimm8として表現できません。または、例えば命令の後など、すでに上位バイトを知っている場合は、ジャンプしない唯一の方法は作成されたときです。(あなたこれを言ったと思いますが、あなたの例ではを使用しています)。レジスタの下位バイトを他の何かに使用することもできます。ただし、完了時に他の何かがゼロ(または何でも)に戻す限りです。たとえば、私のフィボナッチプログラムはebxを保持し、blを使用します。mov cl, 0x10looplooprcx=0xor-1024
ピーター

私はあなたのプッシュ/ポップ技術を追加しました@PeterCordes
QWR

おそらく、anatolygがすでにコメントで提案している定数に関する既存の答えに進むべきです。その答えを編集します。IMOでは、これを作り直して、8ビットのオペランドサイズを使用して、(/ を除くxchg eax, r32)より多くのもの(たとえば、mov bl, 10/ dec bl/ )を使用することをお勧めしjnzます。
ピーター

@PeterCordesうーん。いつ8ビットオペランドを使用するかについてはまだよく分からないので、その答えに何を入れるべきかわかりません。
qwr

2

FLAGSは、多くの命令の後に設定されています

多くの算術命令の後、キャリーフラグ(符号なし)およびオーバーフローフラグ(符号付き)が自動的に設定されます(詳細)。符号フラグとゼロフラグは、多くの算術演算と論理演算の後に設定されます。これは条件分岐に使用できます。

例:

d1 f8                   sar    %eax

ZFはこの命令によって設定されるため、条件分岐に使用できます。


パリティフラグはいつ使用したことがありますか?結果の下位8ビットの水平方向のxorであることを知っていますか?(かかわらず、オペランド・サイズの、PFは、下位8ビットからのみ設定されている ;参照)。偶数/奇数ではありません。そのためにZFをチェックしてくださいtest al,1。通常は無料で入手できません。(またはand al,1奇数/偶数に応じて整数0/1を作成します。)
ピーター

とにかく、この答えが「test/ を避けるために他の命令によって既に設定されたフラグを使用する」と言った場合cmp、それはかなり基本的な初心者のx86ですが、それでも賛成に値します。
ピーター

@PeterCordesええ、パリティフラグを誤解していたようです。私はまだ他の答えに取り組んでいます。答えを編集します。おそらくおわかりのように、私は初心者なので基本的なヒントが役立ちます。
qwr

2

whileループの代わりにdo-whileループを使用する

これはx86固有ではありませんが、広く適用可能な初心者向けアセンブリのヒントです。whileループが少なくとも1回実行されることがわかっている場合、ループをdo-whileループとして書き換え、最後にループ条件チェックを行うと、多くの場合2バイトのジャンプ命令が節約されます。特別な場合には、を使用することもできますloop


2
関連:なぜループは常にこのようにコンパイルされるのですか?do{}while()アセンブリの自然なループイディオムが(特に効率のため)なぜであるかを説明します。注また、2バイトのことjecxz/ jrcxzループは非常によく働く前にloop(まれCPU上「効率的」の場合、「ゼロ回を実行する必要がある」処理するためにloop遅いではありませんが)。 使用可能な内部ループを実装すると、下部に。jecxzwhile(ecx){}jmp
ピーター

@PeterCordesは非常によく書かれた答えです。コードゴルフプログラムのループの途中にジャンプするための使用方法を見つけたいです。
qwr

goto jmpとインデントを使用...ループフォロー
RosLuP

2

便利な呼び出し規約を使用する

System Vのx86のは、スタックを使用し、System Vのx86-64の用途はrdirsirdxrcx、などの入力パラメータについて、およびrax戻り値として、あなた自身の呼び出し規約を使用して完全に合理的です。__fastcallecxand edxを入力パラメーターとして使用し、他のコンパイラ/ OSは独自の規則を使用します。便利な場合は、スタックと任意のレジスタを入力/出力として使用します。

例:1バイトソリューションの巧妙な呼び出し規約を使用した反復バイトカウンター

メタ:レジスタへの入力を書き込みレジスタに出力を書きます

その他のリソース:呼び出し規約に関するAgner Fogのメモ


1
私はついに、呼び出し規約の作成に関するこの質問に関する独自の回答投稿することに取り掛かりました。
ピーターコーデス

@PeterCordesは無関係ですが、x86で印刷する最良の方法は何ですか?これまでのところ、印刷が必要な課題を避けてきました。DOSにはI / Oに便利な割り込みがあるように見えますが、32/64ビットの回答を書くことしか計画していません。私が知っている唯一の方法int 0x80は、たくさんのセットアップが必要です。
qwr

そう、int 0x8032ビットコードまたはsyscall64ビットコードで呼び出すのsys_writeが唯一の良い方法です。Extreme Fibonacciに使用したものです。64ビットコードで__NR_write = 1 = STDOUT_FILENOは、次のことができmov eax, ediます。またはmov al, 4、32ビットコードでEAXの上位バイトがゼロの場合。call printfまたはputs、「Linux + glibc用のx86 asm」という回答を書くこともできます。PLTやGOTのエントリスペース、またはライブラリコード自体をカウントしないのが妥当だと思います。
ピーターコーデス

1
呼び出し側にa char*bufを渡して、その中に文字列を手動でフォーマットさせて生成させたいと思います。たとえば、このように(速度に対して不便に最適化された)asm FizzBu​​zzの場合、文字列データがレジスタに格納され、それを使用して保存されmovます。
ピーターコーデス

1

条件付きの移動CMOVccとセットを使用するSETcc

これは私自身を思い出させるものですが、条件付きセット命令が存在し、条件付き移動命令がプロセッサP6(Pentium Pro)以降に存在します。EFLAGSで設定された1つ以上のフラグに基づいた多くの命令があります。


1
通常、分岐は小さいことがわかりました。自然にフィットする場合もありますcmovが、2バイトのオペコード(0F 4x +ModR/M)があるため、最小で3バイトです。ただし、ソースはr / m32であるため、条件付きで3バイトでロードできます。分岐以外にsetcc、より多くの場合に役立ちcmovccます。それでも、ベースラインの386命令だけでなく、命令セット全体を検討してください。(SSE2およびBMI / BMI2命令は、彼らはめったに便利だほど大きいですが rorx eax, ecx, 32、MOV + RORよりも長く、6バイトであるニースは、パフォーマンスのために、ではないゴルフがPOPCNTまたはPDEPは多くのISNを保存しない限り。)
ピーター・コルド

@PeterCordesありがとう、私は追加しましたsetcc
QWR

1

jmpif / then / elseではなくif / thenに配置することでバイトを節約

これは確かに非常に基本的なものであり、ゴルフのときに考えるものとしてこれを投稿すると思いました。例として、16進数字をデコードするための次の簡単なコードを検討してください。

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

これは、「then」ケースを「else」ケースに分類することで2バイト短縮できます。

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

パフォーマンスを最適化するとき、特にsub1つのケースのクリティカルパスでの余分なレイテンシがループキャリーの依存関係チェーンの一部ではない場合(各入力桁が4ビットチャンクをマージするまで独立している場合) )。しかし、とにかく+1。ところで、あなたの例には最適化されていない個別の最適化があります:movzx最後に必要な場合は、sub $imm, %alEAXを使用せずにの非modrm 2バイトエンコーディングを利用してくださいop $imm, %al
ピーターコーデス

また、あなたは排除することができますcmp行うことでsub $'A'-10, %aljae .was_alpha; add $('A'-10)-'0'。(論理は正しいと思います)。'A'-10 > '9'あいまいさはないことに注意してください。文字の修正を減算すると、10進数がラップされます。したがって、入力が有効な16進数であると想定している場合、これはあなたの入力と同じように安全です。
ピーターコーデス

0

esiをespに設定し、lodsd / xchg reg、eaxのシーケンスを実行することにより、スタックから順次オブジェクトをフェッチできます。


なぜこれがpop eax/ pop edx/ ... よりも優れているのでしょうか?それらをスタックに残す必要がある場合は、pushESPを復元するためにそれらをすべて戻すことができますが、オブジェクトごとに2バイトは必要ありませんmov esi,esp。または、64ビットコードの4バイトオブジェクトの場合、pop8バイトを取得するという意味ですか?ところで、たとえば、Extreme Fibonacci拡張精度を追加するpop場合などlodsd、より優れたパフォーマンスでバッファをループすることもできます。
Peter Cordes

"lea esi、[esp + size of ret address]"の後の方がより便利です。予備レジスタがない場合は、popを​​使用できません。
ピーターフェリー

ああ、関数の引数は?レジスタよりも多くの引数が必要な場合や、すべての引数をレジスタに渡すのではなく、呼び出し元にメモリ内に残しておくことが非常にまれです。(標準の登録呼び出し規則の1つが完全に収まらない場合のカスタム呼び出し規則の使用については、半ば答えがあります。)
ピーター

fastcallの代わりにcdeclを使用すると、パラメーターがスタックに残り、多くのパラメーターを簡単に設定できます。たとえば、github.com / peterferrie / tinycryptを参照してください。
ピーターフェリー

0

codegolfおよびASMの場合:使用手順では、レジスタのみを使用し、プッシュポップし、レジスタメモリを最小化するか、メモリを即時に


0

64ビットレジスタをコピーするにはpush rcx、;を使用します。pop rdx3バイトの代わりにmov
push / popのデフォルトのオペランドサイズは64ビットで、REXプレフィックスは不要です。

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

(オペランドサイズのプレフィックスはプッシュ/ポップサイズを16ビットにオーバーライドできますが、32ビットのプッシュ/ポップオペランドサイズは、REX.W = 0であっても64ビットモードエンコードできません。)

どちらかまたは両方のレジスタがr8.. r15である場合mov、プッシュおよび/またはポップにはREXプレフィックスが必要になるため、使用します。最悪の場合、両方にREXプレフィックスが必要な場合、実際にこれは失われます。明らかに、通常、コードゴルフではr8..r15を避ける必要があります。


このNASMマクロを使用して開発している間、ソースをより読みやすく保つことができます。RSPの下の8バイトをステップ実行することを覚えておいてください。(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に値を取得する必要があり、古いコピーを保存する必要がないためです。ただし、プッシュ/ポップは実際の交換には役立ちません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.