【问题标题】:Branching elimination using bitwise operators使用按位运算符消除分支
【发布时间】:2012-08-15 07:58:40
【问题描述】:

我在一个运行了大约 2^26 次的循环中有一些关键的分支代码。分支预测不是最优的,因为m 是随机的。我将如何删除分支,可能使用按位运算符?

bool m;
unsigned int a;
const unsigned int k = ...; // k >= 7
if(a == 0)
    a = (m ? (a+1) : (k));
else if(a == k)
    a = (m ?     0 : (a-1));
else
    a = (m ? (a+1) : (a-1));

这里是gcc -O3生成的相关程序集:

.cfi_startproc
movl    4(%esp), %edx
movb    8(%esp), %cl
movl    (%edx), %eax
testl   %eax, %eax
jne L15
cmpb    $1, %cl
sbbl    %eax, %eax
andl    $638, %eax
incl    %eax
movl    %eax, (%edx)
ret
L15:
cmpl    $639, %eax
je  L23
testb   %cl, %cl
jne L24
decl    %eax
movl    %eax, (%edx)
ret
L23:
cmpb    $1, %cl
sbbl    %eax, %eax
andl    $638, %eax
movl    %eax, (%edx)
ret
L24:
incl    %eax
movl    %eax, (%edx)
ret
.cfi_endproc

【问题讨论】:

  • 在开始之前,您可能需要确保您关心的编译器实际上无法将这些优化为条件移动。
  • 0
  • @Eric 是的,0
  • 我很惊讶 GCC 不能将内部分支变成条件移动。你这里的那些比this question 中的那个要简单得多,-O3 可以完成这项工作。
  • 貌似基本是a = a + (m?1:-1) % k;一个二元 LUT 可以修复的分支。还是我的负模数错误...?

标签: c++ optimization bit-manipulation


【解决方案1】:

无分支无除法模可能很有用,但测试表明在实践中并非如此。

const unsigned int k = 639;
void f(bool m, unsigned int &a)
{
    a += m * 2 - 1;
    if (a == -1u)
        a = k;
    else if (a == k + 1)
        a = 0;
}

测试用例:

unsigned a = 0;
f(false, a);
assert(a == 639);
f(false, a);
assert(a == 638);
f(true, a);
assert(a == 639);
f(true, a);
assert(a == 0);
f(true, a);
assert(a == 1);
f(false, a);
assert(a == 0);

实际计时,使用测试程序:

int main()
{
    for (int i = 0; i != 10000; i++)
    {
        unsigned int a = k / 2;
        while (a != 0) f(rand() & 1, a);
    }
}

(注意:没有srand,所以结果是确定性的。)

我原来的答案:5.3s

问题中的代码:4.8s

查找表:4.5s (static unsigned lookup[2][k+1];)

查找表:4.3s (static unsigned lookup[k+1][2];)

埃里克的回答:4.2s

这个版本:4.0s

【讨论】:

  • 哇!你的版本确实比我的和其他版本快,它从 30 纳秒/循环中减少了大约 7 纳秒。很大的进步!感谢您的所有努力!
【解决方案2】:

现在我发现最快的是表格实现

我得到的时间(更新了新的测量代码)

HVD 的最新:9.2 秒

表格版本:7.4s(k=693)

建表代码:

    unsigned int table[2*k];
    table_ptr = table;
    for(int i = 0; i < k; i++){
      unsigned int a = i;
      f(0, a);
      table[i<<1] = a;

      a = i;
      f(1, a);
      table[i<<1 + 1] = a;
    }

表运行时循环:

void f(bool m, unsigned int &a){
  a = table_ptr[a<<1 | m];
}

通过 HVD 的测量代码,我看到了 rand() 支配运行时的成本,因此无分支版本的运行时与这些解决方案的范围大致相同。我将测量代码更改为此(更新以保持随机分支顺序,并预先计算随机值以防止 rand() 等破坏缓存)

int main(){
  unsigned int a = k / 2;
  int m[100000];
  for(int i = 0; i < 100000; i++){
    m[i] = rand() & 1;
  }

  for (int i = 0; i != 10000; i++
  {
    for(int j = 0; j != 100000; j++){
      f(m[j], a);  
    }
  }
}

【讨论】:

  • 不错!我创建了一个测试程序并进行了测量,原始版本需要约 4.8 秒,我的无分支版本需要约 5.3 秒,所以不买任何东西(我在现在删除的答案中已经指出),你的需要约 4.2 秒。
  • 哇。无分支无除法模仍然比分支差。请参阅我的新答案。
  • 我没有看到您在最新版本中获得的好处。是的,rand() 会占用大部分时间,但它占用的时间应该是恒定的,所以这没关系,不同版本之间的绝对差异仍然是一个有用的衡量标准。我用那个版本大约 4.3 秒。
  • 无论哪种方式,我只是检查并认为查找表明显优于我们的两种解决方案。
  • 我已经在我的测试中包含了它,但它没有(可能是因为它以不可预测的顺序访问内存)。
【解决方案3】:

我认为你不能完全删除分支,但你可以通过先在 m 上分支来减少数量。

if (m){
    if (a==k) {a = 0;} else {++a;}
}
else {
    if (a==0) {a = k;} else {--a;}
}

【讨论】:

    【解决方案4】:

    添加到锑的重写:

    if (a==k) {a = 0;} else {++a;}
    

    看起来像环绕式增加。你可以这样写

    a=(a+1)%k;
    

    当然,只有当除法实际上比分支快时才有意义。

    另一个不确定;懒得去想 (~0)%k 会是什么。

    【讨论】:

    • +1,gcc 很好地优化了常量除法(用依赖整数溢出的乘法替换它),但它应该是% (k + 1)。对于减法,只需在减 1 并得到余数之前再加一个 k + 1a = (a + k) % (k + 1);
    • 我自己扩展了你的答案。
    【解决方案5】:

    这没有分支。因为 K 是常数,编译器可能能够根据它的值优化模数。如果 K 是“小”,那么完整的查找表解决方案可能会更快。

    bool m;
    unsigned int a;
    const unsigned int k = ...; // k >= 7
    const int inc[2] = {1, k};
    
    a = a + inc[m] % (k+1);
    

    【讨论】:

      【解决方案6】:

      如果 k 不够大导致溢出,你可以这样做:

      int a; // Note: not unsigned int
      int plusMinus = 2 * m - 1;
      a += plusMinus;
      if(a == -1) 
          a = k; 
      else if (a == k+1) 
          a = 0; 
      

      仍然是分支,但分支预测应该更好,因为边缘条件比 m 相关条件少。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-07-14
        • 1970-01-01
        • 2020-08-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-01-23
        • 1970-01-01
        相关资源
        最近更新 更多