说明:除法相对其他几种运算的优化又要更难了,主要是其包含的数学知识(离散数学?)
由于我本人比较笨,也不喜欢深究数学,所以尽量跳过数学,以更直观的方式来解释(其实也就是我自己的理解方法了)
取整分为向上取整和向下取整以及向0取整,我们在程序语言中的取整都是向0取整,但是有一些运算却是向下取整的,比如右移运算就是向下取整的,所以在负数的时候,为了保证向0取整,就会做一些调整。
说明:分为被除数大于0和小于0两种情况讨论。
对于被除数小于0的情况,算术右移依然向下取整,对于整数情况没有影响,但是对于非整数,向下取整将会违反向0取整的最终目的,所以需要调整。具体如图
在这样的讨论下,对于大于0,我们直接右移,对于小于0,则为 (x + 2^n - 1)再右移n位。
示例:
mov eax, VAL
cdq ;符号扩展,eax符号位为1,设edx为-1,否则设edx为0
sub eax, edx
sar eax, 1
这段代码是x / 2的代码,使用cdq避免了分支。
当x >= 0, edx为0,sub eax, edx没有作用,直接右移1位
当x < 0,edx为-1, sub eax, edx使得eax = eax + 1(在这里由于2^1-1 == 1所以没有后续操作),然后移位
另外一个示例:
mov eax, VAL
cdq
and edx, 7
add eax, edx
sar eax, 3
这段代码是x/8的代码。
当x >= 0时,cdq使得edx为0,and和add操作没有作用,sar移位直接完成操作。
当x < 0时,cdq使得edx为-1,即全F,全F和7按位和的结果是保留7的结果,于是add完成了eax = eax + 2^3 - 1即eax = eax + 7的操作,最后移位完成运算。 由于8是常量,所以这里的2^n - 1是可以提前算出来的。
;ecx 为某通用寄存器(可替换)
mov ecx, VAL
mov eax, 38E38E39h
imul ecx ;有符号乘法,用eax乘ecx,edx存放高4字节,eax存放低4字节
sar edx, 1 ;有符号移位
mov eax, edx
shr eax, 1Fh ;无符号移位
add edx, eax
这段代码是x/9的代码。令magic number m = 38E38E39h
对于x>=0时,eax = eax*m,相当于完成了x * (2^n/y)的操作,我们现在暂时还不知道n具体是多少。imul之后使用的是edx,edx为高4字节,之后eax弃用,说明用edx记录结果,相当于使用了右移了32位的值,加之sar的右移1位,所以移位了33位。然后将eax用来记录该值右移33位之后再右移1F(31)位。之后的右移31位即获取到了sar之后edx的符号位,用eax存,大于0,为0,所以add不做操作。由此,移动了共33位,即n=33,m = 2^n / y = 2^33 / y = 38E38E39h,可得y约等于9。
对于x<0时,eax将会得到符号位,小于0,eax为符号位,也就是1,所以add使得结果+1。
更难一点的示例2:
;ecx为某通用寄存器(可替换)
mov eax, 24924925h
mul ecx
sub ecx, edx
shr ecx, 1
add ecx, edx
shr ecx, 2
这段代码是x/7的代码。和上一例的区别在于,magic number的计算得出的结果超出了一个四字节整数的范围,所以对其做出了调整,导致之后的步骤也做出了调整,但总体思路一致。
另外还要注意,由于是mul,这里的计算是无符号计算,可以认为都是正数,没有负数处理的部分。
首先m = 24924925h,剩下的部分就需要用笔算了,因为涉及到化简,我在这里不再赘述,基本上就是把等效的表达式写出来然后化简,最后结果为ecx = ecx0 * (2^32+m) /2^35,ecx0为最初的ecx(除数)。根据之前的原理,如图有:
所以m其实是(2^32 * 2^n/y) n为3,就符合之前得到的运算结果了。
根据n和m即可算出来y = 7了。
示例3:
;ecx 为某通用寄存器(可替换为其他)
mov eax, 92492493h(大于0x7fffffff)
imul ecx
add edx, ecx
sar edx, 2
mov eax, edx
shr eax, 1Fh
add edx, eax
这里也是x/7的代码。但是是有符号运算。
一个细节:对于有符号运算,大于0x8000 0000的数值,会被认为是负数,其值为 VAL-2^32,VAL为对应无符号数值。因此对公式做出相应变形:
由此分析代码
首先我们应该通过示例1,可以识别
mov eax, edx
shr eax, 1Fh
add edx, eax
是在针对负数做调整,所以关注点为前面几行。
根据公式的变形,magic number现在是(2^n/y-2^32),加之对代码的公式进行转换,可以得到
edx = (ecx*eax+2^32*ecx)/2^34 符合我们的形式,所以即可得到最终为x/7.
对于除数大于0的情况,首先我们得认识到一个取整的公式,就是将被除数为负的情况的向下取整变成向上取整,然后其余的情况就简单了。 分析的方法基本上就是把汇编代码对应的表达式写出来并化简,然后通过对公式的恒等变形,尽力化成表达式的形式,对于负数,一般是在汇编代码中设法获得符号位然后做一个加法。