i386(x86-32)机器代码,8个字节(9B为无符号)
如果我们需要处理b = 0
输入,则+ 1B 。
amd64(x86-64)机器码,9个字节(无符号为10B,有符号或无符号的64b整数为14B 13B)
amd64上的无符号的10 9B随任一输入= 0中断
输入是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版本可以处理该问题。免费,请参见下文。
Mike Shlanta的回答是此出发点。我的循环与他的循环执行相同的操作,但是对于有符号整数,因为cdq
比短一字节xor edx,edx
。是的,在一个或两个输入为负的情况下,它确实可以正常工作。Mike的版本运行速度更快,并且在uop缓存中占用的空间更少(xchg
在Intel CPU上为3 uops,loop
在大多数CPU上确实很慢),但是此版本以机器代码的大小为准。
起初我没有注意到该问题需要未签名的 32位。返回xor edx,edx
而不是cdq
将花费一个字节。 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
或除外)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
完整的可运行测试程序,包括一个可在Godbolt Compiler Explorer上main
运行32和64b版本的printf("...", gcd(atoi(argv[1]), atoi(argv[2])) );
源代码和asm输出的程序。经过测试,可以在32位(-m32
),64位(-m64
)和x32 ABI(-mx32
)上使用。
还包括:仅使用重复减法的版本,对于无符号,甚至对于x86-64模式,其值为9B,并且可以将其输入之一输入任意寄存器中。但是,它不能处理任何一个输入都为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中具有in / outputs。如您所见,GNU C内联asm语法使汇编变得丑陋且嘈杂。这也是学习 asm的一种非常困难的方法。
实际上,它将编译并在.att_syntax noprefix
模式下工作,因为使用的所有insn均为单/无操作数或xchg
。并不是真正有用的观察。