8

我在一个运行了大约 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
4

6 回答 6

4

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

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

HVD 的最新:9.2s

表版本: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);  
    }
  }
}
于 2012-08-19T21:32:33.143 回答
4

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

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

于 2012-08-19T21:45:48.563 回答
1

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

if (m){
    if (a==k) {a = 0;} else {++a;}
}
else {
    if (a==0) {a = k;} else {--a;}
}
于 2012-08-19T21:11:37.843 回答
1

添加到锑的重写:

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

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

a=(a+1)%k;

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

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

于 2012-08-19T21:19:00.123 回答
1

这没有分支。因为 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);
于 2012-08-19T22:15:34.863 回答
1

如果 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 相关条件更罕见。

于 2012-08-19T22:19:39.433 回答