假设我们有以下三个代码片段:
-
// both a and b are non-volatile ints a = 123; b = 456;
-
// both a and b are non-volatile ints a = rand(); b = rand();
-
cout << "foo" << endl; cout << "bar" << endl;
据我所知,(1) 中的语句可以由编译器重新排序,而 (2)(3) 中的语句不能,因为这会改变程序的可观察行为。
但是编译器如何知道当事情不是那么“明显”依赖时不能重新排序(像 ++a; b = a * 2;
显然是相关的),如(2)(3)?例如,也许像非 constexpr
函数调用这样的某些事情会阻止重新排序......?
最佳答案
管理重新排序(和一般优化)的唯一标准规则是“好像”规则,这意味着编译器可以做任何它喜欢的事情,如果它确定最终结果是相同的,并且标准不会'不要挡路。然而,当我们达到那个级别时,编译器可能不再在 C++ 源代码级别上运行:它可能正在处理一种中间形式,并且在语句中思考实际上可能无法最好地反射(reflect)正在发生的事情。
例如,在你的第二个例子中:
// both a and b are non-volatile ints
a = rand();
b = rand();
编译器可以调用 rand
两次并将第二次 rand
调用的结果存储到 b
,然后再存储第一次 的结果>rand
调用 a
。换句话说,分配已经重新排序,但调用没有。这是可能的,因为编译器优化了比 C++ 更细粒度的程序表示。
各种编译器使用各种技巧来确定两个中间表示指令是否可以重新排序,但大多数情况下,函数调用是无法重新排序的不可逾越的障碍。特别是,对外部库(如 rand
)的调用甚至无法分析并且肯定不会重新排序,因为编译器会更喜欢保守但已知正确的方法。
事实上,函数调用只有在编译器确定它们不能相互干扰时才能重新排序。编译器试图通过别名分析来解决这个问题。这个想法是超越值依赖(例如,在 a * b + c
中,+
操作取决于 a * b
的结果因此 a * b
必须先发生),排序(几乎)仅在您在某处写入并稍后从那里读回时才重要。这意味着,如果您可以正确识别每个内存操作及其影响,则可以确定两个内存操作是否可以重新排序,甚至完全消除。出于这些目的,一个调用被认为是一个大内存操作,包含它所做的所有较小的加载和存储。
不幸的是,众所周知,别名分析的一般情况是不可计算的。虽然编译器变得越来越聪明,但您可能永远不会拥有一个能够系统地对调用重新排序做出最佳决策的编译器,即使您拥有链接所针对的所有源代码也是如此。
一些编译器具有特定于它们自己的属性,这些属性决定了它们是否应该考虑函数重新排序是安全的,而不管它们自己的分析如何。例如,如果 gcc 认为这会提高性能,它会很高兴地重新排序、缓存甚至消除带有 [[gnu::pure]]
属性的函数调用。
关于c++ - 可以重新排序的语句,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38064071/