(另请参阅@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; 在 asm 中按该顺序发生。)
- 中断到达;在某些时候,包含
x[] 的缓存行会一直被逐出缓存,进入持久性域。 (也许在上下文切换到另一个线程之后,这两个 asm 存储之间会运行许多其他代码)。
- 系统在恢复执行之前崩溃。 (或者在
x[1]=100; 变得耐用之前。)
如果您想依靠 x86 内存排序规则来控制高速缓存行内的持久性顺序,则需要确保 C 尊重这一点。 volatile 可以工作,或者_Atomic 和memory_order_release 至少适用于第二家商店。 (或者更好的是,如果它们在对齐的 8 字节块中,则将它们作为单个存储完成。)(x86 asm 内存模型 = 具有存储缓冲区的程序顺序;没有 StoreStore 重新排序。)
编译时重新排序通常不会无缘无故发生(但它可以);更多时候是因为周围的代码使它很有吸引力。但是周围的代码可能会导致这种情况。 (当然x[1]=100; / x[2]=0; 可以通过这种机制发生,而无需任何编译时重新排序,如果它是 2 个单独的商店。)
我认为持久性原子性的必要前提条件是作为单个原子存储完成。例如guaranteed atomic by the ISA,或者使用单个更广泛的 SIMD 存储1,因为实际上英特尔 CPU 不会将它们分开(但没有纸上的保证)。是原子的。中断(即单个指令)但没有单个存储 uop 使得拆分变得更加困难,但仍然完全可能2,因此不能保证安全。例如一个 10 字节 x87 fstp tbyte 涉及 2 个单独的存储数据 uop,可以通过来自另一个内核的失效来拆分,即使没有错误共享也是可能的。 (再次参见脚注 2。)
如果对 16 字节或更宽的 SIMD 存储没有任何纸上原子性保证,您将依赖于 SIMD 存储或未对齐存储的实现细节不被拆分。
即使是 ISA 保证的原子性也不够:跨越高速缓存行边界的lock cmpxchg 仍然保证原子性。其他内核和 DMA 阅读器。 (支持这个非常非常慢,不要这样做。)但是没有办法保证这两条线同时变得持久。但是除了原子性的特殊情况,IDK,我不能排除整行原子性。在 asm 中将普通存储到单行中的原子将变得原子持久,没有撕裂的机会,这当然是合理的。
在单个缓存行内,我不知道。
我猜想在一个 8 字节对齐的块中的原子存储会使其以原子方式持久化或根本不持久化,但我没有检查英特尔的文档。 (实际上甚至可能是一整条 64 字节的行,您可以使用 AVX512 存储)。这个答案的重点是你甚至没有一个原子存储,所以有很多其他机制可以破坏你的测试用例。
脚注 1: 现代英特尔 CPU 将 SIMD 存储作为单个事务提交到 L1d 缓存,只要它们不跨越缓存行。自从 Sandy/Ivy Bridge 具有全宽 256 位 AVX 执行单元但只有 128 位宽路径往返加载单元中的缓存和存储中的 AFAIK 以来,英特尔还没有制造将 SIMD 存储分成两半的 CPU -buffer-commit 的东西。 (存储数据执行单元也用了 2 个周期将 32 字节的存储数据写入存储缓冲区)。
脚注 2:对于像 fstp tbyte [rdi] 中属于同一指令的单独存储微指令,这可能是可能的:
-
第一部分从存储缓冲区提交到 L1d 缓存
-
RFO 或共享请求到达并在同一指令提交的第二个存储之前处理:此核心的副本现在无效或共享,因此从存储缓冲区到 L1d 的提交被阻止,直到它重新获得独占所有权。该指令的第二部分存储在存储缓冲区的头部,而不是在相干缓存中。
-
正在执行 RFO 的另一个核心用 clflush 跟进他们的存储,在第一个核心可以取回它并完成从该一条指令提交其他数据之前将该行驱逐到持久内存。
另一个核心像movnti这样的NT存储将强制驱逐该行,作为提交NT存储的一部分,就像普通存储+ clflushopt一样。
这种情况需要两个线程之间进行错误共享,试图在同一行中持久化 2 个不同的东西,因此如果您避免错误共享,例如,可以避免这种情况。带填充物。 (或者一些疯狂的真正共享,或者在没有先存储的情况下触发clflush,在其他线程可能正在写入的内存上)。
-
(或者对于软件来说更合理,对于硬件来说更不合理):在第一个写入者取回它之前,该行被自己逐出,即使核心有一个未决的 RFO。 (一旦失去所有权,第一个核心就会发出 RFO)。
-
(或完全合理而没有错误共享):由于从包容性缓存行跟踪结构中逐出,随时从 L2/L1d 强制逐出。这可能是由对仅在 L3 中为同一组设置别名而不是错误共享的行的需求触发的。
Skylake-server (SKX) 具有非包容性 L3,后来的 Intel 服务器 CPU 也是如此。 Cascade Lake (CSX) 是第一个支持持久内存的。即使它有一个非包含的 L3,窥探过滤器是包含的,并且导致驱逐的填充冲突确实会导致整个 NUMA 节点的反向失效。
因此,无效请求可以在任何时间到达,并且核心/存储缓冲区很可能不会在更多周期内保持线路以将未知数量的更多存储提交到同一行。
(到那时,两个存储缓冲区条目都是一条指令的一部分这一事实可能会丢失。访问模式可以创建一个存储缓冲区条目流,无限期地存储同一高速缓存行的不同部分,所以等到“这条线的所有存储都完成”可以让非特权代码为想要读取它的核心创建拒绝服务。所以我认为硬件不太可能有一种机制来避免释放缓存的所有权来自同一指令的商店之间的线。)