i386(x86-32)マシンコード、8バイト(符号なしの場合は9B)
b = 0
入力を処理する必要がある場合は+ 1B 。
amd64(x86-64)マシンコード、9バイト(符号なしの場合は10B、符号付きまたは符号なしの64b整数の場合は14B 13B)
いずれかの入力= 0で破損するamd64の符号なしの場合は10 9B
入力はおよびの32ビットのゼロ以外の符号付き整数です。に出力します。eax
ecx
eax
## 32bit code, signed integers: eax, ecx
08048420 <gcd0>:
8048420: 99 cdq ; shorter than xor edx,edx
8048421: f7 f9 idiv ecx
8048423: 92 xchg edx,eax ; there's a one-byte encoding for xchg eax,r32. So this is shorter but slower than a mov
8048424: 91 xchg ecx,eax ; eax = divisor(from ecx), ecx = remainder(from edx), edx = quotient(from eax) which we discard
; loop entry point if we need to handle ecx = 0
8048425: 41 inc ecx ; saves 1B vs. test/jnz in 32bit mode
8048426: e2 f8 loop 8048420 <gcd0>
08048428 <gcd0_end>:
; 8B total
; result in eax: gcd(a,0) = a
このループ構造は、テストケースwhereに失敗しますecx = 0
。(ゼロ除算でハードウェアの実行をdiv
引き起こし#DE
ます。(Linuxでは、カーネルはSIGFPE
(浮動小数点例外)を配信します。)ループエントリポイントがの直前であればinc
、問題を回避できます。x86-64バージョンはそれを処理できます。無料で、以下を参照してください。
マイク・シュランタの答えがこれの出発点でした。私のループは彼と同じことをしますが、符号付き整数の場合cdq
は1バイト短いためxor edx,edx
です。はい、一方または両方の入力が負の場合でも正しく機能します。Mikeのバージョンはより高速に実行され、xchg
uop キャッシュのスペースが少なくなります(Intel CPUでは3 uopでloop
、ほとんどのCPUでは本当に遅い)が、このバージョンはマシンコードサイズで勝ちます。
質問が符号なし 32ビットを必要とすることを最初は気がつきませんでした。のxor edx,edx
代わりに戻ると、cdq
1バイトかかります。 はとdiv
同じサイズidiv
であり、他のすべては同じままでかまいません(xchg
データの移動と動作のためinc/loop
)。
興味深いことに、64ビットのオペランドサイズ(rax
およびrcx
)の場合、符号付きバージョンと符号なしバージョンは同じサイズです。署名バージョンにはcqo
(2B)のREXプレフィックスが必要ですが、署名なしバージョンでは2Bを使用できますxor edx,edx
。
64ビットコードでinc ecx
は、2Bです。シングルバイトinc r32
およびdec r32
オペコードはREXプレフィックスとして再利用されました。 inc/loop
64ビットモードではコードサイズが保存されないため、同様に保存できますtest/jnz
。64ビット整数を操作すると、REXプレフィックスの命令ごとに、loop
またはを除く別の1バイトが追加されますjnz
。残りの32b(例gcd((2^32), (2^32 + 1))
:)にすべてゼロがある可能性があるため、rcx全体をテストする必要があり、でバイトを保存できませんtest ecx,ecx
。ただし、遅いjrcxz
insnは2Bのみであり、ループの先頭に配置ecx=0
してエントリ時に処理できます。
## 64bit code, unsigned 64 integers: rax, rcx
0000000000400630 <gcd_u64>:
400630: e3 0b jrcxz 40063d <gcd_u64_end> ; handles rcx=0 on input, and smaller than test rcx,rcx/jnz
400632: 31 d2 xor edx,edx ; same length as cqo
400634: 48 f7 f1 div rcx ; REX prefixes needed on three insns
400637: 48 92 xchg rdx,rax
400639: 48 91 xchg rcx,rax
40063b: eb f3 jmp 400630 <gcd_u64>
000000000040063d <gcd_u64_end>:
## 0xD = 13 bytes of code
## result in rax: gcd(a,0) = a
32および64bバージョンのGodbolt Compiler Explorerでソースおよびasm出力main
を実行するを含む、完全に実行可能なテストプログラム。32ビット()、64ビット()、およびx32 ABI()でテストおよび動作しています。printf("...", gcd(atoi(argv[1]), atoi(argv[2])) );
-m32
-m64
-mx32
また含まれています:x86-64モードの場合でも、符号なしで9Bであり、任意のレジスタで入力の1つを取ることができる、繰り返し減算のみを使用するバージョン。ただし、入力時に0である入力を処理することはできません(sub
ゼロを生成するタイミングを検出しますが、x-0は実行しません)。
32ビットバージョンのGNU Cインラインasmソース(でコンパイルgcc -m32 -masm=intel
)
int gcd(int a, int b) {
asm (// ".intel_syntax noprefix\n"
// "jmp .Lentry%=\n" // Uncomment to handle div-by-zero, by entering the loop in the middle. Better: `jecxz / jmp` loop structure like the 64b version
".p2align 4\n" // align to make size-counting easier
"gcd0: cdq\n\t" // sign extend eax into edx:eax. One byte shorter than xor edx,edx
" idiv ecx\n"
" xchg eax, edx\n" // there's a one-byte encoding for xchg eax,r32. So this is shorter but slower than a mov
" xchg eax, ecx\n" // eax = divisor(ecx), ecx = remainder(edx), edx = garbage that we will clear later
".Lentry%=:\n"
" inc ecx\n" // saves 1B vs. test/jnz in 32bit mode, none in 64b mode
" loop gcd0\n"
"gcd0_end:\n"
: /* outputs */ "+a" (a), "+c"(b)
: /* inputs */ // given as read-write outputs
: /* clobbers */ "edx"
);
return a;
}
通常、関数全体をasmで記述しますが、GNU Cインラインasmは、選択したregで入出力できるスニペットを含める最適な方法のようです。ご覧のとおり、GNU Cインラインasm構文はasmをugくてうるさくします。また、asm を学ぶのは本当に難しい方法です。
.att_syntax noprefix
使用されるすべてのinsnがsingle / no operandまたはであるため、実際にモードでコンパイルおよび動作しますxchg
。本当に有用な観察ではありません。