作者:accera_928 | 来源:互联网 | 2023-06-12 20:13
通常,缓存行是 64B,但非易失性内存的原子性是 8B。例如:x[1]=100;x[2]=100;clflush(x);x缓存行对齐,并且最初设置为0。系统崩溃 clflush();是否有可能x[1]
通常,缓存行是 64B,但非易失性内存的原子性是 8B。
例如:
x[1]=100;
x[2]=100;
clflush(x);
x
缓存行对齐,并且最初设置为0
。
系统崩溃 clflush();
是否有可能x[1]=0
,x[2]=100
重新启动后?
回答
在以下假设下:
- 我假设您显示的代码表示一系列 x86 汇编指令,而不是尚未编译的实际 C 代码。
- 我还假设代码是在 Cascade Lake 处理器上执行的,而不是在下一代 Intel 处理器上执行的(我认为带有 Barlow Pass 的 CPL 或 ICX 支持 eADR,这意味着持久化不需要显式刷新,因为缓存位于持久域)。这个答案也适用于现有的 AMD+NVDIMM 平台。
存储的全局可观察性顺序可能与 Intel x86 处理器上的持久顺序不同。这称为松弛持久性。唯一保证顺序相同的情况是将 WB 类型的存储序列放入同一缓存行(但到达 GO 的存储并不一定意味着它变得持久)。这是因为CLFLUSH
原子性和 WB 存储不能在全局可观察性中重新排序。请参阅:在 x86-64 上,系统崩溃时“movnti”或“movntdq”指令是原子的吗?.
如果两个存储跨越缓存线边界或者如果存储的有效内存类型是 WC:
在x86-TSO内存模型不允许重新排序店,所以它的另一个代理观察是不可能x[2] == 100
与x[1] != 100
正常工作期间(即,在动荡的状态,而不会崩溃)。但是,如果系统崩溃并重新启动,则持久状态可能是x[2] == 100
和x[1] != 100
。即使系统在退休后崩溃也是可能的,clflush
因为退休clflush
并不一定意味着刷新的缓存行已经到达持久域。
如果您想消除这种可能性,您可以clflush
按以下方式移动:
x[1]=100;
clflush(x);
x[2]=100;
clflush
英特尔处理器上的所有写入都是按顺序排列的,这意味着该行保证在任何后续存储变为全局可见之前到达持久域。请参阅:持久内存编程主要 (PDF)和英特尔 SDM V2。第二家商店可以在同一行或任何其他行。
如果您想x[1]=100
在x[2]=100
全局可观察之前变得持久,请在 Intel CSX 或AMD 处理器上添加sfence
after (仅在 AMD 处理器上排序)。本身足以控制持久顺序。clflush
mfence
clflush
mfence
clflush
或者,使用序列clflushopt+sfence
(或clwb+sfence
) 如下:
x[1]=100;
clflushopt(x);
sfence;
x[2]=100;
在这种情况下,如果发生崩溃并且x[2] == 100
处于持久状态,则可以保证x[1] == 100
. clflushopt
本身不会强加任何持久排序。
回答
(另请参阅@Hadi 的回答:x86 TSO 存储排序即使在一行内也不能保证持久性排序。 这个答案并没有试图解决这个问题。基于 Hadi 的回答,我的最佳猜测是单个原子存储到一个 32 字节的一半高速缓存线将自动持续,但这是基于当前硬件的工作方式,在内核、高速缓存和内存控制器之间以 2 个 32 字节的一半传输线。如果这真的很重要,请查找文档或询问英特尔。)
请记住,在显式刷新之前,存储数据可以自行传播出缓存(进入 DRAM 或 NVDIMM)。
以下事件序列是可能的:
x[2]=100;
首先存储缓存行的第 3 个字节。(编译时重新排序:这是一个C不ASM问题x
显然是简单的uint8_t x[64]
,不_Atomic或挥发性所以x[1]=100;
和x[2]=100;
不能保证在ASM的顺序发生。)
- 中断到达;在某些时候,包含的缓存行从缓存中
x[]
被逐出,进入持久性域。(也许在上下文切换到另一个线程之后,因此在这两个 asm 存储之间运行了许多其他代码)。
- 系统在恢复执行之前崩溃。(或在
x[1]=100;
饰面变得耐用之前。)
如果您想依赖 x86 内存排序规则来控制缓存行内的持久性顺序,您需要确保 C 遵守这一点。 volatile
会工作,或_Atomic
有memory_order_release
至少2号店。(或者更好,如果它们在对齐的 8 字节块内,则将它们作为单个存储完成。)(x86 asm 内存模型 = 带有存储缓冲区的程序顺序;没有 StoreStore 重新排序。)
编译时重新排序通常不会无缘无故地发生(但它可以);更常见的是由于周围的代码使得这样做很有吸引力。但是周围的代码可能会导致这种情况。(当然x[1]=100;
/x[2]=0;
可以通过这种机制发生,而无需任何编译时重新排序,如果它是 2 个独立的存储。)
我认为持久性原子性的必要前提是作为单个原子存储完成。 例如,由 ISA 保证原子性,或者使用单个更广泛的 SIMD 存储1,因为实际上英特尔 CPU 不会将它们分开(但没有纸上保证)。原子性。中断(即单个指令)而不是单个存储 uop 使拆分更难,但仍然完全可能2,因此不能保证安全。例如,一个 10 字节的 x87fstp tbyte
涉及 2 个单独的存储数据 uop,可以通过来自另一个核心的失效来拆分,即使没有错误共享也是可能的。(再次参见脚注 2。)
如果没有针对 16 字节或更宽的 SIMD 存储的任何纸上原子性保证,您将依赖于 SIMD 存储或未对齐存储的实现细节不被拆分。
但是,即使是 ISA 保证的原子性也是不够的:lock cmpxchg
跨越缓存行边界的 a 仍然保证原子性。其他内核和 DMA 读取器。(支持这个非常非常慢,不要这样做。)但是没有办法保证这两条线同时变得持久。但除了原子性的特殊情况 IDK 之外,我不能排除整线原子性。毫无疑问,在 asm 中原子化的单行存储将成为原子性的持久化,没有撕裂的机会。
在单个缓存行中,我不知道。
我猜想 8 字节对齐块中的原子存储将使其以原子方式持久化或根本不持久化,但我还没有检查过英特尔的文档。(实际上甚至可能是整个 64 字节的行,您可以使用 AVX512 存储)。这个答案的重点是你甚至没有一个原子存储,所以有很多其他机制可以破坏你的测试用例。
脚注 1: 现代 Intel CPU 将 SIMD 存储作为单个事务提交到 L1d 缓存,只要它们不跨越缓存线即可。自 Sandy/Ivy Bridge 以来,英特尔还没有制造出将 SIMD 存储分成两半的 CPU,它具有全宽 256 位 AVX 执行单元,但只有 128 位宽的路径到/从加载单元中的缓存和存储中的 AFAIK -buffer-commit 的东西。(存储数据执行单元也用了 2 个周期将 32 字节存储数据写入存储缓冲区)。
脚注 2:对于像 in 一样属于同一指令一部分的单独存储 uops 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 节点的返回失效。
因此,一个无效的请求可以到达任何时间,并且很可能在核心/存储缓冲器是不会保持到线以上的周期犯的一个未知的数量多店到同一线路。
(到那时,两个存储缓冲区条目都是一条指令的一部分这一事实可能会丢失。访问模式可能会创建一个存储缓冲区条目流,无限期地存储同一高速缓存行的不同部分,因此请等到“这条线的所有存储都完成了”可能让非特权代码为想要读取它的核心创建拒绝服务。所以我认为硬件不太可能有一种机制来避免释放存储之间缓存线的所有权来自同一条指令。)