我试图找到一种方法来执行间接左移/右移操作而不实际使用变量移位操作或任何分支。
我正在研究的特定PowerPC处理器有一个怪癖,即按常数立即移位,就像
int ShiftByConstant( int x ) { return x << 3 ; }
是快速的,单操作的,超标量的,而变量的变换,如
int ShiftByVar( int x, int y ) { return x << y ; }
是一个 微码操作,需要7-11个周期才能执行,而管道的其余部分都会停止运行。
我想做的是找出哪个非微码整数PPC操作 将sRAW 解码,然后单独发布。这无助于延迟 将sRAW 本身 - 它将用六个替代一个操作 - 但在这六个操作之间,我可以将一些工作分配给其他执行单元并获得净收益。
我似乎无法找到μopssraw解码到的任何地方 - 有没有人知道如何用一系列常量移位和基本整数运算替换变量位移? (for循环或开关或其中带有分支的任何东西都不起作用,因为分支惩罚甚至比微码惩罚更大。)
这不需要在装配中回答;我希望学习算法而不是特定的代码,所以用C语言或高级语言甚至伪代码的答案都会非常有用。
编辑: 我应该补充几点说明:
- 我甚至都不担心
关于可移植性
PPC有一个条件移动,所以我们可以假设
无分支的存在
内在功能
int isel(a,b,c){return a> = 0? b:c; }
(如果你写出三元的话
做同样的事我会得到什么
你意思是)
- 整数乘法也是
微编码,甚至比sraw慢。 :-(
干得好...
我决定尝试这些,因为Mike Acton声称它会比在他的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()代码。
// 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;
可能需要更长时间才能执行,但如果您有其他代码,则更容易交错。
假设你的最大班次为31.所以班次数是一个5位数。因为转移是累积的,我们可以将其分为五个不断变化。明显的版本使用分支,但你排除了这一点。
设N是介于1和5之间的数字。您想将x移动2如果该值为2,则为N.N设置为y,否则保持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位一次。坦率地说,你也可以把你的处理器送去喝咖啡。
我寻求真正帮助的一个地方是汉克沃伦的 黑客的喜悦 (这是这个答案中唯一有用的部分)。
这个怎么样:
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;
}