假设A
,B
,a
和b
都是变量,地址A
,B
,{{ 1}}和a
都不同。然后,对于以下代码:
b
C和C ++标准是否明确要求在A = a;
B = b;
之前严格执行A=a
?鉴于B=b
,A
,B
和a
的地址都不同,编译器是否允许交换两个语句的执行顺序以用于某些目的,例如优化?
如果我的问题的答案在C和C ++中有所不同,我想知道两者。
编辑:问题的背景如下。在棋盘游戏AI设计中,对于优化,人们使用lock-less shared-hash table,如果我们不添加b
限制,则其正确性在很大程度上取决于执行顺序。
答案 0 :(得分:55)
这两个标准都允许这些指令不按顺序执行,只要这不会改变可观察的行为。这被称为as-if规则:
请注意,正如评论中所指出的那样,"可观察行为"是具有已定义行为的程序的可观察行为。如果你的程序有未定义的行为,那么编译器就可以免于推理。
答案 1 :(得分:25)
编译器只有义务模拟程序的可观察行为,因此如果重新排序不违反该原则,那么它将被允许。假设行为定义良好,如果您的程序包含undefined behavior,例如数据竞争,那么程序的行为将是不可预测的,并且评论将需要使用某种形式的同步来保护关键部分。
有用的参考资料
一篇有趣的文章是Memory Ordering at Compile Time,它说:
记忆重新排序的基本规则,这是普遍遵循的 编译器开发人员和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规则:
此规定有时称为“as-if”规则,因为 实施可以忽略任何要求 国际标准只要结果就好像要求一样 从可观察的角度来看,已经服从了 行为的程序。例如,实际的实施需要 如果它可以推导出它的值,则不评估表达式的一部分 未使用,没有影响可观察行为的副作用 制作该节目。
草案C99和草案C11标准在5.1.2.3
程序执行部分中介绍了这一点,尽管我们必须转到索引才能看到它被称为 as-if规则在C标准中:
as-if rule,5.1.2.3
无锁注意事项的更新
文章An Introduction to Lock-Free Programming很好地涵盖了这个主题,对于OP关于无锁共享哈希表实现的关注,本节可能是最相关的:
内存订购
如流程图所示,无论何时进行无锁编程 多核(或任何symmetric multiprocessor),以及您的环境 不保证顺序一致性,你必须考虑如何防止 memory reordering
在今天的架构中,强制执行正确内存排序的工具 通常分为三类,这会阻止compiler reordering和processor reordering:
- 轻量级同步或围栏指令,我将在future posts;
中讨论- 完整的记忆围栏指令,我demonstrated previously;
- 提供获取或释放语义的内存操作。
获取语义可防止对后续操作进行内存重新排序 它按程序顺序排列,释放语义可防止内存重新排序 之前的操作。这些语义特别适合 在有生产者/消费者关系的情况下,其中一个 线程发布一些信息,另一个读取它。我也会 在未来的帖子中更多地谈论这个。
答案 2 :(得分:3)
如果没有指令的依赖性,如果最终结果不受影响,这些也可能无序执行。您可以在调试以更高优化级别编译的代码时观察到这一点。
答案 3 :(得分:1)
由于A = a;和B = b;在数据依赖性方面是独立的,这应该不重要。如果前一条指令的输出/结果影响后续指令的输入,则排序很重要,否则不重要。这通常是严格顺序执行。
答案 4 :(得分:1)
我的阅读是,这需要通过C ++标准工作;但是,如果您尝试将其用于多线程控制,则它在该上下文中不起作用,因为这里没有任何内容可以保证寄存器以正确的顺序写入内存。
正如您的编辑所示,您正试图将其用于无法正常工作的位置。
答案 5 :(得分:0)
如果你这样做可能会引起人们的兴趣:
{ A=a, B=b; /*etc*/ }
请注意逗号代替分号。
然后C ++规范和任何确认编译器必须保证执行顺序,因为总是从左到右计算逗号运算符的操作数。 这确实可以用来防止优化器通过重新排序来破坏你的线程同步。逗号实际上成为一个障碍,不允许重新排序。