我可以在多核 x86 CPU 上强制缓存一致性吗?

Posted

技术标签:

【中文标题】我可以在多核 x86 CPU 上强制缓存一致性吗?【英文标题】:Can I force cache coherency on a multicore x86 CPU? 【发布时间】:2010-10-08 05:08:53 【问题描述】:

另一周,我写了一个小线程类和一个单向消息管道来允许线程之间的通信(显然,每个线程有两个管道,用于双向通信)。在我的 Athlon 64 X2 上一切正常,但我想知道如果两个线程都在查看同一个变量并且每个内核上该变量的本地缓存值不同步,我是否会遇到任何问题。

我知道 volatile 关键字会强制变量从内存中刷新,但是在多核 x86 处理器上是否有办法强制所有内核的缓存同步?这是我需要担心的事情吗,或者 volatile 和正确使用轻量级锁定机制(我使用 _InterlockedExchange 来设置我的 volatile 管道变量)会处理我想要编写“无锁”的所有情况多核 x86 CPU 的代码?

我已经知道并使用了关键部分、互斥体、事件等。我主要想知道是否存在我不知道的 x86 内在函数或可用于强制缓存一致性。

【问题讨论】:

您想要跨平台的东西还是在 Windows 或 Linux 上? 现在可能只是 Windows。代码库可能会扩展到 MacOS、WinMobile 以及 iPhone 在某些时候使用的任何东西,但最初的开发是在 Win32/64 下。 这是一个常见的误解,volatile 并不意味着“从内存中刷新”。查看 Fedor Pikus 的关于无锁的视频,他也描述了“不稳定”。 youtu.be/lVBvHbJsg5Y?t=16m17s 【参考方案1】:

Volatile 不会这样做。在 C++ 中,volatile 只影响编译器的优化,例如将变量存储在寄存器而不是内存中,或者完全删除它。

【讨论】:

【参考方案2】:

您无需担心缓存一致性。硬件会解决这个问题。您可能需要担心的是由于缓存一致性导致的性能问题。

如果 core#1 写入一个变量,这会使其他核心中缓存行的所有其他副本无效(因为它必须在提交存储之前获取缓存行的exclusive ownership)。当 core#2 读取同一个变量时,它会在缓存中丢失(除非 core#1 已经将它写回到缓存的共享级别)。

