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


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

我正在研究的特定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语言或高级语言甚至伪代码的答案都会非常有用。

编辑: 我应该补充几点说明:

  1. 我甚至都不担心 关于可移植性
  2. PPC有一个条件移动,所以我们可以假设 无分支的存在 内在功能

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

    (如果你写出三元的话 做同样的事我会得到什么 你意思是)

  3. 整数乘法也是 微编码,甚至比sraw慢。 :-(

8463
2018-02-12 03:09


起源

让人想到的一件事是Duffs Device(en.wikipedia.org/wiki/Duffs_device)改为使用一位移位指令。你需要一个分支,然后需要几个移位指令,所以我猜它会慢一些。 - some
@Some:单分支惩罚大于微码指令惩罚,因此Duffs设备不是优化。 - Adisak
playstation3 / cell programmer,是吗? - Nils Pipenbrinck


答案:


干得好...

我决定尝试这些,因为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);

6
2017-10-21 21:35



谢谢!是的,经过一段时间的浮躁之后我得出结论,这里没有办法击败微码。我想它使用微操作,ISA中没有相应的操作码。感谢改进的isel() - 我刚刚使用道森的,甚至从未想过它可以改进! - Crashworks
当我第一次阅读你的帖子时,我以为你找到了一个神奇的isel()内在/ asm-op,我已经错过了一些非常好的掩码函数。 FWIW,您可以在PC上以及CMOVcc asm-ops上快速完成,因此请记住在不同的目标平台上可能有不同版本的isel。 - Adisak
哦,它可能很明显,但nVal =行基本上是一个已扩展的isel()。 - Adisak


这个怎么样:

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;

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


4
2018-02-12 03:19



问题没有指明分支! - Norman Ramsey
是的,但他所说的可以通过无分支的条件移动操作来实现 - 我得到了他正在尝试沟通的东西。 - Crashworks
哦,预测指令是一个完全改变游戏规则的人。约书亚从一个讨厌的downvote中得救了!它是如何表现的? - Norman Ramsey
我不认为代码是对的。这些位测试应该是2的幂。 - MSN


假设你的最大班次为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是一个宏变量并且变为常量。

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


3
2018-02-12 04:06



这很有趣 - 我会在模拟器中试一试。微编码操作肯定是通过用一些其他非微编码操作序列替换自己然后运行它们来工作的;问题是它没有流水线,所以我试图弄清楚μops的神奇序列是什么。 - Crashworks
如果使用x = isel( - (signed)(y >> N&1),x,x <<(1 << N)),则可以保存额外的op。 - MSN


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

我寻求真正帮助的一个地方是汉克沃伦的 黑客的喜悦 (这是这个答案中唯一有用的部分)。


1
2018-02-12 03:27



是的,我遇到了你所做的同一面墙。然而,我发现“你可能会把你的处理器送去喝杯咖啡”这句话非常令人愉快,并且今后将以各种可能的借口使用它。 =) - Crashworks


这个怎么样:

int[] multiplicands = { 1, 2, 4, 8, 16, 32, ... etc ...};

int ShiftByVar( int x, int y )
{
    //return x << y;
    return x * multiplicands[y];
}

0
2018-02-12 03:33



可悲的是,乘法也很慢。 =( - Crashworks


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

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


0
2018-02-12 04:27





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

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;
}

0
2017-08-24 17:34