我正在阅读一些较旧的 MS 文档,理论与实践中的 C# 内存模型,第 2 部分,并且有兴趣阅读:
一个可能的修复方法是在 ThreadA 和 ThreadB 中插入内存屏障...CLR JIT 将插入“锁或”指令来代替内存屏障。锁定的 x86 指令具有刷新存储缓冲区的副作用
事实上,查看 godbolt.org 上 dotnet 工具的 输出,我发现
System.Threading.Thread.MemoryBarrier()
被编译(大概是 jitted)为以下内容:
lock
or dword ptr [rsp], 0
ret
(sharplab.io 为发布版本提供等效输出。)
这似乎有点令人惊讶...英特尔提供了
mfence
指令,这似乎是此目的的理想选择,并且实际上旧的 dotnet 框架方法 Interlocked.SpeculationBarrier 已被记录在 x86 和 amd64 下生成 mfence
(就像旧的 Thread.VolatileRead
和 write 方法一样,但后来已被弃用)。我没有合适的工具来查看为其他架构生成的 MemoryBarrier()
程序集,但内存模型文档表明 ARM64 获得了 dmb
指令,这是一个完整的内存屏障,因此大概相当于 mfence
。
BeeOnRope 对 lock xchg 与 mfence 有相同的行为吗? 有一个有趣的答案,这表明
mfence
在某些情况下比 lock
提供更强的保证,我无法对此提供意见,但即使如果这两条指令完全等价:其他条件都相同,我会选择 mfence
,因为在意图。想必微软的编译器工程师更了解。
接下来的问题是:为什么是
lock or
而不是mfence
?
lock or
出奇地比 mfence
快,而且足够强大。 (只要缓存线的 RMWing 在缓存中已经很热并且是独占的,这通常是堆栈的情况。)
lock add
双字或字节,也带有立即数零,是另一种常见的选择。 lock and
与 -1
也是可能的;任何保持目标和寄存器(EFLAGS 除外)不变的内存目标 RMW 都是等效的。
在某些 CPU 上进行的实验发现,低于 ESP / RSP 几个字节的 RMW 速度更快,至少在要执行
ret
(并弹出返回地址)的函数中是这样。 (TODO:找到我最近读到的有关此问题的博客/文章,尽管我不记得该文章有多旧了。)
我发现这有点令人惊讶,因为 x86
lock
ed 指令必须在完成之前以及在允许稍后的内存操作开始之前耗尽存储缓冲区并修改 L1d 缓存,所以我不会认为以后的加载可能即使他们位于不同的地址,也能取得领先优势。 但显然这是一件事。 尽管如此,许多编译器并没有这样做。 例如,GCC 避免使用它因为它会让 Valgrind 抱怨触摸未分配的内存。 (使用 0
的偏移量可以节省一个字节的机器代码大小。)lock; addl $0,-4(%rsp)
表示 smp_mb()
(用于内核之间的通信),但仍然使用 mfence
表示 mb()
(用于通过其他访问订购 MMIO 的驱动程序)。
mfence
超越一切,确保即使在从 WC 内存(视频 RAM)加载弱有序 NT 的情况下,它也能执行规范的要求。 例如,在我的 Skylake 上,它还包括类似于阻止非内存操作的无序执行的行为,作为他们如何使其如此强大的实现细节。
lfence
会阻塞长 mfence
dep 链的 OoO 执行,例如 imul reg,reg
。lfence
存储,与 mov+seq_cst
的 mov+mfence 来堆栈空间。lock add
而不是 xchg
,但灾难性的选择是拥有单个静态所有线程中的所有屏障都竞争的虚拟变量。mfence
与 xchg
+mov
- 我的答案有一整节关于如何编译 mfence
。 你的问题基本上是该部分的重复,但不是整个问题,而且我没有找到任何我写的作为答案的地方。atomic_thread_fence(seq_cst)
在 2014 年速度较慢。mfence
时。