5

通常,高速缓存行是 64B,但非易失性存储器的原子性是 8B。

例如:

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

x缓存行对齐,最初设置为0.

系统崩溃clflush();

重启后可以吗?</ x[1]=0p >x[2]=100

4

2 回答 2

6

在以下假设下:

  • 我假设您显示的代码代表一系列 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] == 100and x[1] != 100。即使系统在退休后崩溃也是可能的,clflush因为退休clflush并不一定意味着刷新的缓存行已到达持久域。

如果您想消除这种可能性,您可以clflush按以下方式移动:

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

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

如果您想在全局可观察x[1]=100之前变得持久,请在 Intel CSX 或AMD 处理器上添加after (仅在 AMD 处理器上订购)。本身就足以控制持久性顺序。x[2]=100sfenceclflushmfenceclflushmfenceclflush

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

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

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

于 2020-12-25T01:36:02.980 回答
5

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


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

以下事件序列是可能的:

  • x[2]=100; 首先存储缓存行的第 3 个字节。(编译时重新排序:这是一个 C 而不是 asm 的问题,x显然是 plain 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 个单独的商店。)


我认为持久性原子性的必要先决条件是作为单个原子存储完成。 例如,由 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: 现代英特尔 CPU 将 SIMD 存储作为单个事务提交到 L1d 缓存,只要它们不跨越缓存行。自从 Sandy/Ivy Bridge 具有全宽 256 位 AVX 执行单元但只有 128 位宽路径往返加载单元中的缓存和存储中的 AFAIK 以来,英特尔还没有制造将 SIMD 存储分成两半的 CPU -buffer-commit 的东西。(存储数据执行单元也花了 2 个周期将 32 字节的存储数据写入存储缓冲区)。

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

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

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

  • 正在执行 RFO 的另一个核心在他们的存储之后使用clflush,在第一个核心可以取回它并完成从该一条指令提交其他数据之前将这条线驱逐到持久内存。

    像另一个核心一样的 NT 存储movnti将强制驱逐线路作为提交 NT 存储的一部分,就像普通存储 + clflushopt 一样。

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

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

  • 或者在没有错误共享的情况下完全合理):由于从包容性缓存行跟踪结构中逐出,随时从 L2/L1d 强制逐出。这可能是由对仅在 L3 中为同一集合起别名而不是错误共享的线路的需求触发的。

    Skylake-server (SKX) 具有非包容性 L3,后来的 Intel 服务器 CPU 也是如此。Cascade Lake (CSX) 是第一个支持持久内存的。尽管它有一个非包含的 L3,但 snoop 过滤器是包含的,并且导致驱逐的填充冲突确实会导致整个 NUMA 节点的反向失效。

因此,无效请求可以随时到达并且核心/存储缓冲区很可能不会在更多周期内保留该行以将未知数量的更多存储提交到同一行。

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

于 2020-12-24T23:17:25.483 回答