在Hacker的喜悦中,有一种计算the double word product of two (signed) words的算法。
函数muldws1
使用四次乘法和五次加法来计算
两个词中的双字。
在该代码的末尾有一行注释掉
/* w[1] = u*v; // Alternative. */
该替代方案使用五次乘法和四次加法,即它为乘法交换加法。
但我认为这种替代方法可以改进。我还没有说过硬件。假设一个假设的CPU,它可以计算两个字但不是高位字的乘积的低位字(例如,对于32位字32x32到低32)。在这种情况下,在我看来,这个算法可以改进。这就是我想出的 假设32位字(相同的概念适用于64位字)。
void muldws1_improved(int w[], int32_t x, int32_t y) {
uint16_t xl = x; int16_t xh = x >> 16;
uint16_t yl = y; int16_t yh = y >> 16;
uint32 lo = x*y;
int32_t t = xl*yh + xh*yl;
uint16_t tl = t; int16_t th = t >>16;
uint16_t loh = lo >> 16;
int32_t cy = loh<tl; //carry
int32_t hi = xh*yh + th + cy;
w[0] = hi; w[1] = lo;
}
这使用了四次乘法,三次加法和一次比较。这是我所希望的一个小改进。
这可以改善吗?有没有更好的方法来确定进位标志?我应该指出我还假设硬件没有进位标志(例如没有ADDC指令)但可以比较单词(例如word1<word
)。< / p>
编辑:正如Sander De Dycker指出我的功能未通过单元测试。这是一个通过单元测试的版本,但效率较低。我认为可以改进。
void muldws1_improved_v2(int w[], int32_t x, int32_t y) {
uint16_t xl = x; int16_t xh = x >> 16;
uint16_t yl = y; int16_t yh = y >> 16;
uint32_t lo = x*y;
int32_t t2 = xl*yh;
int32_t t3 = xh*yl;
int32_t t4 = xh*yh;
uint16_t t2l = t2; int16_t t2h = t2 >>16;
uint16_t t3l = t3; int16_t t3h = t3 >>16;
uint16_t loh = lo >> 16;
uint16_t t = t2l + t3l;
int32_t carry = (t<t2l) + (loh<t);
int32_t hi = t4 + t2h + t3h + carry;
w[0] = hi; w[1] = lo;
}
这使用了四次乘法,五次加法和两次比较,这比原始函数更糟糕。
答案 0 :(得分:1)
我的muldws1_improved
函数存在两个问题。其中一个是当我做xl*yh + xh*yl
时错过了一个进位。这就是单元测试失败的原因。 但另一个是签名*未签名的产品需要比C代码中更多的机器逻辑。(参见下面的编辑)。 I found a better solution首先优化未签名的产品函数muldwu1,然后执行
muldwu1(w,x,y);
w[0] -= ((x<0) ? y : 0) + ((y<0) ? x : 0);
纠正标志。
这是我尝试使用低级字muldwu1
来改进lo = x*y
(是的,这个函数通过了黑客高兴的单元测试)。
void muldwu1_improved(uint32_t w[], uint32_t x, uint32_t y) {
uint16_t xl = x; uint16_t xh = x >> 16;
uint16_t yl = y; uint16_t yh = y >> 16;
uint32_t lo = x*y; //32x32 to 32
uint32_t t1 = xl*yh; //16x16 to 32
uint32_t t2 = xh*yl; //16x16 to 32
uint32_t t3 = xh*yh; //16x16 to 32
uint32_t t = t1 + t2;
uint32_t tl = 0xFFFF & t;
uint32_t th = t >> 16;
uint32_t loh = lo >> 16;
uint32_t cy = ((t<t1) << 16) + (loh<tl); //carry
w[1] = lo;
w[0] = t3 + th + cy;
}
这比使用Hacker的喜悦中的原始功能少了一个,但它必须进行两次比较
1 mul32x32 to 32
3 mul16x16 to 32
4 add32
5 shift logical (or shuffles)
1 and
2 compare32
***********
16 operations
编辑:
我对Hacker's Delight(第2版)中的一个声明感到困扰,该声明中描述了mulhs和mulhu算法。
该算法在有符号或无符号版本中需要16条基本RISC指令,其中四条是乘法。
我在only 16 SSE instructions中实现了无符号算法,但我的签名版本需要更多指令。我想出了为什么,我现在可以回答我自己的问题了。
我无法在Hacker's Delight中找到更好的版本的原因是他们的假设RISC处理器有一个指令来计算两个单词的乘积的低位字。 换句话说,他们的算法已针对此案例进行了优化,因此不太可能存在比他们已有的更好的版本。
他们列出替代方案的原因是因为他们假设乘法(和除法)可能比其他指令更昂贵,因此他们将备选方案作为优化的案例。
因此C代码不会隐藏重要的机器逻辑。它假定机器可以用单词*单词来降低单词。
为什么这很重要?在他们的算法中,他们先做
u0 = u >> 16;
以后
t = u0*v1 + k;
如果u = 0x80000000
u0 = 0xffff8000
。但是,如果您的CPU只能使用半字产品来获得完整的单词,则忽略u0
的上半部分字,并且您得到错误的签名结果。
在这种情况下,您应该计算无符号的高位字,然后使用hi -= ((x<0) ? y : 0) + ((y<0) ? x : 0);
进行更正,正如我已经说过的那样。
我对此感兴趣的原因是Intel的SIMD指令(SSE2到AVX2)没有64x64到64的指令,它们只有32x32到64.这就是我的签名版本需要更多指令的原因。
但AVX512有64x64到64的指令。因此,对于AVX512,签名版本应采用与unsigned相同数量的指令。但是,由于64x64到64指令可能比32x32到64指令慢得多,因此无论如何都可以更有意义地执行无符号版本然后更正。