编译器除法优化

Posted 不会写代码的丝丽

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了编译器除法优化相关的知识,希望对你有一定的参考价值。

我们知道除法在现代CPU计算中耗费更多的时钟周期:
如下图的Adddiv做的对比


Div延迟60-80之间 而add只有 1,可见我们在真实的情况应当避免调用除法汇编指令。

解决办法:

对于除以2的倍数优化

对于要进行除法运行的且除数是2的倍数,我们可以通过位移来完成
如下图:

int x = 58;
// x = x%8
x >>= 3;

但是对于负数来说位移是可能发生错误现象的如下:

对于负数我们可以利用如下公式进行转化

举个例子:
a = -58
b = 8

我们本例中结合计算机特性符合如下公式(比如结果是-7.25我们应该向上取而不是向下):

ceil ((a+b-1)/b) =ceil ((-58+8-1)/8) =ceil(-6.375)=-7

于是我们的除法优化可以如此这般消除除法

	int x = -58;
	//如果被除数小于0 利用公式消除除法
	if (x < 0)
	
		//x = (x + 8 - 1) / 8; => (x + 7) >> 3;
		//计算机默认向下取整
		x = (x + 7) >> 3;
	
	else 
		x >>= 3;
	

我们看看如下代码:

#include<stdio.h>

int main(int argc,char* args[])

	
	printf("%d\\r\\n",argc/8);

	return 0;


编译后会变为如下汇编指令


mov eax,dword ptr[argc] 移动到寄存器
cdq 把eax扩展为64位高位移动到edx上.此处是为了处理正负数的情况
and edx ,7 如果为正数 edx比为0,如果为负数edx全为1。因此正数执行后 edx 为0,负数为7。
add eax,edx 想当与我们上面的(x + 7),不过后面的7可能为0
sar eax,3 eax算数右移动3位保证

除以非2倍数优化

我们可以利用以下公式

反推公式如下:

tip: M 不会被整除 因为2^n 是偶数 而c是非2的倍数,所以结果默认向下取整,这个结论在负数情况需要注意

其中 M是常量所以可以被编译器优化为编译常量,其中n至少为32n越大结果越精确)

举例如下:

无符号除法

int main(unsigned int argc, char* args[])
	
	//注意无符号除法结果为无符号、、argc是无符号数
	printf("y  ===>>> %d\\r\\n", argc / 3);
	return 0;

对应的汇编语句:

其中0AAAAAAABh就是的我们公式中M

; eax = M
mov eax,0AAAAAAABh
; 其中edx是存储高32位 eax是低32位 ret标记为我们的计算结果。argc为a  于是乎得到
; (edx,eax) = ret = eax * argc   = aM  
mul eax,dword ptr [argc]
;edx由于是高32,移动一位相当于ret移动33位,也就是 ret>>33  等价  edx=edx/(2^33)
shr edx,1 

我们借用公式反推得到除数

可以看到上面整个算式中没有一个除法指令。
例外强悍的IDA pro可以帮我们快速识别

有符号除法

#include<stdio.h>
int main( int argc, char* args[])

	//注意argc是有符号的
	printf("y  ===>>> %d\\r\\n", argc / 3);
	return 0;


相较于无符号的除法多出以下汇编


0040104B  mov         eax,edx  
;1F转为10进制31
0040104D  shr         eax,1Fh  
00401050  add         eax,edx 

之所以多出几行行汇编是用于规避负数除法默认向下取整问题,所以为负数的时候需要加1.(参考上文)

;将a*m乘积的高位给eax
0040104B  mov         eax,edx  
;1F转为10进制31
;右移动31后只剩下符号位也就是1或者0
;负数为1 正数为0
0040104D  shr         eax,1Fh  
;如果是负数eax为1 那么进行加一补位
00401050  add         eax,edx 

M大于32位数的变形

#include<stdio.h>

int main(unsigned int argc, char* args[])

	//注意argc是有符号的
	printf("y  ===>>> %d\\r\\n", argc / 7);	
	return 0;


上面的汇编转化为数学公式如下:

其中M24924925h,而我们原来真正公式M2^32+M 很明显这数32寄存器不足以存放,以及除以2^35也是同理

乘法优化

假设我们只能使用16位乘法器进行运算以下算式:

求 A*8086h 的结果(8086h存储在ax中,A存储在cx)

其中8086h是无符号数,A是用户输入的word大小数值是一个有符号数。

因此如果直接用 mul cx; 的话 会把cx作为无符号数,但是有可能用户输入的数为负数
imul cx; 会让ax视为负数,但实际ax为无符号数。

我们首先看下如下推到公式:

;x是负数的word大小
x + ~x = ffffh
x+ ~x + 1 =10000h
//这个就是~x+1就是x的补码
~x+1=10000h-x
x = 10000h - (~x+1)

;于是乎
;(~x+1)就是8086h 二进制转化为10进制为-32634   。(10000h-8086h) 等于8086在二进制中符号的正数表示    10000h-8086h= 32634
;而前面的负号 - (10000h-8086h) 用于转化为正确数字 (因为8086h在16位imul是一个负数)
dx.ax                =  A *  - (10000h-8086h)
dx.ax 		        =   A * (8086h-10000h)
dx.ax 		        =  8086Ah-10000Ah
dx.ax+10000A  = 8086h*A
(dx+A).ax = 8086h*A

通过上面的公式我们可以得出以下汇编代码计算本题

mov ax,8086h
mov cx,A
imul cx;
add dx,cx

如果你传入的A是负数那么只需要把 add dx,cx转化为 sub dx,cx

假设我们想计算 *-8086h A 的数值 我们这里这里只能用16位寄存器计算,但是-8086h 已经超出16精度

因为
-x =  (~x+1)-10000h

所以:

dx.ax                  =  A *  -(10000h-8086h)
dx.ax 		          =  8086Ah-10000h
A0000h+dx.ax   = 8086h*A
;两边乘上负一
(dx-A).ax          = -8086h*A


mov ax,8086h
mov cx,A
imul cx;
sub dx,cx

除法的负数优化

int main( int argc, char* args[])
	printf("y  ===>>> %d\\r\\n", argc / -5);
	return 0;


我们首先回顾上面的除法优化公式

当c 为负数时:

所以本例中的99999999h 是一个负数,且当前的二进制是一个补码编码.

对应的十进制数为 -1717986919,所以当我们需要反推除数的时候使用10进制数进行计算即可

c = 2^33 / -1717986919=-4.999999998 最终得到 -5

除法的负数优化2

 int main( int argc, char* args[])


	printf("y  ===>>> %d\\r\\n", argc / -7);

	return 0;


这里编译器其实想表示的M并不是6DB6DB6D同时数实际表示一个负数,6DB6DB6D大于4字节有符号存储范围 所以才有以上的汇编代码

设6DB6DB6D为Y,A为被除数

因为
-x =  (~x+1)-100000000h

所以:
;因为M小于0所以负号符号
dx.ax    =  A *  (100000000h-Y)
所以 M = - (100000000h-Y)
M =- (100000000h-6DB6DB6DH) =-92492493h

当我求出M后便可以换算出除数 c= 2^34 / - 92492493h =- 7

以上是关于编译器除法优化的主要内容,如果未能解决你的问题,请参考以下文章

编译器除法优化

逆向-除法优化下

逆向课程第五讲逆向中的优化方式,除法原理,以及除法优化下

编译器求余优化

编译器求余优化

PC逆向之代码还原技术,第六讲汇编中除法代码还原以及原理第二讲,被除数是正数 除数非2的幂