编译器除法优化
Posted 不会写代码的丝丽
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了编译器除法优化相关的知识,希望对你有一定的参考价值。
我们知道除法在现代CPU计算中耗费更多的时钟周期:
如下图的Add和div做的对比
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至少为32(n越大结果越精确)
举例如下:
无符号除法
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;
上面的汇编转化为数学公式如下:
其中M为24924925h,而我们原来真正公式M为 2^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
以上是关于编译器除法优化的主要内容,如果未能解决你的问题,请参考以下文章