Acquire/release x64 上非时态存储的语义

Acquire/release semantics with non-temporal stores on x64

我有类似的东西:

if (f = acquire_load() == ) {
   ... use Foo
}

和:

auto f = new Foo();
release_store(f)

您可以轻松想象 acquire_load 和 release_store 的实现,它使用带有加载 (memory_order_acquire) 和存储 (memory_order_release) 的原子。但是现在如果 release_store 是用 _mm_stream_si64 实现的,一个非时间写入,它相对于 x64 上的其他存储没有排序?如何获得相同的语义?

我认为以下是最低要求:

atomic<Foo*> gFoo;

Foo* acquire_load() {
    return gFoo.load(memory_order_relaxed);
}

void release_store(Foo* f) {
   _mm_stream_si64(*(Foo**)&gFoo, f);
}

并按原样使用它:

// thread 1
if (f = acquire_load() == ) {
   _mm_lfence(); 
   ... use Foo
}

和:

// thread 2
auto f = new Foo();
_mm_sfence(); // ensures Foo is constructed by the time f is published to gFoo
release_store(f)

对吗?我很确定这里绝对需要 sfence。但是围栏呢?对于 x64 来说,它是必需的还是一个简单的编译器屏障就足够了?例如asm 易失性(“”:::"memory")。根据 x86 内存模型,加载不会与其他加载一起重新排序。因此,据我了解,只要存在编译器障碍,acquire_load() 就必须在 if 语句内的任何加载之前发生。

我在这个答案中的某些地方可能是错误的(欢迎了解这些内容的人进行校对!)。它基于阅读文档和 Jeff Preshing 的博客,而不是最近的实际经验或测试。

Linus Torvalds 强烈建议不要尝试发明自己的锁,因为它很容易出错。在为 Linux 内核编写 portable 代码时,这比 x86-only 的代码更重要,所以我有足够的勇气 尝试 来排序x86 的事情。


使用 NT 存储的正常方法是连续做一堆,比如作为 memset 或 memcpy 的一部分,然后是 SFENCE,然后是到共享标志变量的正常释放存储: done_flag.store(1, std::memory_order_release).

使用 movnti 存储同步变量会影响性能。您可能想在它指向的 Foo 中使用 NT 存储,但是从缓存中逐出指针本身是有悖常理的。 (movnt 存储驱逐缓存行,如果它在缓存中以 开头;参见 vol1 ch 10.4.6.2 时态数据与非时态数据的缓存).

NT 存储的全部意义在于与非时态数据一起使用,如果有的话,很长一段时间都不会再次使用(任何线程)。控制对共享缓冲区的访问的锁,或 producers/consumers 用于将数据标记为已读的标志, 预期被其他内核读取的。

您的函数名称也没有真正反映您在做什么。

x86 硬件针对正常(非 NT)发布存储进行了极大优化,因为 每个 正常存储都是发布存储。硬件必须擅长 x86 才能 运行 快速。

使用正常 stores/loads 只需要访问 L3 缓存,而不是 DRAM,以便在 Intel CPU 上的线程之间进行通信。 Intel 的大型 inclusive L3 缓存用作缓存一致性流量的后盾。探测一个内核未命中时的 L3 标签将检测到另一个内核在 Modified or Exclusive state 中具有缓存行的事实。 NT 存储需要同步变量一直到 DRAM 并返回另一个内核才能看到它。


NT 流媒体商店的内存排序

movnt 商店可以与其他商店重新排序,但 不能 与旧的阅读。

