由于优化而导致的代码重新排序

时间:2015-03-11 22:35:59

标签: c++ optimization

我已经多次听到优化程序可能会重新排序我开始相信它的代码。 是否有任何可能发生这种情况的例子或典型案例,我怎样才能避免这样的事情(例如我希望基准不受此影响)?

2 个答案:

答案 0 :(得分:5)

有许多不同类型的“代码运动”(移动代码),它是由优化过程的许多不同部分引起的:

  • 移动这些指令,因为等待内存读取完成而不使用我们从内存中获取的内容在内存读取和操作之间至少放置一条或两条指令是浪费时间
  • 将事物移出循环,因为它只需要发生一次(如果你在不改变y的情况下调用x = sin(y)一次或1000次,x将具有相同的值,所以没有必要这样做在循环中。所以编译器将其移出。
  • 基于“编译器期望此代码比其他位更频繁地触及代码来移动代码,因此如果我们这样做,则更好的缓存命中率” - 例如错误处理被移离错误源,因为你不太可能得到错误[编译器经常理解常用的功能,而且它们通常会导致成功]。
  • 内联 - 代码从实际函数移动到调用函数。这通常会导致其他效果,例如从堆栈中减少推送/弹出寄存器以及参数可以保留在原来的位置,而不必将它们移动到“正确的参数位置”。

我确信我已经错过了上面列表中的一些案例,但这肯定是最常见的一些案例。

编译器完全有权这样做,只要它没有任何“可观察的差异”(除了运行所用的时间和使用的指令数量 - 那些“不计算”)在编译器方面存在可观察到的差异)

你可以做很少的事情来避免编译器重新排序你的代码 - 你可以编写一些代码来确保订单的某种程度。例如,我们可以使用这样的代码:

{
int sum = 0;
for(i = 0; i < large_number; i++)
  sum += i;
}

现在,由于未使用sum,编译器可以将其删除。添加一些检查打印总和的代码将确保根据编译器“使用”它。

同样地:

 for(i = 0; i < large_number; i++)
 {
     do_stuff();
 }

如果编译器可以发现do_stuff实际上没有改变任何全局值或类似值,它会移动代码来形成这个:

 do_stuff();
 for(i = 0; i < large_number; i++)
 {
 }

编译器也可以删除 - 事实上几乎肯定会 - 现在是空循环,以便它根本不存在。 [正如评论中所提到的:如果do_stuff实际上没有改变任何外部的东西,它也可能被移除,但我想到的例子是do_stuff产生结果的地方,但结果是每次都一样]

(例如,如果您在Dh​​rystone基准测试中删除结果的打印输出,则会发生上述情况,因为某些循环会计算除打印输出之外从未使用的值 - 这可能会导致基准测试结果超出最高理论吞吐量处理器的大小为10左右 - 因为基准测试假定循环所需的指令实际上存在,并说它需要X个标称操作来执行每次迭代)

除了确保do_stuff更新函数外部的某个变量,或返回“已使用”的值(例如汇总或其他内容)之外,没有简单的方法可以确保不会发生这种情况。

删除/省略代码的另一个例子是多次将值重复存储到同一个变量:

int x;

for(i = 0; i < large_number; i++)
    x = i * i;

可以替换为:

x = (large_number-1) * (large_number-1);

有时,您可以使用volatile来确保真正发生的事情,但在基准测试中,这可能是有害的,因为编译器也无法优化它应该优化的代码(如果您不小心)你如何使用volatile)。

如果您有一些特别关注的SPECIFIC代码,最好发布它(并使用几个最先进的编译器进行编译,看看它们实际上用它做了什么)。

[请注意,移动代码通常不是一件坏事 - 我确实想要我的编译器(无论是我自己编写的编译器,还是我正在使用的编译器是由其他人编写的)通过移动代码进行优化,因为只要它正确地执行,它就会产生更快/更好的代码!]

答案 1 :(得分:2)

大多数情况下,只有在程序的可观察效果相同的情况下才允许重新排序 - 这意味着您无法分辨。

反例确实存在,例如操作数的顺序未指定,优化器可以自由重新排列。您无法预测这两个函数调用的顺序,例如:

int a = foo() + bar();

阅读sequence points以了解提供的保证。