我试图弄清楚如何在汇编中计算模10,所以我在gcc中编译了以下c代码,看看它是什么产生的。
unsigned int i=999;
unsigned int j=i%10;
令我惊讶的是我得到了
movl -4(%ebp), %ecx
movl $-858993459, %edx
movl %ecx, %eax
mull %edx
shrl $3, %edx
movl %edx, %eax
sall $2, %eax
addl %edx, %eax
addl %eax, %eax
movl %ecx, %edx
subl %eax, %edx
movl %edx, %eax
movl %eax, -12(%ebp)
其中-4(%ebp)或“i”是输入,-12(%ebp)或“j”是答案。我已经测试了这个,无论你做出什么数字,它都能正常工作-4(%ebp)。
我的问题是这段代码是如何工作的,它如何比使用div操作符更好。
答案 0 :(得分:21)
第二个问题:div
是一个非常慢的指令(超过20个时钟周期)。上面的序列包含更多指令,但它们都相对较快,所以它在速度方面是一个净赢。
前五条指令(最多包括shrl
)计算i / 10(我将在一分钟内解释)。
接下来的几条指令再次将结果乘以10,但避免使用mul
/ imul
指令(这是否胜利取决于您所针对的确切处理器 - 较新的x86具有非常快的乘数,但是较旧的乘数不会。)
movl %edx, %eax ; eax=i/10
sall $2, %eax ; eax=(i/10)*4
addl %edx, %eax ; eax=(i/10)*4 + (i/10) = (i/10)*5
addl %eax, %eax ; eax=(i/10)*5*2 = (i/10)*10
然后再次从i
中扣除,以获得i - (i/10)*10
i % 10
(对于无符号数字)。
最后,关于i / 10的计算:基本思想是将除以10乘以1/10。编译器通过乘以(2 ** 35/10 + 1)进行定点逼近 - 这是加载到edx
中的魔法值,尽管它输出为有符号值,即使它实际上是无符号的 - 并且将结果右移35。这样可以为所有32位整数提供正确的结果。
有确定这种近似的算法可以保证误差小于1(对于整数意味着它是正确的值),而GCC显然使用了一个:)
最后评论:如果你想实际看到GCC计算模数,可以使用除数变量(例如函数参数),这样它就不能进行这种优化。无论如何,在x86上,您使用div
计算模数。 div
期望edx:eax
中的64位被除数(edx中的高32位,eax中的低32位 - 如果使用32位数字,则清除edx为零)并将其除以您指定的任何操作数(例如div ebx
将edx:eax
除以ebx
)。它返回eax
中的商和edx
中的余数。 idiv
对签名值执行相同的操作。
答案 1 :(得分:3)
第一部分,直到shrl $3, %edx
,实现快速整数除法10.有一些不同的算法在预先知道划分的数字时有效。注意,858993459是“0.2 * 2 ^ 32”。这样做的原因是,即使指令集中存在整数除法指令div
/ idiv
,它通常非常慢,比乘法慢几倍。
第二部分通过将除法结果乘以10来计算余数(以间接方式,通过移位和加法;可能是编译器认为它会更快),然后从原始数字中减去它。 / p>