【问题标题】:Can any reasonable CPU implementation give foo = 2 in this case?在这种情况下,任何合理的 CPU 实现都可以给出 foo = 2 吗?
【发布时间】:2016-01-11 07:43:24
【问题描述】:

阅读了 Dan Luu 撰写的一篇非常有趣的blog post,关于过去几十年 x86 架构的进步,他说:

如果我们将 _foo 设置为 0 并且有两个线程都执行 incl (_foo) 10000 次,用一条指令将同一位置递增 20000 次,保证不超过 20000,但它可以(理论上)是低至 2。如果不清楚为什么理论最小值是 2 而不是 10000,那么弄清楚这一点是一个很好的练习。

_foo 是一些内存地址。

很明显,这是因为(正如他往下说的)incl 被实现为加载,然后是添加,然后是存储。因此,如果您将其“脱糖”为:

mov reg, _foo ;; #1
inc reg       ;; #2
mov _foo, reg ;; #3

那么 u-ops 的以下排序结果为_foo = 2:

Thread A executes #1, #2
Thread B executes #1, #2
Thread A executes #3
Thread B executes #3
Thread A executes #1, #2.... etc

(我可能在这里稍微混淆了汇编程序的细节,但据我所知,这是对_foo = 2 的情况的相当准确的描述。)

我想知道的是他的下一个“锻炼”:

[M] 对您的奖励练习是,任何合理的 CPU 实现都可以得到这个结果,还是规范允许的一些愚蠢的事情永远不会发生?这篇文章中没有足够的信息来回答奖金问题...

可以吗?我的直觉是否定的,因为我相信当 A 执行 #3 时,要么:

  • A 和 B 在同一个 CPU 上。在 A 的时间片结束之前,B 不会运行,并且不可能花费整个时间片来执行一条指令,所以最终有人会写出一个大于 2 的值,或者

    李>
  • A 和 B 位于不同的 CPU 上。 A 的写操作导致 B 的缓存失效,A 继续执行,写出大于 2 的值。

但是,如果每个存储都导致所有其他缓存失效,或者 A 是否能够在此期间继续运行,我不肯定,并且我不确定操作系统级别的东西是否应该适用于时间片的推理CPU。

【问题讨论】:

  • 你的问题被“合理”搞糊涂了。您认为合理的 CPU 是多少?例如,当 CPU 乱序执行指令时,它是否合理?因为大多数现代 CPU 都是这样做的。或者当不同的内核具有不同的速度或某些内核被关闭时,CPU是否合理?因为 CPU 也这样做。
  • @SedatKapanoglu 该博客的作者为 MS 和 Google 进行硬件设计,所以我希望他对“合理”的定义能够非常接近大多数现代 CPU 所做的事情。还值得注意的是,我并不是在寻找对我来说似乎“合理”的答案,我想知道原作者的意图是什么,因为他是该领域的专家。
  • 现代 CPU 并不合理。相反,他们现在做了我上面提到的所有疯狂的事情。他的意思一定是“没有错误”或类似的意思。 (我以为是你对不起)。
  • 同样的 CPU 情况甚至没有那么有趣。 INC指令中途不能中断,保证结果始终为20000。

标签: assembly concurrency operating-system race-condition


【解决方案1】:
A executes #1, #2
B executes #1, #2, #3 9999 times  (_foo == 9999)
A executes #3    (_foo == 1)
B executes #1, #2   (part of iteration 10000, and reg == 2)
A executes #1, #2, #3 9999 times  (completing its total of 10000 iterations)
B executes #3  (writing 2 to _foo)

【讨论】:

  • 这不是我想过的执行,但我不太确定它回答了问题的“任何合理的 CPU 实现”方面。是否有任何类型的相关规范、标准或其他“基本事实”可以说明这种情况?
  • 特别是,我想知道“B 执行#1、#2、#3,A 执行#3;B 执行#1、#2...”实际上不是不可能的,因为 A 写使 B 的缓存无效,从而阻止 B 在至少几十个周期内执行其他任何操作。这就是我所说的“合理实现”可能与规范不同的地方,因为规范不会考虑缓存失效需要多长时间。
  • @PatrickCollins:如果这是一系列 3 个 x86 指令执行 load / inc reg / store,那么很容易看出这是如何发生的:中断随时可能出现,即使一个线程刚刚开始执行。即使在单核系统中,上下文切换也可以产生这种情况。但是,由于这是inc [mem],因此该指令要么退出,要么不退出,即使它解码为 3 uop(英特尔 SnB 系列)。在加载完成但不是存储的情况下,中断不能暂停/恢复执行。但是,x86 允许 StoreLoad 重新排序(即您获得过时的预取数据)。
  • @PeterCordes 我不太理解你所说的关于 StoreLoad 重新排序的内容——它是阻止这篇文章中的例子,还是允许它?我真的很想得到一个与规范或真实处理器的内存模型相关的答案,听起来你可以按照这些思路给出答案。
  • 见preshing.com/20120515/memory-reordering-caught-in-the-act。您不会总是从加载中获取陈旧数据,因此 A 可以通过提早加载将 _foo 从 9999 恢复到 1,但也可以让 B 在加载时看到 A 的 1 存储为其最后一次迭代执行加载。中断可以解释1 变得全局可见,而不是在其他 CPU 看到之前在 L1 缓存行中被覆盖。
