这里有一个算法文档的链接,它生成了我在Visual Studio(大多数情况下)看到的值和代码,并且我假设它仍然用于GCC对变量整数除以常量整数的操作。
http://gmplib.org/~tege/divcnst-pldi94.pdf
在文章中,uword有N位,udword有2N位,n = 分子 = 被除数,d= 分母 = 除数,ℓ最初设置为ceil(log2(d)),shpre是预移位(用于乘法之前)= e = d中的尾随零位数,shpost是后移位(用于乘法之后),prec是精度 = N - e = N - shpre。目标是通过预移位、乘法和后移位来优化计算 n/d。
向下滚动到图6.2,定义了如何生成udword乘法器(最大大小为N+1位),但并没有清楚地解释该过程。下面我将解释这一点。
图4.2和图6.2显示了如何将大多数除数的乘法器缩减为小于或等于N位。方程式4.5说明了图4.1和4.2中处理N+1位乘法器的公式是如何推导出来的。
对于现代X86和其他处理器,乘法时间是固定的,因此在这些处理器上,预移位并不能帮助,但仍有助于将乘法器从N+1位缩减到N位。我不知道GCC或Visual Studio是否已经为X86目标消除了预移位。
回到图6.2。当分母(除数)> 2^(N-1)(当ℓ == N => mlow = 2^(2N))时,mlow和mhigh的分子(被除数)可能大于udword,此时n/d的优化替换是一个比较操作(如果n>=d,则q = 1,否则q = 0),因此不生成乘法器。mlow和mhigh的初始值将是N+1位,每个N+1位值可以使用两个udword/uword除法产生(mlow或mhigh)。以64位模式下的X86为例:
; upper 8 bytes of dividend = 2^(ℓ) = (upper part of 2^(N+ℓ))
; lower 8 bytes of dividend for mlow = 0
; lower 8 bytes of dividend for mhigh = 2^(N+ℓ-prec) = 2^(ℓ+shpre) = 2^(ℓ+e)
dividend dq 2 dup(?) ;16 byte dividend
divisor dq 1 dup(?) ; 8 byte divisor
; ...
mov rcx,divisor
mov rdx,0
mov rax,dividend+8 ;upper 8 bytes of dividend
div rcx ;after div, rax == 1
mov rax,dividend ;lower 8 bytes of dividend
div rcx
mov rdx,1 ;rdx:rax = N+1 bit value = 65 bit value
你可以使用GCC测试这个。你已经看到了i/5的处理方式。再来看看i/7的处理方式(应该是N+1位乘法器的情况)。
在大多数现代处理器上,乘法器都有固定的时序,因此不需要预先进行移位。对于X86,大多数除数的最终结果是一个两条指令的序列,而像7这样的除数则需要五条指令的序列(以模拟pdf文件中的等式4.5和图4.2所示的N+1位乘法器)。以下是X86-64代码示例:
; rbx = dividend, rax = 64 bit (or less) multiplier, rcx = post shift count
; two instruction sequence for most divisors:
mul rbx ;rdx = upper 64 bits of product
shr rdx,cl ;rdx = quotient
;
; five instruction sequence for divisors like 7
; to emulate 65 bit multiplier (rbx = lower 64 bits of multiplier)
mul rbx ;rdx = upper 64 bits of product
sub rbx,rdx ;rbx -= rdx
shr rbx,1 ;rbx >>= 1
add rdx,rbx ;rdx = upper 64 bits of corrected product
shr rdx,cl ;rdx = quotient
; ...
为了解释这个5条指令序列,一个简单的3条指令序列可能会溢出。 让 u64() 意味着高64位(对于商来说所有这些都是必要的)。
mul rbx ;rdx = u64(dvnd*mplr)
add rdx,rbx ;rdx = u64(dvnd*(2^64 + mplr)), could overflow
shr rdx,cl
为了处理这种情况,设置cl = post_shift-1,rax = multiplier - 2^64,rbx = dividend。其中u64()表示高64位。注意,rax = rax<<1 - rax。商为:
u64( ( rbx * (2^64 + rax) )>>(cl+1) )
u64( ( rbx * (2^64 + rax<<1 - rax) )>>(cl+1) )
u64( ( (rbx * 2^64) + (rbx * rax)<<1 - (rbx * rax) )>>(cl+1) )
u64( ( (rbx * 2^64) - (rbx * rax) + (rbx * rax)<<1 )>>(cl+1) )
u64( ( ((rbx * 2^64) - (rbx * rax))>>1) + (rbx*rax) )>>(cl ) )
mul rbx
sub rbx,rdx
shr rbx,1
add rdx,rbx
shr rdx,cl