为啥 GCC 没有尽可能地优化这组分支和条件?
Posted
技术标签:
【中文标题】为啥 GCC 没有尽可能地优化这组分支和条件?【英文标题】:Why does GCC not optimize this set of branching and conditionals as much as it could?为什么 GCC 没有尽可能地优化这组分支和条件? 【发布时间】:2011-10-06 15:46:24 【问题描述】:以下三段代码实现的效果完全相同。然而,当在 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.
【问题讨论】:
什么是or
?您的第一个示例似乎不是有效的 C。
它(至少)是 ||
的 C++ 同义词。
@Mark B - 是吗?从什么时候开始?
@HeathHunnicutt: Apparently, since forever. 它们还作为一组在 C89 之后添加的宏存在(似乎不是 C99,这很奇怪)。
@Heath Hunnicutt C++98 C.2.2.2 The tokens and, and_eq, bitand, bitor, compl, not_eq, not, or, or_eq, xor, and xor_eq are keywords in this International Standard (2.11). They do not appear as macro names defined in <ciso646>
。另请参阅 2.5/2/table 2,其中显示了直接备用令牌映射。
【参考方案1】:
自我最初的回答以来,OP 发生了一些变化。让我试着重温这里。
在情况 1 中,由于 or
短路,我希望编译器生成四个比较然后分支代码段。分支显然会非常昂贵,尤其是如果它们没有按照预期的路径运行。
在情况 2 中,编译器可以决定进行所有四个比较,将它们转换为 bool 0/1 结果,然后按位 or
所有四个部分一起,然后执行单个(附加)分支。这可能会用更多的比较来换取可能更少的分支。似乎减少分支数量确实可以提高性能。
在第 3 种情况下,情况与第 2 种情况几乎相同,只是在最后,可以通过明确告诉编译器“我知道结果将是 0 或 1,所以只需将左边的东西相乘,就可以消除另一个分支按那个值”。乘法显然比硬件上的相应分支更快。这与第二个示例形成对比,其中编译器不知道按位 or
的可能输出范围,因此它必须假设它可以是任何整数,并且必须进行比较和跳转。
历史原答案:
如果 random
有副作用(普通 PRNG 会),第一种情况在功能上与第二种和第三种不同,因此编译器可能会以不同方式优化它们是有道理的。具体来说,第一种情况只会调用random
所需的次数以通过检查,而在其他两种情况下,random
将始终调用四次。这将(假设 random
确实是有状态的)导致未来的随机数不同。
第二个和第三个之间的区别是因为编译器可能由于某种原因无法计算出按位 or 的结果将始终为 0 或 1。当您提示它执行乘法而不是分支时由于流水线,乘法可能会更快。
【讨论】:
我很想看看if (rand()>3) ++a
和 a+=(rand()>3)
之间的时间差异是否出于某种原因 GCC 不进行此优化?
@Mooing Duck G++ 是否可能无法确定 ++
的副作用与 +=
的副作用相同(假设你可以神奇地 LD_PRELOAD 一个共享库,它会改变一个的语义那些运营商)。
+1,如果 random()
碰巧调用了 rand()
则没有“可能”,它保证是为每个种子输出可重复序列的 PRNG。
@MarkB:我假设a
和result
是原始类型。如果没有,那么我们需要更多信息来回答这个问题。
@MarkB:非常抱歉。您对random
及其副作用是完全正确的。实际上,我在最初的基准测试中它是正确的,但是在将它放在这里时却搞砸了。您对第二个和第三个之间差异的评论很有趣,但我不完全确定我明白了。可以考虑一下吗?【参考方案2】:
使用逻辑运算符,代码将分支并提前退出。位运算符总是能完成全部工作。
分支预测在第一种情况下会更差,尽管它会在更大的示例中优于按位情况。
它无法优化掉random()
,因为该函数不是pure(幂等)。
【讨论】:
这解释了为什么分支速度较慢。它没有解释为什么 GCC 不承认分支本来可以避免的。 因为它们无法一开始就被避免了,简单地说:random()
会改变程序的行为,即使你忽略了结果。 gcc pure 属性将启用此优化。
如果我错了,请纠正我,但是看到原始基准实际上在之前对random()
进行了所有调用,并且无论条件句中发生了什么,该函数的细节变得无关紧要?我最初的例子有一个拙劣的基准测试版本,其中随机的副作用肯定会把事情搞砸。【参考方案3】:
您始终可以尝试优化分支并相乘。而不是:
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
。
【讨论】:
这是我以前没有注意到的另一个巧妙的优化概念。 +1 这种微优化最好留给编译器。 @spraff:这就是我对分支的看法。你是说例如 GCC 会识别 blah*(test) 总是乘以 1 或 0 并会像上面那样优化它吗?这周我已经花光了我琐碎的研究津贴,所以我不会自己测试这个。 我不知道它是否会,但它可以。更重要的是让下一个人类能够快速阅读。【参考方案4】:在我的带有 gcc 4.5.3 的机器(Intel E5503)上,我发现版本 1 通常是最快的,尽管差异在测量噪声范围内(f3 是最慢的,虽然只比 f1 慢 2%) .
您如何衡量您的时间安排?您可能会发现,您看到的差异更多地是由于生成的代码的实际差异。
【讨论】:
我发布了一个指向实际基准的链接;它在每个循环中放置几个条件来强调计算的那部分。需要提升 1.47。我很想看看你的结果是否仍然不同。以上是关于为啥 GCC 没有尽可能地优化这组分支和条件?的主要内容,如果未能解决你的问题,请参考以下文章