Intel's x86 manual vol3, chapter 8.2.2 (Memory Ordering in P6 and More Recent Processor Families):

  • Reads are not reordered with other reads.
  • Writes are not reordered with older reads. (note the lack of exceptions).
  • Writes to memory are not reordered with other writes, with the following exceptions:
    • streaming stores (writes) executed with the non-temporal move instructions (MOVNTI, MOVNTQ, MOVNTDQ, MOVNTPS, and MOVNTPD); and
    • string operations (see Section 8.2.4.1). (note: From my reading of the docs, . There's only potential reordering between the stores within a single rep movs or rep stos.)
  • ... stuff about clflushopt and the fence instructions

更新: 还有一条注释(在8.1.2.2 软件控制总线锁定中)说:

Do not implement semaphores using the WC memory type. Do not perform non-temporal stores to a cache line containing a location used to implement a semaphore.

这可能只是一个性能建议;他们没有解释它是否会导致正确性问题。请注意,NT 存储不是高速缓存一致的(数据可以位于行填充缓冲区中,即使同一行的冲突数据存在于系统中的其他位置或内存中)。也许您可以安全地将 NT 存储用作与常规加载同步的发布存储,但是 运行 会遇到原子 RMW 操作的问题,例如 lock add dword [mem], 1.


Release semantics 防止在程序顺序中先于它的任何 读取或写入 操作对写入释放进行内存重新排序。

要阻止对较早商店的重新排序,我们需要一个 SFENCE 指令,即使对于 NT 商店也是 a StoreStore barrier。 (并且也是某些类型的编译时重新排序的障碍,但我不确定它是否会阻止早期加载越过障碍。)普通存储不需要任何类型的障碍指令来发布存储,所以使用 NT 商店时,您只需要 SFENCE

对于加载:WB (write-back, i.e. "normal") memory already prevents LoadStore reordering even for weakly-ordered stores, so we don't need an LFENCE for its LoadStore barrier effect 的 x86 内存模型,在 NT 存储之前只有一个 LoadStore 编译器屏障。 至少在 gcc 的实现中,std::atomic_signal_fence(std::memory_order_release) 即使对于非原子 loads/stores 也是编译器障碍,但是 atomic_thread_fence 只是 atomic<> loads/stores 的障碍(包括mo_relaxed)。使用 atomic_thread_fence 仍然允许编译器更自由地将 loads/stores 重新排序为非共享变量。 .

// The function can't be called release_store unless it actually is one (i.e. includes all necessary barriers)
// Your original function should be called relaxed_store
void NT_release_store(const Foo* f) {
   // _mm_lfence();  // make sure all reads from the locked region are already globally visible.  Not needed: this is already guaranteed
   std::atomic_thread_fence(std::memory_order_release);  // no insns emitted on x86 (since it assumes no NT stores), but still a compiler barrier for earlier atomic<> ops
   _mm_sfence();  // make sure all writes to the locked region are already globally visible, and don't reorder with the NT store
   _mm_stream_si64((long long int*)&gFoo, (int64_t)f);
}

这存储到原子变量(注意缺少取消引用 &gFoo)。您的函数存储到它指向的 Foo ,这太奇怪了; IDK那是什么意思。另请注意,它 compiles as valid C++11 code.

在考虑释放存储的含义时,请将其视为释放共享数据结构上的锁的存储。在您的情况下,当发布存储变得全局可见时,任何看到它的线程都应该能够安全地取消引用它。


要进行获取加载,只需告诉编译器您想要一个。

x86 不需要任何屏障指令,但指定 mo_acquire 而不是 mo_relaxed 可为您提供必要的编译器屏障。作为奖励,此功能是 portable:您将在其他架构上获得任何和所有必要的障碍:

Foo* acquire_load() {
    return gFoo.load(std::memory_order_acquire);
}

你没有说在弱序 WC (uncacheable write-combining) 内存中存储 gFoo。将程序的数据段映射到 WC 内存可能真的很难...... gFoo 简单地 指向 WC 内存会容易得多,之后你 mmap 一些 WC 视频 RAM 或其他东西。但是如果你想从 WC 内存中获取负载,你可能确实需要 LFENCE。 IDK。问另一个问题,因为这个答案主要假设你正在使用 WB 内存。

请注意,使用指针而不是标志会产生数据依赖性。我认为您应该能够使用 gFoo.load(std::memory_order_consume),即使在弱序 CPU(Alpha 除外)上也不需要障碍。一旦编译器足够先进以确保它们不会破坏数据依赖性,它们实际上可以编写更好的代码(而不是将 mo_consume 提升为 mo_acquire。在使用 mo_consume 之前阅读此内容在生产代码中,尤其是要注意正确测试它是不可能的,因为未来的编译器预计会比当前的编译器在实践中提供更弱的保证。


最初我在想我们确实需要 LFENCE 来获得 LoadStore 屏障。 ("Writes cannot pass earlier LFENCE, SFENCE, and MFENCE instructions"。这反过来会阻止它们传递(之前变得全局可见)在 LFENCE 之前的读取)。

请注意,LFENCE + SFENCE 仍然比完整的 MFENCE 弱,因为它不是 StoreLoad 障碍。 SFENCE 自己的文档说它是按顺序订购的。 LFENCE,但是英特尔手册第 3 卷中的 x86 内存模型 table 没有提到这一点。如果 SFENCE 直到 LFENCE 之后才能执行,那么 sfence / lfence 实际上可能等同于 mfence,但 lfence / sfence / movnti 会在没有完全障碍的情况下给出发布语义。请注意,与正常的强顺序 x86 存储不同的是,NT 存储在一些后续 loads/stores 之后可能会变得全局可见。)


相关:NT 负载

在 x86 中,除了从 WC 内存加载外,每个加载都有获取语义。 SSE4.1 MOVNTDQA 是唯一的非临时加载指令,它 在普通(回写)内存上使用时不是 弱排序。所以它也是一个获取负载(在 WB 内存上使用时)。

注意movntdq只有store form,而movntdqa只有load form。但显然英特尔不能只称它们为 storentdqaloadntdqa。它们都具有 16B 或 32B 对齐要求,因此省略 a 对我来说意义不大。我想 SSE1 和 SSE2 已经引入了一些已经使用 mov... 助记符(如 movntps)的 NT 存储,但直到几年后在 SSE4.1 中才加载。 (第二代 Core2:45nm Penryn)。

文档说 MOVNTDQA 不会改变它所使用的内存类型的排序语义

... An implementation may also make use of the non-temporal hint associated with this instruction if the memory source is WB (write back) memory type.

A processor’s implementation of the non-temporal hint does not override the effective memory type semantics, but the implementation of the hint is processor dependent. For example, a processor implementation may choose to ignore the hint and process the instruction as a normal MOVDQA for any memory type.

实际上,当前的英特尔主流 CPU(Haswell、Skylake)似乎忽略了从 WB 内存加载 PREFETCHNTA 和 MOVNTDQA 的提示。有关详细信息,请参阅 Do current x86 architectures support non-temporal loads (from "normal" memory)?, and also Non-temporal loads and the hardware prefetcher, do they work together?


此外,如果您在 WC 内存上使用它(例如 copying from video RAM, like in this Intel guide):

Because the WC protocol uses a weakly-ordered memory consistency model, an MFENCE or locked instruction should be used in conjunction with MOVNTDQA instructions if multiple processors might reference the same WC memory locations or in order to synchronize reads of a processor with writes by other agents in the system.

不过,这并没有说明如何使用它。而且我不确定为什么他们在阅读时说 MFENCE 而不是 LFENCE。也许他们在谈论写入设备内存、从设备内存读取的情况,其中存储必须根据负载(StoreLoad 屏障)排序,而不仅仅是彼此排序(StoreStore 屏障)。

我在第 3 卷中搜索 movntdqa,但没有找到任何结果(在整个 pdf 中)。 movntdq 3 次命中:所有关于弱排序和内存类型的讨论都只讨论存储。请注意 LFENCE 早在 SSE4.1 之前就已引入。大概它对某些东西有用,但 IDK 什么。对于负载排序,可能仅适用于 WC 内存,但我还没有读到什么时候有用。


LFENCE 似乎不仅仅是弱顺序加载的 LoadLoad 屏障:它还对其他指令进行排序。 (不过,不是商店的全球可见性,只是它们的本地执行)。

来自 Intel 的 insn ref 手册:

Specifically, LFENCE does not execute until all prior instructions have completed locally, and no later instruc- tion begins execution until LFENCE completes.
...
Instructions following an LFENCE may be fetched from memory before the LFENCE, but they will not execute until the LFENCE completes.

rdtsc 的条目建议使用 LFENCE;RDTSC 来防止它在前面的指令之前执行,当 RDTSCP 不可用时(并且较弱的顺序保证是可以的:rdtscp 不会停止遵循在它之前执行的指令)。 (CPUID 是围绕 rdtsc 序列化指令流的常见建议)。