除数为2的n次幂时
结论:
对于无符号的被除数,直接右移
对于有符号的被除数,先加上[除数-1],再进行右移
无符号(正2的次幂)
用到了两个公式:
被除数 < 0时
向上取整 = 向下取整 + 1
被除数 < 0时
向上取整 = 向下取整(被除数 + (2^n)-1)
需要使用向下取整构造出向零取整的表达式。
Clang
可以看到Clang
也是使用先加法,再右移的优化方式。
有符号(负2的次幂)
和正2的次幂一样,只是最后多了一个neg
取反。
printf("%d", n1 / -4);
非2次幂除法
核心思想是:将带有常量的除法,优化为乘法。
x/c
等价于x*c的倒数
,那么
无符号除法
无误差
n:编译器选择,一般大于32c:由编译器提前计算出的MagicNumbercx=xc1=xc∗2n1∗2n=xc2n2n1设M=c2n,最终得:cx=2nx∗M对于编译器来说:xc2n2n1⇒x∗M>>n逆向时还原c:c=M2n
计算过程
- 对于
M=2863311531
先进行一次无符号乘法(mul)。
shr edx, 1
,分两步,先取出结果中的低32位(进行右移32位);再让结果右移1位。如此总共是33次右移,n=33
。
- 完成了x∗M>>n的计算
还原除数
- 使用c=M2n公式计算
- 8589934592÷2863311531=2.9999999996507540345598531381041,认为除数是3,且使用无符号乘法。
- 最终结果为
uint32_t x = n1 / 3
有误差(除7优化)
对于无符号整数除法,编译器通过 Magic Number 和移位操作实现优化。虽然 3
和 7
都是小的非 2 的幂次的除数,但它们的 Magic Number 的误差范围不同,导致优化策略的差异。
3
的 Magic Number
- 倒数:
1/3 ≈ 0.333333...
- Magic Number:
ceil(2^32 / 3) = 1431655765
(0xAAAAAAAB
)
- 误差:
1431655765 / 2^32 - 1/3 ≈ 0
(非常接近真实值)
7
的 Magic Number
- 倒数:
1/7 ≈ 0.142857...
- Magic Number:
ceil(2^32 / 7) = 613566757
(0x24924925
)
- 误差:
613566757 / 2^32 - 1/7 ≈ 0.00000001
(误差更大)
结论: 3
的 Magic Number 几乎是精确的,因此不需要额外修正。而 7
的 Magic Number 误差较大,必须通过修正步骤来调整计算结果。
cval=xc1=x232+n232+c232+n−232编译器企图用多次运算来减少精度的丢失,我们来看看此时如何计算c设M=c232+n−232则c=232+M232+n
注意化简后的公式中包含了2^35大数,无法直接运用到代码,仍需使用第一步的公式
还原除数
M=613566757, n=3
(2^(32+n)) =34359738368
- 2^32 + M = 4,908,534,053
- 带入公式c=232+M232+n
34359738368 ÷ 4908534053 =6.99999999938881
除数有符号
除数为正数
M < 80000000H
cx=(x∗M)>>n+α当c≧0则α=1c<0则α=0逆向时还原c:c=M2n
M > 7FFFFFFF
在进行有符号除法时,如果M>7FFFFFFF
符号会被忽略。因为编译器在提前计算M的时候,采用的是无符号,也就是说M在进行imul
的时候,被强制减去了2^32
。
原本的公式为:cx=xc2n2n1,M=c2n但是有符号除法强制忽略的最高位,导致M=M−232,公式变为:cx=x(c2n−232)2n1=(x(c2n−232)+x232)2n1所以要在乘法之后先进行一次+x232的调整逆向时还原c:c=M2n
除数为负数
M < 80000000H
比较简单,反向调整,乘法之后减去一个x232。
cx=−px,p是c的补码上式变为−(x∗p2n∗2n1)=(x∗(−(p2n−232))−x∗232)可以和之前调整的差异,这里减去一个x∗232逆向时还原c:c=232−M2n
M > 7FFFFFFF
除数已经为负数了,此时M也为负数,正常情况用之前的公式再进行一次neg
就可以得出结果,但是编译器使用另一种优化方式。
cx=x∗M∗2n1当c<0时,M=−∣c∣2n=求补∣c∣2n=232−∣c∣2n逆向还原c:c=232−M2n