【问题标题】:Is clflush or clflushopt atomic when system crash?系统崩溃时 clflush 或 clflushopt 是原子的吗?
【发布时间】:2021-04-02 22:49:22
【问题描述】:

通常,cacheline 为 64B,但非易失性内存的原子性为 8B。

例如:

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

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

clflush(); 中的系统崩溃

重启后可以x[1]=0,x[2]=100吗?

【问题讨论】:

  • 从硬件架构的角度来看,如果x[1]和x[2]在同一个cacheline,那么是不可能的。但高级语言有不同的规则,可能会重新排列代码。

标签: c x86-64 atomic persistent-memory clflush


【解决方案1】:

在以下假设下:

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

存储的全局可观察性顺序可能与 Intel x86 处理器上的持久性顺序不同。这称为松弛持久性。唯一保证顺序相同的情况是 WB 类型的存储序列到相同的高速缓存行(但到达 GO 的存储不一定意味着它变得持久)。这是因为CLFLUSH 是原子的,WB 存储不能在全局可观察性中重新排序。请参阅:On x86-64, is the “movnti” or "movntdq" instruction atomic when system crash?

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

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

如果您想消除这种可能性,您可以移动clflush,如下所示:

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

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

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

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

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

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

【讨论】:

  • 在实现持久化的过程中进行存储重新排序是否真的可以在硬件中实现,或者只是在纸上实现?如果是真的,机制是否已知?像 DDR4 突发或 8x 8 字节块的部分传输? (如果一个突发总是从最低的 qword 开始,那么我们可能期望持久性排序将在一行内按地址顺序排列,而不管存储顺序如何,如果它们碰巧都作为同一突发的一部分提交.)
  • @PeterCordes 请记住,内存控制器位于英特尔处理器的持久域中。当一条线路被发送到一个 IMC 时,线路传输过程中可能会发生崩溃,因此 IMC 可能只接收到线路的部分字节(如底部 32 字节或上部 32 字节)。我不知道在这种情况下会发生什么。如果 IMC 坚持这种部分行,那么可能会发生这种重新排序。一条线也可能在不同的 IMC 之间交错,并且可能在所有部件到达所有 IMC 之前发生崩溃。
  • clwb 在这里是一个非常好的主意。在真正实现不刷新的未来 CPU 上,它让第二个存储命中缓存。我建议在示例代码块中使用它而不是 clflushopt。
  • @PeterCordes 我可以确认clflush 是原子的。您可以从您的回答中删除关于可能的 32 字节拆分的那段。
  • 因此,如果使用 WB 存储和 x[1], x[2 ] 是否在同一缓存行中?
【解决方案2】:

(另请参阅@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 可以工作,或者_Atomicmemory_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 节点的反向失效。

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

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

【讨论】:

  • @Hadi:感谢您的编辑,这使得多部分商店的想法更加沮丧;避免虚假分享是不够的。相应地更新了其他部分。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-16
  • 2010-09-11
  • 2019-10-31
  • 1970-01-01
  • 2016-04-30
  • 2018-02-19
相关资源
最近更新 更多