我正在尝试找到一种方法来执行间接左移/右移操作而不实际使用变量移位操作或任何分支。
我正在研究的特定PowerPC处理器有一个怪癖,即按常数立即转换,如
int ShiftByConstant( int x ) { return x << 3 ; }
是快速,单操作和超标量,而是逐个变量,如
int ShiftByVar( int x, int y ) { return x << y ; }
我想要做的是找出sraw解码成哪个非微码整数PPC操作然后单独发布它们。这对sraw
本身的延迟没有帮助 - 它将用六个替换一个操作 - 但是在这六个操作之间我可以将一些工作双重调度到其他执行单元并获得净增益。
我似乎无法找到μopssraw解码到的任何地方 - 有谁知道如何用一系列常量移位和基本整数运算替换变量位移? (for循环或开关或其中带有分支的任何东西都不会起作用,因为即使对于正确预测的分支,分支惩罚甚至大于微码惩罚。)
这不需要在集会中回答;我希望学习算法而不是特定的代码,所以用C语言或高级语言甚至伪代码的答案都会非常有用。
编辑:我应该补充一些说明:
PPC有一个条件移动,所以我们可以假设存在无分支内在函数
int isel(a, b, c) { return a >= 0 ? b : c; }
(如果你写出一个做同样事情的三元组,我会明白你的意思)
sraw
慢。 : - (答案 0 :(得分:8)
你去......
我决定尝试这些,因为Mike Acton声称它比在he suggests to avoid the indirect shift的CellPerformance网站上使用CELL / PS3微码变换更快。但是,在我的所有测试中,使用微编码版本不仅比间接移位的完全通用无分支替换更快,而且代码(1指令)的内存占用更少。
我作为模板执行这些操作的唯一原因是为签名(通常是算术)和无符号(逻辑)移位获得正确的输出。
template <typename T> 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 <typename T> 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() code on your website。
// 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);
答案 1 :(得分:5)
这个怎么样:
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;
可能需要更长时间才能执行,但如果您有其他代码可以交错,则更容易交错。
答案 2 :(得分:4)
假设你的最大班次为31.所以班次数是一个5位数。因为转移是累积的,我们可以将其分为五个不断变化。明显的版本使用分支,但你排除了它。
设 N 是介于1和5之间的数字。如果值为2的位,则希望将 x 移位2 N sup> N 在 y 中设置,否则保持x完整。这是一种方法:
#define SHIFT(N) x = isel(((y >> N) & 1) - 1, x << (1 << N), x);
宏分配给x x << 2ᴺ
或x
,具体取决于是否在y中设置了N th 位。
然后是司机:
SHIFT(1); SHIFT(2); SHIFT(3); SHIFT(4); SHIFT(5)
请注意,N是一个宏变量并且变为常量。
不知道这是否实际上比变速更快。如果是这样的话,人们会想知道为什么微码不会运行这个......
答案 3 :(得分:1)
这个让我失望。我现在已经放弃了六个想法。所有这些都利用了这样的概念:向自身添加一个东西向左移动1,对结果做同样的操作向左移动4,依此类推。如果保留左移0,1,2,4,8和16的所有部分结果,那么通过测试换档变量的第0位到第4位,您可以获得初始换档。现在再做一次,移位变量中每1位一次。坦率地说,你也可以把你的处理器送去喝杯咖啡。
我寻求真正帮助的一个地方是Hank Warren的Hacker's Delight(这是这个答案中唯一有用的部分)。
答案 4 :(得分:0)
这个怎么样:
int[] multiplicands = { 1, 2, 4, 8, 16, 32, ... etc ...};
int ShiftByVar( int x, int y )
{
//return x << y;
return x * multiplicands[y];
}
答案 5 :(得分:0)
这里有一些关于位操纵黑魔法的好东西: Advanced bit manipulation fu (Christer Ericson's blog)
不知道是否有任何直接适用的,但如果有办法,可能会在某处提供一些提示。
答案 6 :(得分:0)
如果可以提前计算班次计数,那么我有两个可行的想法
使用自我修改代码
只需立即修改指令中的移位量即可。或者为可变移位的函数动态生成代码
如果可能,将具有相同移位计数的值组合在一起,并使用Duff的设备或函数指针一次执行所有操作,以最大程度地减少分支预测错误
// shift by constant functions
typedef int (*shiftFunc)(int); // the shift function
#define SHL(n) int shl##n(int x) { return x << (n); }
SHL(1)
SHL(2)
SHL(3)
...
shiftFunc shiftLeft[] = { shl1, shl2, shl3... };
int arr[MAX]; // all the values that need to be shifted with the same amount
shiftFunc shl = shiftLeft[3]; // when you want to shift by 3
for (int i = 0; i < MAX; i++)
arr[i] = shl(arr[i]);
此方法也可以与自修改或运行时代码生成结合使用,以消除对函数指针的需要。
编辑:如所评论的那样,很遗憾,跳转没有注册分支,因此,唯一可行的方法就是生成代码,如上所述。使用SIMD
如果值的范围较小,则查找表是另一种可能的解决方法
#define S(x, n) ((x) + 0) << (n), ((x) + 1) << (n), ((x) + 2) << (n), ((x) + 3) << (n), \
((x) + 4) << (n), ((x) + 5) << (n), ((x) + 6) << (n), ((x) + 7 << (n)
#define S2(x, n) S((x + 0)*8, n), S((x + 1)*8, n), S((x + 2)*8, n), S((x + 3)*8, n), \
S((x + 4)*8, n), S((x + 5)*8, n), S((x + 6)*8, n), S((x + 7)*8, n)
uint8_t shl[256][8] = {
{ S2(0U, 0), S2(8U, 0), S2(16U, 0), S2(24U, 0) },
{ S2(0U, 1), S2(8U, 1), S2(16U, 1), S2(24U, 1) },
...
{ S2(0U, 7), S2(8U, 7), S2(16U, 7), S2(24U, 7) },
}
现在x << n
就是shl[x][n]
,x是uint8_t
。该表占用2KB(8×256 B)的内存。但是,对于16位值,您将需要一个1MB的表(16×64 KB),该表仍然可行,并且可以通过将两个16位移位结合在一起来进行32位移位
答案 7 :(得分:-1)
这里有一些可以轻易展开的东西:
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;
}