A = a;
B = b;
C 和 C++ 标准是否明确要求在执行 A=a 之前必须严格执行 B=b?假设 A、B、a 和 b 的地址都不同,编译器是否允许交换两个语句的执行顺序以进行某些优化?
如果 C 和 C++ 的答案不同,请同时告知。
编辑:问题的背景是,在棋盘游戏 AI 设计中,人们使用 无锁共享哈希表 进行优化,如果没有添加 volatile 限制,其正确性强烈依赖于执行顺序。
A = a;
B = b;
C 和 C++ 标准是否明确要求在执行 A=a 之前必须严格执行 B=b?假设 A、B、a 和 b 的地址都不同,编译器是否允许交换两个语句的执行顺序以进行某些优化?
如果 C 和 C++ 的答案不同,请同时告知。
编辑:问题的背景是,在棋盘游戏 AI 设计中,人们使用 无锁共享哈希表 进行优化,如果没有添加 volatile 限制,其正确性强烈依赖于执行顺序。
这两个标准允许按照任意顺序执行指令,只要不会改变可观察的行为。这被称为“as-if”规则:
需要注意的是,正如评论中所指出的那样,“可观察的行为”是指具有定义行为的程序的可观察行为。如果你的程序存在未定义行为,则编译器无需推理该行为。
一个例子 该文章提供了一个简单的程序,我们可以在其中看到这种重新排序:内存重新排序的基本规则被编译器开发者和CPU供应商普遍遵循,可以概括为:
不得修改单线程程序的行为。
int A, B; // Note: static storage duration so initialized to zero
void foo()
{
A = B + 1;
B = 0;
}
在更高的优化级别下,B = 0 在 A = B + 1 之前执行,我们可以使用 godbolt 来复现这个结果,而使用 -O3 时会产生以下结果(see it live):
movl $0, B(%rip) #, B
addl $1, %eax #, D.1624
为什么?
编译器为什么要重新排序呢?文章解释说,这正是处理器这样做的原因,因为体系结构的复杂性:
正如我在开头提到的那样,编译器修改内存交互顺序的原因与处理器这样做的原因完全相同——性能优化。这些优化是现代CPU复杂性的直接结果。
标准
在C++草案标准中,这在第1.9节“程序执行”中有所涵盖,其中写道(以后重点突出):
本国际标准中的语义描述定义了一个参数化的不确定的抽象机。本国际标准对符合规范的实现的结构没有要求。特别是,它们不需要复制或模拟抽象机的结构。相反,符合规范的实现需要模拟(仅)抽象机的可观察行为,如下所述。5
脚注5告诉我们这也被称为as-if规则:
内存排序
如流程图所示,在进行无锁编程的时候,特别是在多核或任何对称多处理器环境下,并且您的环境没有保证顺序一致性,您必须考虑如何防止内存重排序。
在当今的架构中,用于强制正确内存排序的工具通常分为三类,可以同时防止编译器重排序和处理器重排序:
获取语义可以防止程序顺序后续的内存重排序操作,而释放语义可以防止程序顺序前面的内存重排序操作。这些语义特别适用于生产者/消费者关系的情况,其中一个线程发布一些信息,另一个线程读取它。我也会在未来的文章中更详细地讨论这个问题。
addl $1, %eax而不是incl %eax?即使针对a++,它也只会生成a += 1...不过ICC的行为符合预期。 - user719662gcc所做出的假设。 - Shafik Yaghmour由于A = a;和B = b;在数据依赖方面是独立的,因此这并不重要。如果先前指令的输出/结果影响后续指令的输入,则顺序很重要,否则不重要。这通常是严格的顺序执行。
如果您这样做,可能会对您有兴趣:
{ A=a, B=b; /*etc*/ }
请注意在分号位置的逗号。
然后,C++规范和任何确认的编译器都必须保证执行顺序,因为逗号运算符的操作数始终从左到右进行评估。这确实可以用于防止优化器通过重新排序来破坏您的线程同步。逗号有效地成为一个障碍,跨越该障碍不允许重新排序。