系统崩溃时clflush或clflushopt是原子的吗?

Is clflush or clflushopt atomic when system crash?

一般来说,cacheline是64B,非易失性内存的原子性是8B。

例如:

x[1]=100;
x[2]=100;
clflush(x);

x 是缓存行对齐的,初始设置为 0

系统在 clflush();

时崩溃

重启后是否可以x[1]=0x[2]=100

(另请参阅@Hadi 的回答:x86 TSO 存储排序 保证即使在一行内的持久性排序。 这个答案没有'尝试解决这个问题。根据 Hadi 的回答,我最好的猜测是单个原子存储到缓存行的一个 32 字节的一半将以原子方式持续存在,但这是基于当前硬件的工作方式,将行传输为 2 个 32 字节的一半核心、高速缓存和内存控制器之间。如果这真的很重要,请查找文档或询问英特尔。)


请记住,存储数据 可以 在显式刷新之前自行传播出缓存(进入 DRAM 或 NVDIMM)。

可能发生以下事件序列:

  • x[2]=100;先存储缓存行的第3个字节。 (编译时重新排序:这是一个 C 而不是 asm 问题并且 x 显然是普通的 uint8_t x[64],而不是 _Atomic 或 volatile 所以 x[1]=100;x[2]=100; 不能保证发生按照汇编中的顺序。)
  • 中断到达;在某些时候,包含 x[] 的缓存行被逐出缓存,进入持久域。 (也许在上下文切换到另一个线程之后,许多其他代码在这两个 asm 存储之间运行)。
  • 系统在执行恢复之前崩溃了。 (或者在 x[1]=100; 完成成为耐用之前。)

如果您想依靠 x86 内存排序规则来控制高速缓存行内的持久性顺序,则需要确保 C 遵守该规则。 volatile 会工作,或者 _Atomicmemory_order_release 至少在第二家商店。 (或者更好的是,如果它们在对齐的 8 字节块内,则将它们作为单个存储完成。)(x86 asm 内存模型 = 带有存储缓冲区的程序顺序;没有 StoreStore 重新排序。)

编译时重新排序通常不会无缘无故发生(但它可以);更常见的是由于周围的代码使得这样做很有吸引力。但是周围的代码可能会导致这种情况。 (当然 x[1]=100; / x[2]=0; 可以通过这种机制发生而无需任何编译时重新排序,如果它是 2 个独立的商店。)


我认为持久性原子性的必要先决条件是作为单个原子存储来完成。例如,或者使用一个更宽的 SIMD 存储 1 因为英特尔 CPU 实际上不会将它们分开(但没有书面保证).作为原子wrt。中断(即单个指令)而不是单个存储 uop 使得拆分更难,但仍然完全可能2,因此不能保证安全。例如一个 10 字节的 x87 fstp tbyte 涉及 2 个单独的存储数据 uops 可以被来自另一个核心的无效拆分,即使没有错误共享也是可能的。 (再次参见脚注 2。)

如果 16 字节或更宽的 SIMD 存储没有任何书面原子性保证,您将依赖 SIMD 存储或未对齐存储的实现细节 被拆分.

即使是 ISA 保证的原子性也不够,但是:跨越高速缓存行边界的 lock cmpxchg 仍然保证原子性。其他内核和 DMA 读取器。 (支持这个非常非常慢,不要这样做。)但是没有办法保证这两条线同时变得耐用。但是除了原子性的特殊情况,IDK,我不能排除整行原子性。在 asm 中将一个简单的存储到一行中是原子的将以原子方式变得持久,没有撕裂的机会,这当然是合理的。

单个缓存行内,我不知道。

我猜想 8 字节对齐块中的原子存储将使它以原子方式持久化或根本不持久化,但我没有检查过英特尔的文档。 (实际上,甚至可能是一整行 64 字节的行,您可以将其存储在 AVX512 中)。这个答案的要点是你甚至没有一个原子存储,所以有很多其他机制可以破坏你的测试用例。


脚注 1: 现代英特尔 CPUs 将 SIMD 存储作为单个事务提交到 L1d 缓存,只要它们不跨越缓存行。自从 Sandy/Ivy Bridge 具有全宽 256 位 AVX 执行单元但只有 128 位宽路径 to/from store-buffer-commit 中的负载单元和 AFAIK。 (存储数据执行单元也用了2个周期将32字节的存储数据写入存储缓冲区)。

脚注 2: 对于属于同一指令的单独存储微指令,如 fstp tbyte [rdi],这可能是可能的:

  • 第一部分从存储缓冲区提交到 L1d 缓存

  • RFO 或共享请求到达并在同一指令的第二个存储提交之前处理:该内核的副本现在无效或共享,因此从存储缓冲区到 L1d 的提交被阻止,直到它重新获得独家所有权。一条指令的第二部分存储在存储缓冲区的头部,而不是在连贯缓存中。

  • 另一个正在执行 RFO 的核心跟进他们的存储 clflush,在第一个核心可以取回它并完成提交其他数据之前将此行驱逐到持久内存中来自那条指令。

    另一个核心的 movnti 之类的 NT 存储会在提交 NT 存储的过程中强制逐出该行,就像普通存储 + clflushopt 一样。

    这种情况需要两个线程之间的错误共享,试图在同一行中保留 2 个不同的东西,因此如果您避免错误共享,则可以避免这种情况,例如与填充。 (或者一些疯狂的真实共享,或者在没有先存储的情况下触发 clflush,在其他线程可能正在写入的内存上)。

  • (或者对于软件来说更合理,对于硬件来说更不合理):在第一个写入者取回它之前,该行会自行被驱逐,即使一个核心有一个待处理的 RFO。 (一旦失去所有权,第一个核心就会发出 RFO)。

  • 或在没有虚假共享的情况下完全合理):由于从包容性缓存行中逐出,随时从 L2/L1d 中强制逐出跟踪结构。这可能是由对线路的需求触发的,这些线路只是碰巧为 L3 中的同一组设置了别名,而不是虚假共享。

    Skylake 服务器 (SKX) 具有非包容性 L3,后来的英特尔服务器 CPUs 也是如此。 Cascade Lake (CSX) 是第一个支持持久内存的。尽管它有一个非包容性的 L3,探听过滤器是包容性的,导致逐出的填充冲突确实会导致整个 NUMA 节点的反向失效。

