为什么具有顺序一致性的 std::atomic 存储使用 XCHG?

Why does a std::atomic store with sequential consistency use XCHG?

为什么是std::atomic's store:

std::atomic<int> my_atomic;
my_atomic.store(1, std::memory_order_seq_cst);

在请求具有顺序一致性的存储时执行 xchg


从技术上讲,具有 read/write 内存屏障的普通存储是否足够?相当于:

_ReadWriteBarrier(); // Or `asm volatile("" ::: "memory");` for gcc/clang
my_atomic.store(1, std::memory_order_acquire);

我明确地谈论 x86 和 x86_64。商店有隐式收购围栏的地方。

mov-store + mfence and xchg 都是在 x86 上实现 sequential-consistency 存储的有效方法。 [=13= 上的隐式 lock 前缀] 内存使它成为一个完整的内存屏障,就像 x86 上的所有原子 RMW 操作一样。

(x86 的 memory-ordering 规则本质上使 full-barrier 效果成为任何原子 RMW 的唯一选项:它同时是加载和存储,在全局顺序中粘在一起。原子性要求加载和存储不能通过仅将存储排队到存储缓冲区来分开,因此必须将其排空,并且 load-load 加载端的排序要求它不重新排序。)

普通的mov是不够的;它只有发布语义,没有 sequential-release。 (不像 AArch64 的 stlr 指令,它确实做了一个 sequential-release 存储,它不能用以后的 ldar sequential-acquire 加载重新排序。这个选择显然是由 C++11 激发的seq_cst作为默认的内存顺序。但是AArch64的正常存储要弱得多;放松不释放。)

请参阅 Jeff Preshing's article on acquire / release semantics,并注意常规版本存储(如 mov 或除 xchg 之外的任何 non-locked x86 memory-destination 指令)允许在以后的操作中重新排序,包括获取负载(如 mov 或任何 x86 memory-source 操作数)。例如如果 release-store 正在释放锁,那么后面的事情似乎发生在关键部分内是可以的。


mfencexchg 在不同的 CPUs 上存在性能差异,可能在热缓存与冷缓存中以及有争议的与无争议的案件。 And/or 多个操作在同一个线程中背靠背的吞吐量与单独一个线程的吞吐量,以及允许周围代码与原子操作重叠执行。

请参阅 https://shipilev.net/blog/2014/on-the-fence-with-dependencies 以了解 mfencelock addl [=23=], -8(%rsp)(%rsp) 的实际基准作为一个完整的障碍(当您还没有商店时).

在 Intel Skylake 硬件上,mfence 阻止 out-of-order 独立 ALU 指令的执行,但 xchg 不会 。 (See my test asm + results in the bottom of this SO answer)。英特尔的手册并不要求它那么强大;只有 lfence 被记录在案。但作为一个实现细节,out-of-order 在 Skylake 上执行周围代码非常昂贵。

我没有测试其他CPUs,这可能是a microcode fix for erratum SKL079的结果,SKL079 MOVNTDQA 来自WC内存可能通过 更早的 MFENCE 指令。 erratum的存在基本上证明了SKL曾经可以执行MFENCE之后的指令。如果他们通过在微代码中增强 MFENCE 来修复它,我不会感到惊讶,这是一种显着增加对周围代码影响的钝器方法。

我只测试了 single-threaded 缓存行在 L1d 缓存中是热的情况。 (不是当它在内存中很冷,或者当它在另一个核心上处于修改状态时。)xchg 必须加载以前的值,从而对内存中的旧值产生“错误”依赖。但是 mfence 强制 CPU 等到之前的存储提交到 L1d,这也需要缓存行到达(并处于 M 状态)。所以他们在这方面可能差不多,但英特尔的 mfence 强制一切等待,而不仅仅是加载。

AMD 的优化手册建议xchg 用于原子seq-cst 存储。我以为 Intel 推荐 mov + mfence,旧的 gcc 使用,但是 Intel 的编译器在这里也使用 xchg

当我测试时,xchg 在 Skylake 上的吞吐量比 mov+mfence 在同一位置的 single-threaded 循环中重复。有关详细信息,请参阅 Agner Fog's microarch guide and instruction tables,但他不会花太多时间在锁定操作上。

gcc/clang/ICC/MSVC output on the Godbolt compiler explorer C++11 seq-cst my_atomic = 4; gcc 使用 mov + mfence当 SSE2 可用时。 (使用 -m32 -mno-sse2 让 gcc 也使用 xchg)。其他 3 个编译器都更喜欢 xchg 默认调整,或者 znver1 (Ryzen) 或 skylake.

Linux 内核将 xchg 用于 __smp_store_mb()

