为什么GCC没有尽可能地优化这组分支和条件?
Why does GCC not optimize this set of branching and conditionals as much as it could?
下面的三段代码实现了完全相同的效果。然而,当在GCC 4.5.2上使用-O3编译时,许多迭代的时间变化相当明显。
1 -正常分支,使用多个条件,最佳时间1.0:
// a, b, c, d are set to random values 0-255 before each iteration.
if (a < 16 or b < 32 or c < 64 or d < 128) result += a+b+c+d;
2 -分支,手动按位使用或检查条件,最佳时间0.92:
if (a < 16 | b < 32 | c < 64 | d < 128) result += a+b+c+d;
3 -最后,在没有分支的情况下得到相同的结果,最佳时间为0.85:
result += (a+b+c+d) * (a < 16 | b < 32 | c < 64 | d < 128);
当作为我编写的基准程序的内循环运行时,上述时间是每种方法的最佳时间。random()
在每次运行前以相同的方式播种。
在我做这个基准测试之前,我假设GCC会优化这些差异。尤其是第二个例子让我摸不着头脑。有人能解释为什么GCC不把这样的代码转换成等价的更快的代码吗?
编辑:修正了一些错误,并明确了随机数的创建和使用,以便不被优化掉。它们总是在原始基准中,我只是把这里的代码搞砸了。
下面是一个实际的基准测试函数的例子:boost::random::mt19937 rng;
boost::random::uniform_int_distribution<> ranchar(0, 255);
double quadruple_or(uint64_t runs) {
uint64_t result = 0;
rng.seed(0);
boost::chrono::high_resolution_clock::time_point start =
boost::chrono::high_resolution_clock::now();
for (; runs; runs--) {
int a = ranchar(rng);
int b = ranchar(rng);
int c = ranchar(rng);
int d = ranchar(rng);
if (a < 16 or b < 32 or c < 64 or d < 128) result += a;
if (d > 16 or c > 32 or b > 64 or a > 128) result += b;
if (a < 96 or b < 53 or c < 199 or d < 177) result += c;
if (d > 66 or c > 35 or b > 99 or a > 77) result += d;
}
// Force gcc to not optimize away result.
std::cout << "Result check " << result << std::endl;
boost::chrono::duration<double> sec =
boost::chrono::high_resolution_clock::now() - start;
return sec.count();
}
完整的基准测试可以在这里找到。
OP已经改变了一点,因为我原来的答案。
在情况1中,由于or
短路,我希望编译器生成四个比较然后分支的代码段。分支显然是非常昂贵的,尤其是当它们没有按照预测的路径运行时。
在情况2中,编译器可以决定执行所有四个比较,将它们转换为bool 0/1结果,然后按位or
将所有四个部分放在一起,然后执行单个(额外)分支。这可能会为更少的分支带来更多的比较。似乎减少分支的数量确实提高了性能。
在情况3中,事情的工作原理与2几乎相同,除了在最后可以通过显式地告诉编译器"我知道结果将是0或1,所以只需将左边的东西乘以该值"来消除另一个分支。乘法运算显然比硬件上的相应分支更快。这与第二个示例形成对比,在第二个示例中,编译器不知道按位or
可能输出的范围,因此它必须假设它可以是任何整数,并且必须执行比较-跳转操作。
历史的原始答案:如果random
有副作用(正常的PRNG会有),那么第一种情况在功能上与第二种和第三种情况不同,因此编译器可能会以不同的方式优化它们。具体地说,第一种情况将只调用random
所需的次数以通过检查,而在其他两种情况中,random
将始终调用四次。这将(假设random
确实是有状态的)导致未来的随机数不同。
第二个和第三个之间的区别是因为编译器可能由于某种原因无法确定按位或的结果总是0或1。当你提示它做乘法而不是分支时,由于流水线,乘法运算可能会更快。
使用逻辑运算符,代码将进行分支并提前退出。位操作符总是完成全部工作。
在第一种情况下,分支预测会更差,尽管对于更大的示例,它会优于按位的情况。
它不能优化掉random()
,因为这个函数不是纯的(幂等的)。
您总是可以尝试优化分支和乘法。而不是:
if (test) result+= blah;
或
result+= blah*(test);
你可以这样做:
result+= blah&(-(test));
如果test
为false,则-false==0
和(blah&0)==0
为false。如果test
为true,则-true==~0
和(blah&~0)==blah
。您可能需要将test
更改为!!test
以确保true==1
。
在我使用gcc 4.5.3的机器(Intel E5503)上,我发现版本1通常是最快的,尽管差异在测量噪声范围内(f3是最慢的,尽管只比f1慢约2%)。
你如何衡量你的时间?您可能会发现,您所看到的差异更多是由于生成的代码中的实际差异。
- 为什么"do while"循环不断退出,即使条件计算结果为 false?
- 在没有太多条件句的情况下,我如何避免被零除
- 基于多个条件处理地图中的所有元素
- 函数指针与条件分支
- 条件分支评估
- 如果条件取决于模板类型并且在编译时已知,是否可以保证C++编译器不会生成分支?
- Clang++ 6.0 内存清理器未报告返回值指示条件分支的函数中的未初始化局部变量
- 根条件分支选择
- 最佳无分支有条件选择两个SSE2填充双打
- 如何避免参数数量依赖于条件的调用分支?
- 有没有办法避免此函数中的分支/条件逻辑?
- C ,最有效的方法将大写速度更改为小写,反之亦然,而无需条件分支
- 常量条件的分支预测
- C++位掩码/条件分支优化
- 条件分支
- 如何在多个条件下进行分支/切换
- 如果设置了最低位,则有条件地无分支异或
- 为什么GCC没有尽可能地优化这组分支和条件?
- 指针解引用开销vs分支/条件语句
- 如果不能在条件分支中调用 CUDA,如何在 CUDA 中减少__syncthreads?