因此无效请求可以在 任何 时间到达,并且核心/存储缓冲区很可能不会在线路上保持更多周期以提交未知数更多商店到同一行。

(到那时,两个存储缓冲区条目都是一条指令的一部分的事实可能已经丢失。访问模式有可能创建一个存储缓冲区条目流,这些条目存储同一缓存行的不同部分无限期地,所以等到“这条线的所有存储都完成”可能会让非特权代码为想要读取它的核心创建拒绝服务。所以我认为 HW 不太可能有一种机制来避免释放所有权来自同一指令的存储之间的缓存行。)

在以下假设下:

  • 我假设您显示的代码表示一系列 x86 汇编指令,而不是尚未编译的实际 C 代码。
  • 我还假设代码是在 Cascade Lake 处理器上执行的,而不是在下一代英特尔处理器上执行的(我认为 CPL 或 ICX with Barlow Pass 支持 eADR,这意味着持久性不需要显式刷新,因为缓存在持久域中)。这个答案也适用于现有的 AMD+NVDIMM 平台。

存储的全局可观察性顺序可能与 Intel x86 处理器上的持久顺序不同。这被称为宽松的持久性。保证顺序相同的唯一情况是 WB 类型的一系列存储到同一缓存行(但到达 GO 的存储并不一定意味着它变得持久)。这是因为 CLFLUSH 是原子的,WB 存储不能在全局可观察性中重新排序。参见:.

如果两个存储跨缓存线边界或者如果存储的有效内存类型是WC:

x86-TSO 内存模型不允许对存储进行重新排序,因此另一个代理不可能在正常操作期间观察到 x[2] == 100x[1] != 100(即,在没有崩溃的情况下处于不稳定状态).但是,如果系统崩溃并重新启动,则持久状态可能为 x[2] == 100x[1] != 100。即使系统在退出 clflush 后崩溃也是可能的,因为 clflush 的退出并不一定意味着刷新的缓存行已到达持久域。

如果你想尽可能地消除它,你可以移动 clflush 如下:

x[1]=100;
clflush(x);
x[2]=100;

clflush 在 Intel 处理器上是针对所有写入进行排序的,这意味着该行保证在任何后续存储变得全局可观察之前到达持久域。请参阅:Persistent Memory Programming Primary (PDF) 和英特尔 SDM V2。第二家商店可以在同一行或任何其他行。

如果您希望 x[1]=100x[2]=100 变得全局可见之前持久化,请在 Intel CSX 上的 clflush 或 AMD 处理器上的 mfence 之后添加 sfenceclflush 仅在 AMD 处理器上由 mfence 排序)。 clflush 本身足以控制持久化顺序。

或者,使用序列clflushopt+sfence(或clwb+sfence)如下:

x[1]=100;
clflushopt(x);
sfence;
x[2]=100;

在这种情况下,如果发生崩溃并且x[2] == 100处于持久状态,则可以保证x[1] == 100clflushopt 本身不会强加任何持久排序。