仅使用恒定移位来模拟可变位移?

我试图找到一种方法来执行间接左移/右移操作而不实际使用变量移位操作或任何分支。

我正在研究的特定PowerPC处理器有一个怪癖,即按常数立即移位,就像

int ShiftByConstant( int x ) { return x << 3 ; } 

是快速的,单操作的,超标量的,而变量的变换,如

 int ShiftByVar( int x, int y ) { return x << y ; } 

是一个微编码操作,需要7-11个周期才能执行,而管道的其余部分都停止运行 。

我想做的是找出哪些非微码整数PPC操作sraw解码然后单独发出它们。 这对于sraw本身的延迟没有帮助 – 它将用6替换一个op – 但是在这六个操作之间我可以将一些工作双重调度到其他执行单元并获得净增益。

我似乎无法找到μopssraw解码到的任何地方 – 有没有人知道如何用一系列常量移位和基本整数运算替换变量位移? (for循环或开关或其中带有分支的任何东西都不起作用,因为分支惩罚甚至比微码惩罚更大。)

这不需要在assembly中回答; 我希望学习算法而不是特定的代码,所以用C语言或高级语言甚至伪代码的答案都会非常有用。

编辑:我应该补充一些说明:

  1. 我甚至不担心可移植性
  2. PPC具有条件移动,因此我们可以假设存在无分支内部函数

    int isel(a,b,c){return a> = 0? b:c; }

    (如果你写出一个做同样事情的三元组,我会明白你的意思)

  3. 整数乘法也是微编码的,甚至比sraw慢。 🙁

干得好…

我决定尝试这些,因为Mike Acton声称它比在他的CellPerformance网站上使用CELL / PS3微码变换更快, 他建议避免间接转换 。 但是,在我的所有测试中,使用微编码版本不仅比间接移位的完全通用无分支替换更快,而且代码(1指令)占用的内存更少。

我作为模板执行这些操作的唯一原因是为签名(通常是算术)和无符号(逻辑)移位获得正确的输出。

 template  FORCEINLINE T VariableShiftLeft(T nVal, int nShift) { // 31-bit shift capability (Rolls over at 32-bits) const int bMask1=-(1&nShift); const int bMask2=-(1&(nShift>>1)); const int bMask3=-(1&(nShift>>2)); const int bMask4=-(1&(nShift>>3)); const int bMask5=-(1&(nShift>>4)); nVal=(nVal&bMask1) + nVal; //nVal=((nVal<<1)&bMask1) | (nVal&(~bMask1)); nVal=((nVal<<(1<<1))&bMask2) | (nVal&(~bMask2)); nVal=((nVal<<(1<<2))&bMask3) | (nVal&(~bMask3)); nVal=((nVal<<(1<<3))&bMask4) | (nVal&(~bMask4)); nVal=((nVal<<(1<<4))&bMask5) | (nVal&(~bMask5)); return(nVal); } template  FORCEINLINE T VariableShiftRight(T nVal, int nShift) { // 31-bit shift capability (Rolls over at 32-bits) const int bMask1=-(1&nShift); const int bMask2=-(1&(nShift>>1)); const int bMask3=-(1&(nShift>>2)); const int bMask4=-(1&(nShift>>3)); const int bMask5=-(1&(nShift>>4)); nVal=((nVal>>1)&bMask1) | (nVal&(~bMask1)); nVal=((nVal>>(1<<1))&bMask2) | (nVal&(~bMask2)); nVal=((nVal>>(1<<2))&bMask3) | (nVal&(~bMask3)); nVal=((nVal>>(1<<3))&bMask4) | (nVal&(~bMask4)); nVal=((nVal>>(1<<4))&bMask5) | (nVal&(~bMask5)); return(nVal); } 

编辑:关于isel()的注意事项我在您的网站上看到了您的isel()代码 。

 // if a >= 0, return x, else y int isel( int a, int x, int y ) { int mask = a >> 31; // arithmetic shift right, splat out the sign bit // mask is 0xFFFFFFFF if (a < 0) and 0x00 otherwise. return x + ((y - x) & mask); }; 

FWIW,如果你重写你的isel()做一个掩码和掩码补充,它将在你的PowerPC目标上更快,因为编译器足够聪明,可以生成'andc'操作码。 它的操作码数量相同,但操作码中的结果与输入寄存器相关性较少。 两个掩码操作也可以在超标量处理器上并行发布。 如果所有内容都正确排列,它可以快2-3个周期。 您只需要为PowerPC版本更改返回值:

 return (x & (~mask)) + (y & mask); 

这个怎么样:

 if (y & 16) x <<= 16; if (y & 8) x <<= 8; if (y & 4) x <<= 4; if (y & 2) x <<= 2; if (y & 1) x <<= 1; 

可能需要更长时间才能执行,但如果您有其他代码,则更容易交错。

假设你的最大class次为31.所以class次数是一个5位数。 因为转移是累积的,我们可以将其分为五个不断变化。 明显的版本使用分支,但你排除了这一点。

设N是介于1和5之间的数字。 如果值为2 N 的位在y中设置,则要将x移位2 N ,否则保持x不变。 这是一种方法:

 #define SHIFT(N) x = isel(((y >> N) & 1) - 1, x << (1 << N), x); 

宏根据是否在y中设置第N位,将x分配给x << 2 ** N或x。

然后是司机:

 SHIFT(1); SHIFT(2); SHIFT(3); SHIFT(4); SHIFT(5) 

注意,N是一个宏变量并且变为常量。

不知道这是否实际上比变速更快。 如果它会,人们想知道为什么微代码不会运行这个...

这个让我失望。 我现在已经放弃了六个想法。 所有这些都利用了这样的概念:向自身添加一个东西向左移动1,对结果做同样的操作向左移动4,依此类推。 如果保留左移0,1,2,4,8和16的所有部分结果,则通过测试换档变量的第0位到第4位,您可以获得初始换档。 现在再做一次,移位变量中每1位一次。 坦率地说,你也可以把你的处理器送去喝咖啡。

我寻求真正帮助的一个地方是Hank Warren的Hacker’s Delight (这是这个答案中唯一有用的部分)。

这个怎么样:

 int[] multiplicands = { 1, 2, 4, 8, 16, 32, ... etc ...}; int ShiftByVar( int x, int y ) { //return x << y; return x * multiplicands[y]; } 

这里有一些关于位操作黑魔法的好东西: 高级位操作fu(Christer Ericson的博客)

不知道它是否可以直接应用,但如果有办法,可能会在某处提供一些提示。

这是一个简单的不可滚动的东西:

 int result= value; int shift_accumulator= value; for (int i= 0; i<5; ++i) { result += shift_accumulator & (-(k & 1)); // replace with isel if appropriate shift_accumulator += shift_accumulator; k >>= 1; }