更新:最近的 GCC(如 GCC10)更改为像其他编译器一样对 seq-cst 存储使用 xchg,即使 mfence 的 SSE2 可用。


另一个有趣的问题是如何编译atomic_thread_fence(mo_seq_cst);。显而易见的选项是 mfence,但 lock or dword [rsp], 0 是另一个有效选项(当 MFENCE 不可用时由 gcc -m32 使用)。堆栈底部通常在 M 状态的缓存中已经很热。如果本地存储在那里,缺点是会引入延迟。 (如果它只是一个 return 地址,return-address 预测通常非常好,所以延迟 ret 读取它的能力不是什么大问题。)所以 lock or dword [rsp-4], 0 可以在某些情况下值得考虑。 (gcc did consider it,但因为它让 valgrind 不高兴而恢复了它。这是在知道它可能比 mfence 更好之前,即使 mfence 可用。)

所有编译器当前都使用 mfence 作为 stand-alone 屏障。这些在 C++11 代码中很少见,但需要更多研究关于在无锁通信的线程内部进行实际工作的真实 multi-threaded 代码实际上最有效的方法。

但是多个源建议使用lock add作为堆栈的屏障而不是mfence,所以Linux内核最近切换到将它用于 x86 上的 smp_mb() 实现,即使 SSE2 可用。

请参阅 https://groups.google.com/d/msg/fa.linux.kernel/hNOoIZc6I9E/pVO3hB5ABAAJ 进行一些讨论,包括提及 HSW/BDW 关于 movntdqa 从 WC 内存加载通过早期 locked 指令的勘误表。 (与 Skylake 相反,它是 mfence 而不是 locked 指令,这是一个问题。但与 SKL 不同的是,微码中没有修复。这可能就是为什么 Linux 仍然使用 mfence 因为它的 mb() 用于驱动程序,以防任何使用 NT 加载从视频 RAM 或其他东西复制回来但不能让读取发生直到更早的存储可见之后。)

  • In Linux 4.14smp_mb() 使用 mb()。如果可用,则使用 mfence,否则 lock addl [=70=], 0(%esp).

    __smp_store_mb(存储+内存屏障)使用xchg(在以后的内核中不会改变)。

  • In Linux 4.15smb_mb()使用lock; addl [=74=],-4(%esp)%rsp,而不是使用mb()。 (内核即使在 64 位中也不使用 red-zone,因此 -4 可能有助于避免局部变量的额外延迟)。

    mb() 被驱动程序用来命令访问 MMIO 区域,但是 smp_mb() 在为单处理器系统编译时变成 no-op。更改 mb() 风险更大,因为它更难测试(影响驱动程序),并且 CPUs 有与锁定和 mfence 相关的勘误表。但无论如何,如果可用,mb() 使用 mfence,否则 lock addl [=82=], -4(%esp)。唯一的变化是 -4.

  • In Linux 4.16,除了删除 #if defined(CONFIG_X86_PPRO_FENCE) 之外没有变化,它定义了比现代硬件实现的 x86-TSO 模型更多 weakly-ordered 内存模型的东西。


x86 & x86_64. Where a store has an implicit acquire fence

你的意思是发布,我希望。 my_atomic.store(1, std::memory_order_acquire); 不会编译,因为 write-only 原子操作不能是获取操作。另见 Jeff Preshing's article on acquire/release semantics

Or asm volatile("" ::: "memory");

不,那只是一个编译器障碍;它阻止了所有 compile-time reordering across it, but doesn't prevent runtime StoreLoad reordering,即存储被缓冲到以后,并且直到以后加载之后才出现在全局顺序中。 (StoreLoad 是 x86 允许的唯一一种运行时重新排序。)

无论如何,另一种表达你想要的方式是:

my_atomic.store(1, std::memory_order_release);        // mov
// with no operations in between, there's nothing for the release-store to be delayed past
std::atomic_thread_fence(std::memory_order_seq_cst);  // mfence

使用释放栅栏不够强大(它和 release-store 都可以延迟到以后的加载之后,这与说释放栅栏不能阻止以后的加载发生是一回事早期的)。但是,release-acquire 栅栏可以解决问题,防止稍后加载提前发生,并且本身不能通过发布商店重新排序。

相关:Jeff Preshing's article on fences being different from release operations.

但请注意,根据 C++11 规则,seq-cst 是特殊的:只有 seq-cst 操作保证具有所有线程同意看到的单个全局/总顺序。因此,即使在 x86 上,用较弱的顺序 + 栅栏模拟它们在 C++ 抽象机上通常可能并不完全等效。 (在 x86 上,所有存储都有一个所有内核都同意的总订单。另请参见 :负载可以从存储缓冲区中获取数据,因此我们不能真正说负载有一个总订单 +商店。)