【解决方案2】:

tl;dr 摘要:不可能在单核上使用单指令inc [foo]。也许每个线程都在自己的核心上是可能的,但我认为只有通过超线程来通过导致加载/inc和存储之间的缓存驱逐来在存储上创建额外的延迟。

我认为即使是多套接字缓存一致性也不会慢到 B 的最终存储在 B 的最终加载后延迟 50k 周期,但超线程可能能够在它之前排队多个缓存/TLB 未命中。


在单核情况下:您对 B 在 A 的时间片结束之前不会运行 的假设不一定成立。中断(例如定时器中断或 NIC)可以在任何时候进入,在任何指令边界暂停用户空间线程的执行。也许在中断之后,更高优先级的进程会被唤醒并被调度到 CPU 上一段时间,因此调度程序没有理由更喜欢已经运行了一小部分时间片的线程。

但是,如果我们只讨论单核情况,并发只能通过上下文切换发生,inc [mem] 与mov reg, [mem] / inc reg / mov [mem], reg 有很大不同。不管 CPU 内部如何处理inc [mem],上下文切换只会保存/恢复架构状态。如果 load 和 inc 部分已经在内部完成,而不是 store,则整个指令不可能退出。上下文切换不会保存/恢复该进度:当线程再次开始执行并且 CPU 再次看到 inc [mem] 指令时,必须重新运行 load 和 inc。

如果测试使用了单独的 load/inc/store 指令,即使是单核机器理论上也可以按照 Michael Burr 指出的顺序得到2:

A loads 0 from _foo
B loops 9999 times (finally storing _foo = 9999)
A stores _foo = 1  (end of first iteration)
B's final iteration loads 1 from _foo
A loops 9999 times (eventually storing _foo = 10000)
B's final iteration stores _foo = 2

这是可能的,但需要由在特定时间到达的中断触发多次上下文切换。从导致调度程序抢占线程的中断到新线程的第一条指令实际运行时需要许多周期。可能有足够的时间让另一个中断到达。我们只是对它的可能性感兴趣,即使经过几天的试验也不太可能被观察到!

同样,对于inc [mem],这在单个内核上是不可能的,因为上下文切换只能在整个指令之后发生。 CPU 的架构状态要么执行了inc,要么没有。


在多核情况下,两个线程同时运行,情况就完全不同了。缓存一致性操作可能发生在单条指令解码成的微指令之间。所以inc [mem] 在这种情况下不是一个单一的操作。

