为什么内存重新排序在单个 core/processor 机器上不是问题?

Why memory reordering is not a problem on single core/processor machines?

考虑以下取自维基百科的示例,稍作改编,其中程序的步骤对应于单独的处理器指令:

x = 0;
f = 0;

Thread #1:
   while (f == 0);
   print x;

Thread #2: 
   x = 42;
   f = 1;

我知道当线程 运行 在两个不同的物理 cores/processors 上时,print 语句可能会打印不同的值(42 或 0) -订单执行。

但是我不明白为什么这在单核机器上不是问题,因为这两个线程 运行 在同一个核心上(通过抢占)。根据Wikipedia:

When a program runs on a single-CPU machine, the hardware performs the necessary bookkeeping to ensure that the program executes as if all memory operations were performed in the order specified by the programmer (program order), so memory barriers are not necessary.

据我所知,单核 CPU 也会对内存访问进行重新排序(如果它们的内存模型较弱),那么如何确保程序顺序得以保留?

CPU 不会意识到这是两个线程。线程是一种软件构造 (1)。

因此 CPU 会按以下顺序看到这些说明:

store x = 42
store f = 1
test f == 0
jump if true ; not taken
load x

如果 CPU 将 x 的存储重新排序到末尾,加载后,它会改变结果。虽然 CPU 允许乱序执行,但它仅在不更改结果时执行此操作。如果允许这样做,几乎每个指令序列都可能失败。不可能产生一个工作程序。

在这种情况下,不允许单个 CPU 对同一地址加载后的商店重新排序。至少,就 CPU 可以看出它没有重新排序。就 L1、L2、L3 缓存和主内存(以及其他 CPUs!)而言,可能尚未提交存储。

(1) 像 HyperThreads 这样的东西,每个内核两个线程,在现代 CPUs 中很常见,不会算作 "single-CPU" w.r.t。你的问题。

CPU 不知道也不关心“上下文切换”或软件线程。它所看到的只是一些存储和加载指令。 (例如,在 OS 的上下文切换代码中,它保存旧的寄存器状态并加载新的寄存器状态)

乱序执行的基本规则是它不能破坏单个指令流。代码必须运行就像 每条指令按程序顺序执行,其所有副作用在下一条指令开始之前完成。这包括单个内核上线程之间的软件上下文切换。例如进程中的单核机器或绿色线程。

(通常我们将此规则声明为不破坏单线程代码,理解其确切含义;只有当 SMP 系统从其他内核存储的内存位置加载时才会发生怪异)。

As far as I know single-core CPUs too reorder memory accesses (if their memory model is weak)

但请记住,其他线程不会直接使用逻辑分析器观察内存,它们只是运行在正在执行和跟踪重新排序的同一个 CPU 内核上加载指令。

如果您正在编写设备驱动程序,是的,您可能必须在存储之后实际使用内存屏障以确保在从另一个 MMIO 位置加载之前,它实际上 对片外硬件 可见。

或者在与 DMA 交互时,确保数据实际上 在内存中 ,而不是在 CPU-私有回写缓存中,这可能是个问题。此外,MMIO 通常在不可缓存的内存区域中完成,这意味着强内存排序。 (x86 具有缓存一致的 DMA,因此您不必实际刷新回 DRAM,只需使用 x86 mfence 等等待存储缓冲区耗尽的指令确保其全局可见即可。但是一些非 x86 OSes 从一开始就设计了缓存控制指令确实需要 OSes 知道它。即确保缓存在从磁盘读取新内容之前无效,并确保在要求设备从页面读取之前,它至少被写回到 DMA 可以读取的地方。)

顺便说一句,即使是 x86 的“强”内存模型也只是 acq/rel,而不是 seq_cst(完全障碍的 RMW 操作除外)。 (或者更具体地说,). Stores can be delayed until after later loads. (StoreLoad reordering). See https://preshing.com/20120930/weak-vs-strong-memory-models/

so what makes sure the program order is preserved?

硬件依赖跟踪; 加载 窥探存储缓冲区 以查找来自最近存储到 的位置的负载。这确保加载从最后一个程序顺序写入数据到任何给定的内存位置1.

没有这个,代码像

  x = 1;
  int tmp = x;

可能会加载 x 的陈旧值。如果您必须在每个存储之后放置内存屏障以供 您自己的 重新加载以可靠地查看存储的值,那将是疯狂和无法使用的(并且会降低性能)。

我们需要所有指令 运行ning 在单个内核上,以根据 ISA 规则在程序顺序上给出 运行ning 的错觉。只有 DMA 或其他 CPU 内核可以观察到重新排序。


脚注 1: 如果还没有旧商店的地址,CPU 甚至可以 推测 它将指向不同的地址并从缓存中加载,而不是等待存储指令的存储数据部分执行。如果它猜错了,它将不得不回滚到已知的良好状态,就像分支预测错误一样。 这被称为 "memory disambiguation". See also Store-to-Load Forwarding and Memory Disambiguation in x86 Processors 用于技术观察,包括从更广泛的商店的一部分重新加载的情况,包括未对齐和可能跨越缓存行边界...