C11 内存模型真的与常见的优化冲突吗?

Does the C11 memory model really conflict with common optimizations?

C11语言规范a recent question added a comment to it linking a paper entitled Common Compiler Optimisations are Invalid in the C11 Memory Model and what we can do about it, which apparently was presented at POPL 2015. Among other things, it purports to show several unexpected and counterintuitive conclusions derived from the specifications for what it calls the "C11 memory model", which I take to consist largely of the provisions of section 5.1.2.4的OP

这篇论文有些冗长,但出于本题的目的,我将重点放在第二页对方案“SEQ”的讨论上。这涉及一个多线程程序,其中...

... 并出现以下情况(从伪代码音译):


线程 1

a = 1;

线程 2

if (atomic_load_explicit(&x, memory_order_relaxed))
    if (a)
        atomic_store_explicit(&y, 1, memory_order_relaxed);

线程 3

if (atomic_load_explicit(&y, memory_order_relaxed))
    atomic_store_explicit(&x, 1, memory_order_relaxed);

论文作此论证:

First, notice that there is no execution (consistent execution in the terminology of Section 2) in which the load of a occurs. We show this by contradiction. Suppose that there is an execution in which a load of a occurs. In such an execution the load of a can only return 0 (the initial value of a) because the store a=1 does not happen before it (because it is in a different thread that has not been synchronised with) and non-atomic loads must return the latest write that happens before them. Therefore, in this execution the store to y does not happen, which in turn means that the load of y cannot return 1 and the store to x also does not happen. Then, x cannot read 1, and thus the load of a does not occur. As a consequence this program is not racy: since the load of a does not occur in any execution, there are no executions with conflicting accesses on the same non-atomic variable. We conclude that the only possible final state is a=1 ∧ x=y=0.

(问题 1)但是这个论点不是有致命的缺陷吗?

a 的负载只能 return 0 的断言是基于 a 实际上被读取的假设,该论证旨在反驳这一假设。但在那种情况下,正如本文所观察到的,在线程 1 中存储到 a 和线程 2 中从 a 加载之间没有 happens before 关系.这些都是冲突访问,都不是atomic,一个是write。因此,根据第 5.1.2.4/25 段,该程序包含导致未定义行为的数据竞争。因为行为是未定义的,所以关于线程 2 从 a 加载的值没有任何结论,特别是,不能从规范中得出加载必须 return 0 的结论。其余的论点然后崩溃。

虽然该论文声称该论点表明该程序不包含数据竞争(“不活泼”),但实际上这不是该论点的结果,而是一个隐藏的假设。只有与 5.1.2.4/25 相反,该程序不包含数据竞争时,该论点才能站得住脚。

现在也许关键是上面的论点只考虑了“一致的执行”,一个在本文后面的部分定义的术语。我承认在这一点上它对我来说有点深了,但如果事实上将行为限制为一致的执行足以支持 a 的负载必须 return 0 的断言,那么它似乎它不再(只是)我们正在谈论的 C11 内存模型的规则。

这很重要,因为作者得出结论,源到源的转换结合线程 1 和 2 产生......


线程 2'

a = 1;
if (atomic_load_explicit(&x, memory_order_relaxed))
    if (a)
        atomic_store_explicit(&y, 1, memory_order_relaxed);

... 不允许 C11 内存模型实现,因为它允许最终状态为 a = x = y = 1 的执行。这个和其他各种代码转换和优化是无效的是论文的论点。

(问题 2)但是 C11 实现将原始的三线程程序视为由线程 2' 和线程 3 组成的双线程程序是否确实有效?

如果这允许“一致执行”并产生原始程序的任何一致执行都无法产生的结果,那么我认为这只是表明 C11 不限制程序表现出一致的执行。我在这里遗漏了什么吗?

显然,没有人既有足够的兴趣又有足够的信心来写一个答案,所以我想我会继续。

isn't that argument fatally flawed?

就论文中引用的证明旨在证明不允许符合标准的 C 实现来执行问题中描述的源到源转换或等效项而言,是的,证明是有缺陷的。问题中的反驳是正确的。

评论中有一些关于如何将反驳视为归结为在未定义行为的情况下允许的任何事情的讨论。这是一个有效的观点,绝不会削弱论点。但是,我认为这是不必要的极简主义。

同样,论文证明的关键问题在这里:

the load of a can only return 0 (the initial value of a) because the store a=1 does not happen before it (because it is in a different thread that has not been synchronised with) and non-atomic loads must return the latest write that happens before them.

证明的错误是语言规范要求 a 的读取必须 return 写入 a 的结果“发生在”之前程序没有数据竞争。这是整个模型的重要基础,而不是某种逃生舱口。如果实际上执行了 a 的读取,则该程序显然不是没有数据竞争,因此在这种情况下要求没有实际意义。线程2对a的读完全可以观察到线程1的写,并且有充分的理由假设它有时可能会在实践中这样做。

换个角度来看,证明选择关注写在读之前没有发生,而忽略了读也没有在写之前发生的事实。

考虑宽松的原子访问并没有改变任何东西。在本文的三线程程序的实际执行中,实现(例如)推测性地在线程 2 中执行 x 的松弛负载是合理的,假设它将 return 1,然后从 a 读取线程 1 写入的值,结果执行到 y 的存储。因为原子访问是用宽松的语义执行的,所以线程 3 的执行可以将 y 的值读取为 1(或推测它会这样做),然后执行对 x 的写入。所有的猜测都可以被证实是正确的,最后的结果是a = x = y = 1。这个看似矛盾的结果是有意为之的,是“宽松”的记忆顺序允许的。

isn't it indeed valid for a C11 implementation to treat the original three-threaded program as if it were the two-threaded program consisting of threads 2' and 3?

至少,即使我们——在规范中没有依据——将数据竞争引起的 UB 的范围解释为仅限于从 a是它的初始一个还是线程1写的那个。

实现被授予广泛的许可,可以按照他们选择的方式行事,只要它们产生与抽象机器所需的行为一致的可观察行为。多个执行线程的创建和执行本身并不是规范所定义的程序可观察行为的一部分。因此,是的,一个程序执行了建议的转换然后相应地表现,或者一个表现得好像在写入 a 和 之间有一个 a 读取,不会与规范不一致。