由于必须从内存中读取整个高速缓存行(64 字节)(或写回共享高速缓存,然后由核心#2 读取),因此会产生一些性能成本。在这种情况下,这是不可避免的。这是期望的行为。


问题在于,当您在同一高速缓存行中有多个变量时,即使内核在同一高速缓存行中读取/写入不同的变量,处理器也可能会花费额外的时间来保持高速缓存同步。

可以通过确保这些变量不在同一个缓存行中来避免这种成本。这种效果被称为 错误共享,因为您正在强制处理器同步线程之间实际上未共享的对象的值。

【讨论】:

“必须从内存中读取”位具有误导性,因为数据可能是从另一个缓存中窥探​​到的。 我没想到。我认为仍然会有性能成本,但与从 RAM 读取的数量不同。 我认为这里提及虚假分享是合理的? @WiSaGaN - 这不是我回答的最后一段所描述的吗?还是我错过了什么? 是的,这正是你在这里提到的。由于它已经有了一个既定的名称,我们可以在这里添加名称。【参考方案3】:

您没有指定您使用的是哪个编译器,但如果您使用的是 Windows,请查看 this article here。另请查看可用的 synchronization functions here。您可能要注意,通常volatile 不足以完成您希望它执行的操作,但在 VC 2005 和 2008 下,添加了非标准语义,在读写周围添加了隐含的内存屏障。

如果你想让东西便携,你将面临更加艰难的道路。

【讨论】:

【参考方案4】:

volatile 仅强制您的代码重新读取值,它无法控制从何处读取值。如果您的代码最近读取了该值,那么它可能会在缓存中,在这种情况下,volatile 会强制它从缓存中重新读取,而不是从内存中读取。

x86 中没有很多缓存一致性指令。有像prefetchnta 这样的预取指令,但这不会影响内存排序语义。它过去是通过将值带入 L1 缓存而不污染 L2 来实现的,但是对于具有大型共享包含 L3 缓存的现代 Intel 设计来说,事情变得更加复杂。

x86 CPU 使用 MESI protocol 的变体(英特尔的 MESIF,AMD 的 MOESI)来保持它们的缓存相互一致(包括不同内核的私有 L1 缓存)。想要写入缓存行的核心必须强制其他核心使其副本无效,然后才能将自己的副本从共享状态更改为修改状态。


您不需要任何栅栏指令(如 MFENCE)来在一个线程中生成数据并在 x86 上的另一个线程中使用它,因为 x86 加载/存储具有内置的 acquire/release semantics。您确实需要 MFENCE (完全障碍)来获得顺序一致性。 (此答案的先前版本表明需要clflush,这是不正确的)。

您确实需要阻止compile-time reordering,因为 C++ 的内存模型是弱序的。 volatile 是一种旧的、糟糕的方法; C++11 std::atomic 是编写无锁代码的更好方法。

【讨论】:

那么这里的正确顺序是什么? _InterlockedExchange(); // 原子写入 _clflush() // 同步缓存 _mfence() // 导致等待,直到缓存同步 或者我是否需要在 _clflush() 之上再添加一个 _mfence()?谢谢。 AtomicWrite、Memory fence 等待 AtomicWrite 命中缓存、CacheFlush、Memory Fence 以确保您写入的下一个内容在刷新之前不可见。最后一道栅栏可能不需要,我不确定。 好的,很酷,我会试试的。当然,我必须将整个事情包装在一个条件中以确定 _cflush 是否存在,并且由于整个事情应该紧密包装,我猜我应该只有一个内联函数,它根据运行时系统信息决定要做什么班级。谢谢! -1 'volatile' 的全部意义在于强制 CPU 忽略缓存值。也许你的 'volatile' 版本坏了。 答案是对的。 @SoapBox 可能意味着 cpu 缓存 - 但您所说的是将结果缓存到寄存器中。本质上, volatile 用于声明“设备寄存器”变量——它告诉编译器“这不是从内存中读取的,而是从外部源中读取的”——因此编译器会随时重新读取它,因为它不能确保读取的值将等于上次写入的值。如果您的实现的“读取”被定义为发出“loadw”,那么它肯定有时会从 CPU 缓存中读取 - 但从 C 的角度来看这很好。【参考方案5】:

Herb Sutter 似乎只是简单地suggest 任何两个变量都应该驻留在不同的缓存行上。他在并发队列中执行此操作,并在锁和节点指针之间进行填充。

编辑:如果您使用的是 Intel 编译器或 GCC,则可以使用 atomic builtins,它们似乎会尽可能地抢占缓存。

【讨论】:

当然,固定长度的填充可能会在以后的一些芯片上失败。 当然,如果现有的垫太小,您以后可以随时选择更大的垫。它可能会增加缓存未命中的可能性,但这不是重点吗? 我们无法定位假设的未来处理器。编写在当今处理器上运行良好的代码。【参考方案6】:

由于 x86 处理器采用 MESI 协议,内核之间的缓存一致性得到了保证。在处理可能在数据仍位于内核缓存上时访问内存的外部硬件时,您只需要担心内存一致性。不过,这里看起来不像你的情况,因为文本表明你正在用户空间中编程。

【讨论】:

关于多处理器系统? x86不使用MESI协议,但MESIF和MOESI可以。 x86 确实处理了连贯性。但是请阅读内存一致性:不能保证所有写入(例如写入数据和释放锁,仅举两例)对所有 CPU 都以相同的顺序可见!这就是内存栅栏的用途。 @Wim 在 x86/x64 上,内存写入保证以相同的顺序可见,因此在此平台上不需要内存栅栏,唯一可能的问题是编译器重新排序。阅读英特尔开发人员手册或在此处获取简短版本multicoreinfo.com/research/papers/2008/damp08-intel64.pdf @camelccc:在 x86 上不允许 StoreStore 重新排序,但在加载后,商店可以全局可见。 x86 加载/存储具有获取/释放语义,而不是顺序一致性。您可以在实际硬件上观察 StoreLoad 重新排序:preshing.com/20120515/memory-reordering-caught-in-the-act。所以你错了,在 x86 上不需要内存栅栏,但你是对的,为此不需要它们。 Sill,您需要像 var.store(newval, std::memory_order_release) 这样的 C++ 代码来避免编译时重新排序,即使在为 x86 编译时也是如此。【参考方案7】:

有一系列文章解释了现代内存架构 here,包括Intel Core2 caches 和更多现代架构主题。

文章可读性强,图文并茂。享受吧!

【讨论】:

【参考方案8】:

您的问题中有几个子问题,因此我会尽我所能回答。

    目前没有可移植的方式在 C++ 中实现无锁交互。 C++0x 提案通过引入原子库解决了这个问题。 Volatile 不能保证在多核上提供原子性,并且它的实现是特定于供应商的。 在 x86 上,您不需要做任何特别的事情,除了将共享变量声明为 volatile 以防止某些可能破坏多线程代码的编译器优化。 Volatile 告诉编译器不要缓存值。 有些算法(例如 Dekker)即使在带有易失变量的 x86 上也无法工作。 除非您确定在线程之间传递对数据的访问是程序的主要性能瓶颈,否则请远离无锁解决方案。使用按值或锁传递数据。

【讨论】:

使变量 volatile 只是难题的一部分。这并不能解决并发问题。内存防护对于确保变量访问在所有处理器内核之间同步是必要的。 更新:C11 和 C++11 为 lock-free programming 引入了 std::atomic。【参考方案9】:

以下是一篇很好的文章,参考了使用带有线程程序的volatile

Volatile Almost Useless for Multi-Threaded Programming.

【讨论】:

以上是关于我可以在多核 x86 CPU 上强制缓存一致性吗?的主要内容,如果未能解决你的问题,请参考以下文章

CPU内核与寄存器关系

三:CPU缓存一致性协议MESI

CPU双核或者多核的速度怎么算?

在“任何 CPU”.NET 程序集上强制 x86 CLR

请问多核CPU还需要超线程技术吗?还有多核CPU的带宽怎样计算?

高速缓存和主内存之间如何保持数据一致性