微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

系统崩溃时 clflush 或 clflushopt 是原子的吗? 如果两个存储跨越缓存线边界或者如果存储的有效内存类型是 WC:在单个缓存行内,我不知道

如何解决系统崩溃时 clflush 或 clflushopt 是原子的吗? 如果两个存储跨越缓存线边界或者如果存储的有效内存类型是 WC:在单个缓存行内,我不知道

通常情况下,cacheline 是 64B,但非易失性内存的原子性是 8B。

例如:

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

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

系统在 clflush(); 中崩溃

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

解决方法

在以下假设下:

  • 我假设您展示的代码代表了一系列 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 内存模型不允许重新排序存储,因此其他代理不可能在正常操作期间观察 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 上的 sfence 或 AMD 处理器上的 clflush 之后添加 mfence({ {1}} 仅由 clflush 在 AMD 处理器上订购)。 mfence 本身足以控制持久顺序。

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

clwb+sfence

在这种情况下,如果发生崩溃并且 x[1]=100; clflushopt(x); sfence; x[2]=100; 处于持久状态,则可以保证 x[2] == 100x[1] == 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 个独立的存储。)


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

如果没有为 16 字节或更宽的 SIMD 存储提供任何纸上原子性保证,您将依赖于 SIMD 存储或未对齐存储的实现细节,以被拆分。

即使是 ISA 保证的原子性也是不够的:跨越缓存行边界的 lock cmpxchg 仍然保证原子性。其他内核和 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:对于像 fstp tbyte [rdi] 中一样属于同一指令一部分的单独存储 uop,这可能是可能的:

  • 第一部分从存储缓冲区提交到 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 节点的返回失效。

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

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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。