为什么处理器需要努力展现内存层次结构的协调抽象,如果上面的下一层只会将其丢弃呢?为什么不让高速缓存变得不一致,并要求软件在想要共享某些内容时发出特殊指令?
std::mutex
(以及其他语言中的等效物和早期的pthread_mutex
)所需的acquire
和release
语义如果没有一致的高速缓存将会非常昂贵。如果无法依靠硬件使您的存储可见并使您的加载不从私有缓存中获取陈旧数据,则每次释放锁时都必须写回每个脏行,并且每次获取锁时都必须驱逐每个干净行。_Atomic int
变量,因此不正确的说高级语言不公开此功能。理论上可以在需要显式刷新/使存储器可见以使其对其他核心可见的机器上实现它,但那将非常缓慢。语言模型假定具有一致的高速缓存,不提供范围的显式刷新,而是具有释放操作,该操作使每个旧存储器对在此线程中同步与释放存储器的获取加载的其他线程可见。(请参见When to use volatile with multi threading?进行讨论,尽管该答案主要是驳斥了混淆编译器可以在寄存器中“缓存”非原子非易失性值的人们的误解。)http://eel.is/c++draft/intro.races#19
[注意:前面的四个一致性要求有效地禁止编译器对单个对象的原子操作进行重新排序,即使两个操作都是松散加载。这实际上使得大多数硬件提供的缓存一致性保证可用于C ++原子操作。——结束说明
在C11和C++11之前,SMP内核和一些用户空间的多线程程序手动编写原子操作,使用了与C11和C++11最终以可移植方式公开的相同的硬件支持。
另外,正如评论中指出的那样,对于其他核心对同一行的不同部分进行写操作时,一致的缓存对于防止彼此干扰至关重要。
ISO C11保证了一个char arr[16]
可以由一个线程写入arr[0]
,而另一个线程写入arr[1]
。如果它们都在同一缓存行中,并且存在两个冲突的脏副本,则只有一个可以“获胜”并被写回。C++内存模型和char数组的竞争条件
ISO C有效地要求char
的大小与你可以写入而不影响周围字节的最小单位一样大。在几乎所有机器上(不包括早期的Alpha和一些DSP),这是一个字节,即使在某些非x86 ISA上,一个字节存储可能需要比对齐的字单词多花费一个周期提交到L1d缓存。
该语言直到C11才正式要求这样做,但这只是标准化了编译器和硬件已经工作的“众所周知”的唯一明智选择。
啊,这确实是一个非常深奥的话题!
核之间的缓存一致性用于尽可能地合成对称多处理(SMP)环境。这可以追溯到20世纪90年代中期,当时多个单核CPU只是简单地连接到同一个单一内存总线上,缓存并不是什么大事。随着每个CPU具有多个核心、多个缓存和多个内存接口,合成类似SMP的环境变得更加复杂,而缓存一致性在其中起着重要作用。
因此,当有人问:“为什么处理器需要费心暴露内存层次结构的一致抽象,如果上一层只是要将其丢弃?”时,实际上是在问“我们是否仍然需要SMP环境?”。
答案是软件。大量的软件,包括所有主要操作系统,都是基于它们正在运行在SMP环境的假设编写的。如果去掉SMP,我们就必须重新编写几乎所有东西。
现在有许多聪明的评论家开始怀疑SMP是否是一条死路,我们应该开始担心如何走出这条死路。我认为这不会很快发生;CPU制造商可能还有更多的技巧来提高性能,只要这些技巧不断被提供,没有人愿意承受软件不兼容的痛苦。安全性是避免SMP的另一个原因——Meltdown和Spectre利用了SMP合成的弱点——但我猜想,在其他缓解措施(无论多么令人不悦)可用的情况下,仅凭安全性就不足以放弃SMP。
“为什么不让高速缓存变得不协调,并要求软件在想共享某些东西时发出特殊指令?” 为什么不呢?我们以前做过这样的事情。Transputers(20世纪80年代,90年代初)实现了通信顺序进程(CSP),如果应用程序需要不同的CPU来处理某些数据,则应用程序必须有意地将数据传输到该CPU。这些传输(在CSP中称为“通道”)更像是网络套接字或IPC管道,而不是共享内存空间。
CSP正经历一种复兴——作为一个多进程编程范式,它具有一些非常有益的特性——比如Go、Rust、Erlang等语言都在实现它。关于这些语言在CSP方面的实现,它们不得不在SMP环境之上合成CSP,而SMP环境本身则被合成在电子架构之上,更像Transputer!
通过与CSP的大量经验,我的观点是,每个多进程软件都应该使用CSP;它更加可靠。在SMP之上正确实现CSP需要“复制”数据(这是必须的),这会带来“性能损失”,但实际上它并没有那么严重;从一个CPU复制数据到另一个CPU所需的缓存一致性连接流量与以SMP方式访问数据所需的流量相当。
Rust非常有趣,因为它的语法可以强烈表达数据所有权,我认为它不必复制数据就可以实现CSP,而是可以在线程(进程)之间传递所有权。因此,即使每个线程都在单核CPU上运行,它也可以获得CSP的好处,并且效率非常高。我还没有深入研究Rust,不知道它是否正在做这件事,但我有希望。
CSP 的一个好处是,由于通道类似于网络套接字或 IPC 管道,因此可以很容易地在实际的网络套接字上实现 CSP。原始套接字本身并不理想 - 它们是异步的,因此更类似于 Actor 模型(就像 ZeroMQ 一样)。Actor 模型还算可以 - 我用过它 - 但它并不像 CSP 那样保证没有运行时问题。因此,人们必须自己实现 CSP 部分或找到一个库。然而,有了这个东西,CSP 就成为了一种软件架构,可以更轻松地跨越任意计算机网络而无需改变软件架构;本地通道和网络通道是“相同”的,只是网络通道稍微慢一些。比起Transputers更近期的,Cell 处理器(Playstation 3 的名声)是一种多核设备,正如您所建议的那样。它有一个单独的 CPU 核心和 8 个 SPE 数学核心,每个核心上都有 255k 的芯片速度静态 RAM。要使用 SPE,您必须编写软件将代码和数据传输进出那个 256k(有一个巨大快速的内部环形总线来完成这个任务,并且一个非常快速的外部存储器接口)。结果是,在正确的开发人员的帮助下,可以实现非常好的结果。
Intel 花费了大约另外 10 年的时间才使 x64 实现了大致相同的性能;将聚合乘加指令添加到 SSE 中就终于让它们达到了这个水平,这是他们一直在 Itanium 的曲目中保留的指令,希望提高其吸引力。Cell (SPE 基于 PowerPC 相当于 SSE 的 Altivec)从一开始就有 FMA 指令。
如果开发人员注意到发出锁(+内存屏障)/(内存屏障)解锁,那么缓存一致性就不是必需的。
从成本、功耗、性能、验证等方面来看,缓存一致性的价值很小,甚至具有负面价值。
如今,软件越来越分布式。无论如何,一致性都不能帮助在两台不同机器上运行的两个进程。
即使对于多线程SW,我们最终还是使用IPC。
只有少量数据在多线程SW中共享。
大部分数据不共享,如果共享,则内存屏障应该解决缓存同步问题。
实际上,SW开发人员依赖显式锁定来访问共享数据。这些锁可以通过硬件辅助有效地刷新缓存(有效意味着仅刷新已修改且在其他地方也被缓存的缓存行)。当我们锁定/解锁时,已经完全做到了这一点。由于每个人都执行上述锁定/解锁操作,因此缓存一致性是多余的,是硅空间/功率的浪费,硬件工程师可以放心睡觉。
所有编译器(至少C/C++、Python VM)都会为单线程、非共享数据生成代码。如果我需要共享数据,我只需告诉它是共享的,但不告诉如何和为什么(volatile?)。开发人员需要注意跨硬件核心/SW线程/hw线程进行管理(再次锁定/解锁)。大多数情况下,我们使用非原子数据编写HLL。缓存一致性对开发人员没有任何价值,因此,他/她会退回到通过锁来管理它,这些锁指示缓存系统有效地刷新。所有缓存系统都有缓存行日志,可以在有或没有一致性支持的情况下高效地刷新。(想想缓存但非一致性内存。这种内存仍然具有可用于高效刷新的日志)
缓存一致性在硅上非常复杂,占用空间和功耗。无论如何,SW开发人员都要注意发出内存屏障(通过锁定)。
所以,我认为摆脱一致性并告诉开发人员自己拥有它是好的。
但是我看到趋势是相反的。 看看 CXL 存储器等……它是一致的。
unlock
会刷新整个私有缓存中的所有脏数据。 - Peter Cordes