对此我不确定,但我认为即使是单指令 inc [foo] 循环也有可能产生最终结果 2。中断/上下文切换不能解释加载和存储之间的延迟,但是,所以我们需要想出其他可能的原因。

  • A 从 foo 加载 0
  • B 循环 9999 次(最终存储 foo = 9999)。缓存行现在在 B 的 CPU 内核上处于 E 状态
  • A 存储 _foo = 1(第一次迭代结束)。可以想象,这可以在超线程 CPU 上延迟这么长时间,因为另一个逻辑线程用大量积压的存储使存储端口饱和,这些存储在缓存和/或 TLB 中未命中,并且存储被缓冲了一段时间。这可能在没有超线程的情况下发生,并且有几个缓存未命中存储等待完成。请记住,在 x86 的强内存模型中,存储在程序顺序中变得全局可见,因此稍后存储到热缓存行仍然需要等待。及时完成 B 的最后一次迭代只是时间上的巧合,这很好。
  • B 的最终迭代从 foo 加载 1。 (中断或其他事情可能会延迟 B 执行最终迭代。这不需要在单个指令的加载/增加/存储 uops 之间发生任何事情,所以我不需要弄清楚是否接收到一致性消息(来自 A) 的缓存行无效将阻止存储转发将 9999 值从上一次迭代的存储转发到本次迭代的负载。我不确定,但我认为它可以。)
  • A 循环 9999 次(最终存储 _foo = 10000)
  • B 的最终迭代存储_foo = 2。 解释如何将这家商店延迟到 A 的循环完成之后似乎是最大的延伸。超线程可以做到这一点:另一个逻辑核心可以驱逐 _foo 的 TLB 条目,也可能驱逐包含该值的 L1 D$ 行。对于最终的inc 指令,这种驱逐可能发生在加载和存储微指令之间。我不确定一致性协议需要多长时间才能获得对当前由另一个核心拥有的缓存行的写访问权限。我确信它通常远少于 50k 周期,实际上少于 CPU 上的主内存访问,包括作为一致性流量支持的最后一级缓存(例如 Intel 的 Nehalem 和后来的设计)。具有多个套接字的非常多核系统可能会很慢,但我认为它们仍然使用环形总线来实现一致性流量。

    我不确定 B 的最终存储是否有可能在没有超线程的情况下延迟 50k 周期来堆积一些存储端口争用并导致缓存驱逐。负载(必须看到 A 的 1 存储,但不能看到 A 的任何其他存储)在 OOO 调度程序中不能超过存储,因为它仍然必须在倒数第二次迭代的存储之后。 (核心必须在单个执行上下文中维护有序语义。)

由于只有一个内存位置在两个线程中被读取然后写入,因此不会对存储和加载进行任何重新排序。加载将始终看到来自同一线程的先前存储,因此只有在存储到同一位置之后才能全局可见。

在 x86 上,只有 StoreLoad reordering 是可能的,但在这种情况下,唯一重要的是,无序的机器可能会延迟存储很长时间,即使没有相对于任何负载对其进行重新排序。



您所指的原始博客文章总体上看起来不错,但我确实注意到至少一个错误。里面有很多不错的链接。

事实证明,在现代 x86 CPU 上,使用锁定来实现 并发原语是often cheaper than using memory barriers

该链接仅表明使用lock add [mem], 0 作为障碍 在 Nehalem 上更便宜,尤其是。它可以更好地与其他指令交错。使用锁定与依赖于障碍的无锁算法没有什么可说的。如果您想原子地增加内存位置,那么到目前为止最简单的选择是locked 指令。如果可能的话,只使用MFENCE 将需要在没有原子 RMW 操作的情况下实现某种单独的锁。

显然他想介绍lock inc [mem] 与inc [mem] 的话题,只是对措辞不小心。在大多数情况下,他的概括效果更好。


示例代码也很奇怪,和往常一样用-O0makes quite nasty code编译。我修复了内联汇编以向编译器询问内存操作数,而不是手动编写incl (reg),因此通过优化,它会生成incl counter(%rip),而不是将指针加载到寄存器中。更重要的是,-O3 还避免将循环计数器保留在内存中,即使使用原始源也是如此。原始源代码上的-O3 似乎仍然可以生成正确的代码,即使它没有告诉编译器它写入内存。

无论如何,尽管实验存在缺陷,但我认为该实验仍然有效,而且使用 -O0 编译的巨大循环开销不太可能对最终计数器的最终范围进行人为限制。


Dan Luu 的示例 asm 语法是 Intel 和 AT&T 语法的奇怪组合:mov [_foo], %eax 是一个负担。它应该写成mov eax, [_foo],或者mov _foo, %eax,或者mov (_foo), %eax,如果你想明确它是一个负载而不是一个mov-immediate。无论如何,如果我还不知道他的意思并试图证明的话,我认为这会令人困惑。


【讨论】:

  • 太棒了,这就是我所希望的。我怀疑他期望的答案是“不”,那么您对这篇文章的评论也很有帮助。谢谢!
  • @PatrickCollins:我可能缺少一些东西。我很想看看 Dan Luu 的官方答案,如果他曾经发布过的话。我怀疑你是对的,他期待的是“不”的答案,尤其是。因为我什至不确定超线程是否会导致单个inc 的加载和存储之间出现 50k 周期延迟。哦,嗯,​​我想知道 CPU 频率之间的转换(例如从 1.6GHz 上升到 3.3GHz)是否会在不刷新 OOO 管道的情况下导致很大的延迟。这当然是合理的,因为它必须等待新的电源电压稳定下来才能再次启动时钟。
猜你喜欢
  • 2018-10-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-05
相关资源
最近更新 更多