对于{A = a; B = B; ,"A = a"会在"B = b"之前严格执行吗?

ACc*_*tor 51 c c++ compiler-construction optimization standards

假设A,B,a,和b都是变量,的地址A,B,a,和b都是不同的.然后,对于以下代码:

A = a;
B = b;
Run Code Online (Sandbox Code Playgroud)

C和C++标准是否明确要求A=a在之前严格执行B=b?考虑到的地址A,B,a,和b都不同,编译器允许交换两个语句的执行顺序为某种目的,如优化?

如果我的问题的答案在C和C++中有所不同,我想知道两者.

编辑:问题的背景如下.在棋盘游戏AI设计中,对于优化,人们使用无锁共享哈希表,如果我们不添加volatile限制,其正确性很大程度上取决于执行顺序.

Dav*_*nan 56

这两个标准都允许这些指令不按顺序执行,只要这不会改变可观察的行为.这被称为as-if规则:

请注意,正如评论中指出的那样,"可观察行为"的含义是具有已定义行为的程序的可观察行为.如果您的程序有未定义的行为,那么编译器就可以免于推理.

  • 此外,如果它们不影响程序的可观察行为,则两者都不能执行.(即完全优化) (3认同)
  • 在这种情况下,我认为值得强调(如G_G所暗示的)"似乎"要求是具有已定义行为*的程序的可观察行为*不会改变.提问者不得将此答案表示保证指令顺序的任何更改都不会改变其无锁哈希表的行为.事实上,如果他问这个问题,那是因为代码包含数据竞争,所以它的行为没有定义,并且可能由于优化,调度事故等等而很好地改变. (3认同)
  • 值得指出的是,如果变量是易失性的,那么访问或修改变量只会被视为"可观察行为". (2认同)

Sha*_*our 25

编译器只有义务模拟程序的可观察行为,因此如果重新排序不违反该原则,那么它将被允许.假设行为定义良好,如果您的程序包含未定义的行为(如数据竞争),则程序的行为将是不可预测的,并且评论将需要使用某种形式的同步来保护关键部分.

一个有用的参考

一篇有趣的文章介绍了编译时的内存排序,它说:

内存重新排序的基本规则,编译器开发人员和CPU供应商普遍遵循,可以表达如下:

你不应该修改单线程程序的行为.

一个例子

本文提供了一个简单的程序,我们可以看到这个重新排序:

int A, B;  // Note: static storage duration so initialized to zero

void foo()
{
    A = B + 1;
    B = 0;
}
Run Code Online (Sandbox Code Playgroud)

并且在B = 0之前完成了更高优化级别的显示A = B + 1,并且我们可以使用godbolt重现此结果,使用时它会-O3产生以下内容(请参见实时):

movl    $0, B(%rip) #, B
addl    $1, %eax    #, D.1624
Run Code Online (Sandbox Code Playgroud)

为什么?

为什么编译器重新排序?文章解释了这与处理器完全相同的原因,因为架构的复杂性:

正如我在开始时提到的,编译器修改了内存交互的顺序,原因与处理器的相同 - 性能优化.这种优化是现代CPU复杂性的直接结果.

标准

在C++标准草案中,这将在1.9 程序执行部分中介绍(强调我的未来):

本国际标准中的语义描述定义了参数化的非确定性抽象机器.本国际标准对符合实施的结构没有要求.特别是,它们不需要复制或模拟抽象机器的结构.相反,需要符合实现来模拟(仅)抽象机器的可观察行为,如下所述.

脚注5告诉我们这也被称为as-if规则:

这项规定有时被称为"假设"规则,因为只要结果就像是遵守了要求,只要可以从可观察的行为中确定,实施就可以自由地忽视本国际标准的任何要求.该计划.例如,实际实现不需要评估表达式的一部分,如果它可以推断出它的值没有被使用,并且没有产生影响程序的可观察行为的副作用.

C99草案和C11标准草案在5.1.2.3 程序执行部分涵盖了这一点,尽管我们必须转到索引,看它在C标准中也被称为as-if规则:

as-if规则,5.1.2.3

关于无锁注意事项的更新

" 无锁编程简介 "一文很好地介绍了这个主题,对于无锁共享哈希表实现的OP问题,本节可能是最相关的:

记忆订购

如流程图所示,无论何时对多核(或任何对称多处理器)进行无锁编程,并且您的环境不保证顺序一致性,您必须考虑如何防止 内存重新排序.

在今天的体系结构中,强制执行正确内存排序的工具通常分为三类,这会阻止编译器重新排序处理器重新排序:

  • 一个轻量级的同步或围栏指令,我将在以后的帖子中讨论 ;
  • 一个完整的记忆围栏指令,我之前已经证明过 ;
  • 提供获取或释放语义的内存操作.

获取语​​义可防止按程序顺序对其后面的操作进行内存重新排序,并且释放语义可防止对其前面的操作进行内存重新排序.这些语义特别适用于存在生产者/消费者关系的情况,其中一个线程发布一些信息而另一个线程读取它.我将在以后的文章中再讨论这个问题.