为什么GCC没有尽可能地优化这组分支和条件?

Why does GCC not optimize this set of branching and conditionals as much as it could?

本文关键字:分支 条件 优化 GCC 尽可能 为什么      更新时间:2023-10-16

下面的三段代码实现了完全相同的效果。然而,当在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%)。

你如何衡量你的时间?您可能会发现,您所看到的差异更多是由于生成的代码中的实际差异。