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

时间:2011-10-06 15:46:24

标签: c++ optimization gcc

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

The full benchmark can be found here

4 个答案:

答案 0 :(得分:12)

自我原来的答案以来,OP已经改变了一点。让我试着再次访问。

在情况1中,由于or短路,我希望编译器生成四个比较然后分支的代码段。分支显然可能相当昂贵,特别是如果它们没有达到预期的路径。

在第2种情况下,编译器可以决定进行所有四次比较,将它们转换为bool 0/1结果,然后按位or将所有四个部分组合在一起,然后执行单个(附加)分支。这可能会对可能更少的分支进行更多比较。似乎减少分支数确实可以提高性能。

在第3种情况下,事情与2完全相同,除非最后通过明确地告诉编译器可以消除一个更多的分支“我知道结果将是零或一,所以只需乘以左边的东西通过该值“。乘法显然比硬件上的相应分支更快。这与编译器不知道按位or的可能输出范围的第二个示例形成对比,因此它必须假设它可以是任何整数并且必须进行比较和跳转。< / p>

历史的原始答案: 第一种情况在功能上与第二种和第三种情况不同,如果random具有副作用(普通PRNG会这样做),那么编译器可能会以不同方式对它们进行优化。具体来说,第一种情况只会根据需要多次调用random来通过检查,而在另外两种情况下,random将始终被调用四次。这将(假设random确实是有状态的)导致未来的随机数不同。

第二个和第三个之间的区别是因为编译器可能由于某种原因无法弄清楚按位的结果或者总是为0或1.当你给它一个提示来进行乘法而不是分支由于流水线操作,乘法可能会更快。

答案 1 :(得分:1)

使用逻辑运算符,代码将分支并提前输出。按位运算符总能完成所有工作。

第一种情况下的分支预测会更糟糕,但是对于更大的例子,分支预测的性能将超过逐位的情况。

它无法优化random(),因为该函数不是pure(幂等)。

答案 2 :(得分:1)

您可以随时尝试优化分支并乘以。而不是:

if (test) result+= blah;

result+= blah*(test);

你可以这样做:

result+= blah&(-(test));

如果test为假,-false==0(blah&0)==0。如果test为真,-true==~0(blah&~0)==blah。您可能必须使用test作为!!test,以确保true==1

答案 3 :(得分:0)

在我的机器(Intel E5503)上使用gcc 4.5.3,我发现版本1通常是最快的,尽管差异在测量噪音范围内(f3是最慢的,但比f1慢了约2%)

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