为什么具有顺序一致性的 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 正在释放锁,那么后面的事情似乎发生在关键部分内是可以的。
mfence
和 xchg
在不同的 CPUs 上存在性能差异,可能在热缓存与冷缓存中以及有争议的与无争议的案件。 And/or 多个操作在同一个线程中背靠背的吞吐量与单独一个线程的吞吐量,以及允许周围代码与原子操作重叠执行。
请参阅 https://shipilev.net/blog/2014/on-the-fence-with-dependencies 以了解 mfence
与 lock 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 内存加载通过早期 lock
ed 指令的勘误表。 (与 Skylake 相反,它是 mfence
而不是 lock
ed 指令,这是一个问题。但与 SKL 不同的是,微码中没有修复。这可能就是为什么 Linux 仍然使用 mfence
因为它的 mb()
用于驱动程序,以防任何使用 NT 加载从视频 RAM 或其他东西复制回来但不能让读取发生直到更早的存储可见之后。)
In Linux 4.14,smp_mb()
使用 mb()
。如果可用,则使用 mfence,否则 lock addl [=70=], 0(%esp)
.
__smp_store_mb
(存储+内存屏障)使用xchg
(在以后的内核中不会改变)。
In Linux 4.15、smb_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 上,所有存储都有一个所有内核都同意的总订单。另请参见 :负载可以从存储缓冲区中获取数据,因此我们不能真正说负载有一个总订单 +商店。)
为什么是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 正在释放锁,那么后面的事情似乎发生在关键部分内是可以的。
mfence
和 xchg
在不同的 CPUs 上存在性能差异,可能在热缓存与冷缓存中以及有争议的与无争议的案件。 And/or 多个操作在同一个线程中背靠背的吞吐量与单独一个线程的吞吐量,以及允许周围代码与原子操作重叠执行。
请参阅 https://shipilev.net/blog/2014/on-the-fence-with-dependencies 以了解 mfence
与 lock 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 内存加载通过早期 lock
ed 指令的勘误表。 (与 Skylake 相反,它是 mfence
而不是 lock
ed 指令,这是一个问题。但与 SKL 不同的是,微码中没有修复。这可能就是为什么 Linux 仍然使用 mfence
因为它的 mb()
用于驱动程序,以防任何使用 NT 加载从视频 RAM 或其他东西复制回来但不能让读取发生直到更早的存储可见之后。)
In Linux 4.14,
smp_mb()
使用mb()
。如果可用,则使用 mfence,否则lock addl [=70=], 0(%esp)
.__smp_store_mb
(存储+内存屏障)使用xchg
(在以后的内核中不会改变)。In Linux 4.15、
smb_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 上,所有存储都有一个所有内核都同意的总订单。另请参见