一、除常数优化的定义
对于任何处理器,进行除法计算所需要的时间都远远高于乘法和移位运算(当然这两者也不在一个量级上),所以我们有一种想法,就是把除法转换成乘法和移位操作,也就是
q u o t i e n t = d i v i d e n d d i v i s o r = ( d i v i d e n d × m u l t i p l i e r ) > > s h i f t \tt{quotient = \frac{dividend}{divisor} = (dividend \times multiplier) >> shift}
quotient = divisor dividend = ( dividend × multiplier ) >> shift
这个式子是一个很容易看懂的式子,不幸的是,这个式子是没有办法直接应用到我们的处理器上的。
如果想要应用到我们的处理器上,需要做出很多的妥协和让步,本文其实是一篇论文的注释,对于具体内容,大家可以参考论文:Division by Invariant Integers using Multiplication 。
二、第 1 次溢出
在机器上计算与数学计算最大的一个区别就是机器上的位宽是有限的,所以会存在溢出问题,我们可以审视一下在这个问题里哪里会有溢出问题。
一个 32 位的除法是不会溢出的,商一定是一个 32 位以内的数,加减法是会溢出的,但是不是我们考虑的重点。乘法是会溢出的,也就是说 32 位的两个数相乘,积可能大于 32 位,所以我们才会在乘法计算中设置 Hi 和 Lo 两个寄存器,分别保存积的高 32 位结果和低 32 位结果。那么让我们审视变化中有一个结构
( d i v i d e n d × m u l t i p l i e r ) \tt{(dividend \times multiplier)}
( dividend × multiplier )
这个东西有可能是一个大于 32 位的结果,所以我们必须选择挑选到底是 hi 还是 lo 作为上面这个的结果。那么结果十分显然,一定要挑 hi,这是因为 lo 有可能不对,而 hi 就没有这种顾虑。
如果我们使用 Hi,那么就要保证上面的积一定是一个极大的数,一定要到 32 位以上,不然就没有意义了,如果我们不能保证 dividend 的大小,但是我们可以保证 multiplier 在一个很大的范围,这样就可以让两者的乘积超过 32 位。
在采用了上面的思想后,我们的目标变成了
⌊ d i v i d e n d d i v i s o r ⌋ = ⌊ d i v i d e n d × m u l t i p l i e r 2 N + s h i f t ⌋ \tt{\lfloor \frac{dividend}{divisor} \rfloor = \lfloor \frac{dividend \times multiplier}{2 ^ {N + shift}} \rfloor}
⌊ divisor dividend ⌋ = ⌊ 2 N + shift dividend × multiplier ⌋
其中 N = 32 N = 32 N = 32 是机器字的字长。这个式子基本上就是真正的原理式了。
而反映到处理器上,就是以下的指令(指令用大写字母表示)
S R L ( M U L U H ( d i v i d e n d , m u l t i p l i e r ) , s h i f t ) \tt{SRL(MULUH(dividend, multiplier), shift)}
SRL ( MULUH ( dividend , multiplier ) , shift )
三、第 2 次溢出
不幸的是,因为我们希望第 1 次溢出,所以我们会把 multiplier 调整的很大(具体的调整方法后面介绍),有的时候就超过了 32 位字长可以表示的程度,那么就会导致没办法用 32 位的寄存器存储,发生了第二次溢出。
那么为了让过大的 multiplier 可以被正确的计算,我们需要利用乘法分配律进行计算,有
d i v i d e n d × m u l t i p l i e r = d i v i d e n d × ( 2 N + m u l t i p l i e r − 2 N ) = ( d i v i d e n d < < N ) + d i v i d e n d × ( m u l t i p l i e r − 2 N ) \tt{dividend \times multiplier = dividend \times (2^{N} + multiplier - 2^{N}) = (dividend << N) + dividend \times (multiplier - 2^{N})}
dividend × multiplier = dividend × ( 2 N + multiplier − 2 N ) = ( dividend << N ) + dividend × ( multiplier − 2 N )
反应到处理器上,就是以下的指令
S R L ( A D D ( d i v i d e n d , M U L U H ( d i v i d e n d − 2 N , m u l t i p l i e r ) ) , s h i f t ) \tt{SRL(ADD(dividend, MULUH(dividend - 2^{N}, multiplier)), shift)}
SRL ( ADD ( dividend , MULUH ( dividend − 2 N , multiplier )) , shift )
当计算出 multiplier 之后,我们需要对其大小进行一个判断,然后选择用哪种式子。
四、第 3 次溢出
当我们发生了第 2 次溢出后,我们采用了如下措施
( d i v i d e n d < < N ) + d i v i d e n d × ( m u l t i p l i e r − 2 N ) \tt{(dividend << N) + dividend \times (multiplier - 2^{N})}
( dividend << N ) + dividend × ( multiplier − 2 N )
但是我们没有意识到加法同样是可能溢出的,所以我们还需要进一步修正,但是我懒得写咋修正了(依然是利用乘法分配律),因为在最终代码中没有体现(这是应为现在讨论的是无符号数,而有符号数的范围会比较小,所以没有这个问题)。
五、向零取整
对于有符号数来说,C 语言的特性是这样的,除法是向零取整的,也就是说 3 / 4 = 0, -3 / 4 = 0。但是对于位移来说,是向下取整的,也就是说 3 >> 2 = 0, -3 >> 2 = -1。因为我们用左移代替了除法,所以我们需要对于这里进行一定的修正。一共有两种修正,一种是对于除数是 2 的幂次的情况,那么我们修改被除数:
n e w D i v i d e n d = o l d D i v i d e n d + d i v i s o r − 1 \tt{newDividend = oldDividend + divisor - 1}
newDividend = oldDividend + divisor − 1
另一种就是除数不为 2 的幂次,对于有符号除法,有如下公式(证明就不列了)
其中 T R U N C \tt{TRUNC} TRUNC 是向零取整的意思。
六、选择 Multiplier 和 Shift
这就要涉及一些神秘的数学证明了,在证明开始之前,我觉得先说一下咱们需要干啥,现在我们需要根据 divisor 来确定 multiplier 和 shift ,但是显然一个数没法确定两个数,所以我们要挑出 multiplier 和 shift。但是也不能瞎挑,我们要保证我们挑出的东西是可以满足底下这个效果的
⌊ d i v i d e n d d i v i s o r ⌋ = ⌊ d i v i d e n d × m u l t i p l i e r 2 N + s h i f t ⌋ \tt{\lfloor \frac{dividend}{divisor} \rfloor = \lfloor \frac{dividend \times multiplier}{2 ^ {N + shift}} \rfloor}
⌊ divisor dividend ⌋ = ⌊ 2 N + shift dividend × multiplier ⌋
待会要证明的东西就是一个 “挑 “ 的指导方针,大致是这个
证明就不写了,因为懒,而且背不过。
七、精度
待会的源码里还会有精度 precision 的说法,但是我对于这个东西的理解很肤浅,所以就不说了,论文中说的也不多,而且在有符号数里没有太多的用到,大致是这样的
八、源码模拟
我用 CPP 写了一份有符号数除法的模拟源码,放在这里以供参考
#ifndef DIV_DIV_H #define DIV_DIV_H typedef unsigned int uword;typedef int sword;typedef unsigned long long udword;typedef long long sdword;const int N = 32 ;struct Multiplier { udword multiplier; int shift; int log; }; int xsign (int a) ;sword shiftRightLogical (sword a, int shift) ;sword signMulHi (sword a, uword b) ;sword clz (uword a) ;Multiplier chooseMultiplier (uword divisor, int precision) ;sword divide (sword dividend, sword divisor) ;#endif #include <iostream> #include "div.h" using namespace std;sword divide (sword dividend, sword divisor) { int quotient; bool isDivisorNegative = divisor < 0 ; uword divisorAbs = isDivisorNegative ? -divisor : divisor; Multiplier ret = chooseMultiplier (divisorAbs, N - 1 ); udword multiplier = ret.multiplier; int shift = ret.shift; int log = ret.log; if (divisorAbs == 1 ) { quotient = dividend; } else if (divisorAbs == (1 << log)) { quotient = (dividend + shiftRightLogical (dividend >> (log - 1 ), N - log)) >> log; } else if (multiplier < 0x80000000 ) { quotient = (signMulHi (dividend, multiplier) >> shift) - xsign (dividend); } else { quotient = ((dividend + signMulHi (dividend, (sword) (multiplier - 0x80000000 ))) >> shift) - xsign (dividend); } return isDivisorNegative? -quotient : quotient; } sword shiftRightLogical (sword a, int shift) { uword a1 = a; return (sword) (a1 >> shift); } sword xsign (sword a) { return -((a >> 31 ) & 1 ); } sword signMulHi (sword a, uword b) { sdword a1 = a; sdword b1 = b; return (sword) ((a1 * b1) >> 32 ); } sword clz (uword a) { return __builtin_clz(a); } Multiplier chooseMultiplier (uword divisor, sword precision) { int log = N - clz (divisor - 1 ); int shift = log; udword low = (udword (1 ) << (N + shift)) / divisor; udword high = ((udword (1 ) << (N + shift)) + (udword (1 ) << (shift + N - precision))) / divisor; while ((low >> 1 ) < (high >> 1 ) && shift > 0 ) { low >>= 1 ; high >>= 1 ; shift--; } return {high, shift, log}; } #include <iostream> #include <random> #include "div.h" using namespace std;int main () { random_device rd; mt19937 mt (rd()) ; uniform_int_distribution<int > randint (-2147483648 , 2147483647 ) ; int dividend, divisor, testTimes = 10000 ; for (int i = 0 ; i < testTimes; i++) { dividend = randint (mt); divisor = randint (mt); int standardAns = dividend / divisor; int ans = divide (dividend, divisor); if (standardAns != ans) { cout << "dividend is " << dividend << endl; cout << "divisor is " << divisor << endl; cout << "standard answer is " << standardAns << endl; cout << "answer is " << ans << endl; return 0 ; } else { cout << "dividend is " << dividend << endl; cout << "divisor is " << divisor << endl; cout << "AC" << endl << endl; } } return 0 ; }
参考文献