【问题标题】:For { A=a; B=b; }, will "A=a" be strictly executed before "B=b"?对于 { A=a;乙=乙; },“A=a”会在“B=b”之前严格执行吗?
【发布时间】:2014-09-15 11:46:06
【问题描述】:

假设ABab都是变量,ABab的地址都是不同的。然后,对于以下代码:

A = a;
B = b;

C 和 C++ 标准是否明确要求在 B=b 之前严格执行 A=a?鉴于ABab的地址都不同,是否允许编译器为了优化等目的交换两条语句的执行顺序?

如果我的问题在 C 和 C++ 中的答案不同,我都想知道。

编辑:问题的背景如下。在棋盘AI设计中,为了优化人们使用lock-less shared-hash table,如果我们不添加volatile限制,其正确性很大程度上取决于执行顺序。

【问题讨论】:

  • 即使保证编译器按该顺序生成代码,CPU 本身也会乱序执行。
  • 不仅编译器被允许这样做,CPU被允许这样做,内存控制器被允许这样做,缓存被允许这样做,等等。
  • 任何时候你在做多线程,你都会完全进入另一个维度。即使代码是按顺序执行的,您也不能保证(没有进一步的控制)从另一个处理器来看,执行将显示为按顺序执行。如果您尝试执行类似于共享哈希表的操作,则需要花费大量时间研究同步问题。
  • @ACcreator:是的,这是可能的,具体取决于使用的特定缓存一致性协议。例如,x86 提供比 Itanium 更强的保证。对于多线程,您还需要关注撕裂和推测性写入。
  • @ACcreator:使用内存栅栏。这比临界区便宜,但仍确保缓存必须以正确的顺序同步。

标签: c++ c optimization compiler-construction standards


【解决方案1】:

这两个标准都允许这些指令乱序执行,只要这不会改变可观察到的行为。这就是所谓的 as-if 规则:

请注意,正如 cmets 中所指出的,“可观察行为”是指具有已定义行为的程序的可观察行为。如果你的程序有未定义的行为,那么编译器就不会对此进行推理。

【讨论】:

  • 此外,如果它们不影响程序的可观察行为,则两者都无法执行。 (即完全优化)
  • 可能值得指出的是,如果变量是 volatile 的,访问或修改变量才算作“可观察行为”。
  • @DavidHeffernan:是的,我应该更准确,抱歉。我的意思是“访问或修改一个基本类型的变量”。当然,用户定义的操作可以有可观察的行为。
  • 它肯定会改变编写不佳的多线程程序中的可观察行为!大声笑!
  • 在这种情况下,我认为值得强调(正如 G_G 所暗示的那样)“好像”要求是具有已定义行为的程序的可观察行为不会改变.提问者不能认为这个答案意味着任何指令顺序的改变都保证不会改变他的无锁哈希表的行为。事实上,如果他问这个问题,那是因为该代码包含数据竞争,所以它的行为没有定义,并且很可能由于优化、调度事故等等而发生变化。
【解决方案2】:

编译器只负责模拟程序的可观察行为,因此如果重新排序不会违反该原则,那么它就会被允许。假设行为已明确定义,如果您的程序包含 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 = 0A = 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 规则

此规定有时称为“假设”规则,因为 实施可以无视任何要求 国际标准只要结果符合要求 已被遵守,只要可以从可观察到的 程序的行为。例如,实际的实现需要 如果可以推断出表达式的值是,则不计算表达式的一部分 未使用,并且没有影响可观察行为的副作用 程序制作完成。

C99 草案和 C11 标准草案在 5.1.2.3 部分中对此进行了说明程序执行,尽管我们必须去索引才能看到它被称为 as-if 规则 em> 在 C 标准中也是如此:

as-if 规则,5.1.2.3

关于无锁注意事项的更新

An Introduction to Lock-Free Programming 文章很好地涵盖了这个主题,对于 OP 对 无锁共享哈希表 实现的关注,这部分可能是最相关的:

内存排序

正如流程图所示,任何时候你为 多核(或任何symmetric multiprocessor),而您的环境确实如此 不能保证顺序一致性,必须考虑如何防止 memory reordering.

在当今的架构中,强制执行正确内存排序的工具 一般分为三类,防止compiler reorderingprocessor reordering

  • 轻量级同步或围栏指令,我将在future posts 中讨论;
  • 一个完整的内存围栏指令,我有 demonstrated previously;
  • 提供获取或释放语义的内存操作。

获取语义可防止后续操作的内存重新排序 它按程序顺序,并释放语义防止内存重新排序 之前的操作。这些语义特别适合 在存在生产者/消费者关系的情况下,其中一个 线程发布一些信息,另一个读取它。我也会 在以后的帖子中详细讨论这个问题。

【讨论】:

  • 这只是让我想知道,为什么 GCC asm 生成 addl $1, %eax 而不是 incl %eax?即使对于a++,它也只会产生a += 1 ... ICC 的行为与预期相同。
  • @vaxquis 我不清楚,似乎是某种形式的优化尝试,可能取决于gcc 所做的假设。
【解决方案3】:

如果指令没有依赖关系,那么在不影响最终结果的情况下,这些指令也可能会乱序执行。您可以在调试以更高优化级别编译的代码时观察到这一点。

【讨论】:

    【解决方案4】:

    因为 A = a;和 B = b;在数据依赖性方面是独立的,这无关紧要。如果上一条指令的输出/结果影响了后续指令的输入,那么排序很重要,否则不重要。这通常是严格的顺序执行。

    【讨论】:

      【解决方案5】:

      我的阅读是,这是 C++ 标准所要求的;但是,如果您尝试将其用于多线程控制,则它在该上下文中不起作用,因为这里没有任何东西可以保证寄存器以正确的顺序写入内存。

      正如您的编辑所表明的那样,您正试图在它不起作用的地方使用它。

      【讨论】:

        【解决方案6】:

        如果你这样做可能会很有趣:

        { A=a, B=b; /*etc*/ }
        

        注意逗号代替分号。

        然后 C++ 规范和任何确认编译器都必须保证执行顺序,因为逗号运算符的操作数总是从左到右计算。 这确实可以用来防止优化器通过重新排序来破坏你的线程同步。逗号实际上成为了不允许重新排序的障碍。

        【讨论】:

        • 除了逗号运算符的使用有问题之外,这并没有解决 OP 对问题的编辑。
        猜你喜欢
        • 2022-01-27
        • 2013-08-27
        • 2014-03-29
        • 2013-06-25
        • 2021-11-28
        • 2013-11-19
        • 1970-01-01
        • 2019-10-27
        • 2010-12-08
        相关资源
        最近更新 更多