请考虑要计算将64位和128位无符号数相乘的结果的低128位,并且可用的最大乘法是类C的64位乘法,该乘法需要两个64位无符号输入,并返回结果的低64位。
需要多少个乘法?
当然,您可以用8个方法做到这一点:将所有输入分成32位块,并使用64位乘法执行4 * 2 = 8所需的全角32 * 32-> 64乘法,但是可以一个做得更好?
当然,该算法仅应在乘法基础上进行“合理”数量的加法或其他基本算术运算(我对将乘法重新发明为加法循环并因此主张“零”乘法的解决方案不感兴趣)。
答案 0 :(得分:9)
四个,但开始变得有些棘手。
让 a 和 b 是要相乘的数字,分别是 a 0 和 a 1 分别是 a 的低32位,而 b 0 ,< em> b 1 , b 2 , b 3 b 的32位组,分别从低到高。
所需的结果是( a 0 + a 1 •2 32的余数)•( b 0 + b 1 •2 32 + b 2 •2 64 + b 3 •2 96 )模2 128 。
我们可以将其重写为( a 0 + a 1 •2 32 )•( b 0 + b 1 •2 32 )+ ( a 0 + a 1 •2 32 )•( b 2 •2 64 + b 3 •2 96 )模2 128 。
后一项取模2 128 的余数可以计算为单个64位乘以64位乘法(其结果隐式乘以2 64 )
然后可以使用a乘以三个乘法来计算前一项 精心实施的Karatsuba步骤。简单版本将涉及到不可用的33位乘33位到66位产品,但是有一个更棘手的版本可以避免这种情况:
z0 = a0 * b0
z2 = a1 * b1
z1 = abs(a0 - a1) * abs(b0 - b1) * sgn(a0 - a1) * sgn(b1 - b0) + z0 + z2
最后一行仅包含一个乘法;其他两个伪乘法只是条件否定。绝对差和条件取反在纯C中实现很烦人,但这是可以做到的。
答案 1 :(得分:4)
当然,如果没有唐津5次乘法。
Karatsuba很棒,但是现在,64 x 64乘法可以在3个时钟内结束,并且可以在每个时钟安排一个新的。因此,处理符号的开销和不包含符号的开销可能比保存一个乘法的开销大得多。
对于简单的64 x 64乘法需求:
r0 = a0*b0
r1 = a0*b1
r2 = a1*b0
r3 = a1*b1
where need to add r0 = r0 + (r1 << 32) + (r2 << 32)
and add r3 = r3 + (r1 >> 32) + (r2 >> 32) + carry
where the carry is the carry from the additions to r0, and result is r3:r0.
typedef struct { uint64_t w0, w1 ; } uint64x2_t ;
uint64x2_t
mulu64x2(uint64_t x, uint64_t m)
{
uint64x2_t r ;
uint64_t r1, r2, rx, ry ;
uint32_t x1, x0 ;
uint32_t m1, m0 ;
x1 = (uint32_t)(x >> 32) ;
x0 = (uint32_t)x ;
m1 = (uint32_t)(m >> 32) ;
m0 = (uint32_t)m ;
r1 = (uint64_t)x1 * m0 ;
r2 = (uint64_t)x0 * m1 ;
r.w0 = (uint64_t)x0 * m0 ;
r.w1 = (uint64_t)x1 * m1 ;
rx = (uint32_t)r1 ;
rx = rx + (uint32_t)r2 ; // add the ls halves, collecting carry
ry = r.w0 >> 32 ; // pick up ms of r0
r.w0 += (rx << 32) ; // complete r0
rx += ry ; // complete addition, rx >> 32 == carry !
r.w1 += (r1 >> 32) + (r2 >> 32) + (rx >> 32) ;
return r ;
}
对于唐津,建议:
z1 = abs(a0 - a1) * abs(b0 - b1) * sgn(a0 - a1) * sgn(b1 - b0) + z0 + z2
比看起来要棘手...刚开始时,如果z1
是64位,则需要以某种方式收集该加法会产生的进位...并且由于签名问题而变得复杂
z0 = a0*b0
z1 = ax*bx -- ax = (a1 - a0), bx = (b0 - b1)
z2 = a1*b1
where need to add r0 = z0 + (z1 << 32) + (z0 << 32) + (z2 << 32)
and add r1 = z2 + (z1 >> 32) + (z0 >> 32) + (z2 >> 32) + carry
where the carry is the carry from the additions to create r0, and result is r1:r0.
where must take into account the signed-ness of ax, bx and z1.
uint64x2_t
mulu64x2_karatsuba(uint64_t a, uint64_t b)
{
uint64_t a0, a1, b0, b1 ;
uint64_t ax, bx, zx, zy ;
uint as, bs, xs ;
uint64_t z0, z2 ;
uint64x2_t r ;
a0 = (uint32_t)a ; a1 = a >> 32 ;
b0 = (uint32_t)b ; b1 = b >> 32 ;
z0 = a0 * b0 ;
z2 = a1 * b1 ;
ax = (uint64_t)(a1 - a0) ;
bx = (uint64_t)(b0 - b1) ;
as = (uint)(ax > a1) ; // sign of magic middle, a
bs = (uint)(bx > b0) ; // sign of magic middle, b
xs = (uint)(as ^ bs) ; // sign of magic middle, x = a * b
ax = (uint64_t)((ax ^ -(uint64_t)as) + as) ; // abs magic middle a
bx = (uint64_t)((bx ^ -(uint64_t)bs) + bs) ; // abs magic middle b
zx = (uint64_t)(((ax * bx) ^ -(uint64_t)xs) + xs) ;
xs = xs & (uint)(zx != 0) ; // discard sign if z1 == 0 !
zy = (uint32_t)zx ; // start ls half of z1
zy = zy + (uint32_t)z0 + (uint32_t)z2 ;
r.w0 = z0 + (zy << 32) ; // complete ls word of result.
zy = zy + (z0 >> 32) ; // complete carry
zx = (zx >> 32) - ((uint64_t)xs << 32) ; // start ms half of z1
r.w1 = z2 + zx + (z0 >> 32) + (z2 >> 32) + (zy >> 32) ;
return r ;
}
我做了一些非常简单的计时(使用times()
,在Ryzen 7 1800X上运行):
...,是的,您可以使用唐津巴(Karatsuba)保存一个乘法,但是是否值得这样做取决于