使用按位运算符消除分支
Branching elimination using bitwise operators
我在一个运行大约 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
无分支无除法模可能很有用,但测试表明,在实践中并非如此。
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.3秒
题中的代码:4.8s
查找表: 4.5s ( static unsigned lookup[2][k+1];
)
查找表: 4.3s ( static unsigned lookup[k+1][2];
)
埃里克的答案:4.2秒
此版本: 4.0s
我发现最快的现在是表实现
我得到的时间(针对新的测量代码进行了更新)
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);
}
}
}
我认为你不能完全删除分支,但你可以通过先在 m 上分支来减少数量。
if (m){
if (a==k) {a = 0;} else {++a;}
}
else {
if (a==0) {a = k;} else {--a;}
}
添加到锑的重写中:
if (a==k) {a = 0;} else {++a;}
看起来像是环绕式的增加。你可以把它写成
a=(a+1)%k;
当然,只有当部门实际上比分支快时,这才有意义。
不确定另一个;懒得考虑(~0)%k会是什么。
这没有分支。由于 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);
如果 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 相关条件更罕见。
相关文章:
- 为什么比较运算符如此快速
- C++映射:具有自定义类的运算符[]不起作用(总是返回0)
- 使用C++中的模板和运算符重载执行矩阵运算
- 为什么这个运算符<重载函数对 STL 算法不可见?
- 增量运算符与后缀混淆
- 一个关于在C++中重载布尔运算符的问题
- 运算符C++ "delete []"仅删除 2 个前值
- IPC使用多个管道和分支进程来运行Python程序
- 模板类无法识别友元运算符
- 我可以使用条件运算符初始化C风格的字符串文字吗
- 关闭||运算符优化
- 通过继承类使用来自不同命名空间的运算符
- C++Cast运算符过载
- 如何使用AngelScript注册SFML Vector2运算符
- 重载元组索引运算符-C++
- 如何使用重载的相等(==)运算符向测试用例添加描述
- 为什么Mat类的两个对象可以在不重载运算符+的情况下添加
- 为什么在三元运算符的分支之间返回 lambda 对某些 lambda 有效?
- 使用按位运算符消除分支
- 使用值而不是引用的赋值运算符的分支