[c++] C ++ 11引入了标准化的内存模型。 这是什么意思? 它将如何影响C ++编程?


2 Answers

我将给出我理解内存一致性模型(或内存模型,简称)的类比。 它受Leslie Lamport的开创性论文“时间,时钟和分布式系统中的事件排序”的启发。 类比是恰当的,具有根本性的意义,但对许多人来说可能是矫枉过正的。 但是,我希望它提供了一个能够促进关于内存一致性模型推理的心理图像(图形表示)。

我们来看一个空间 - 时间图中所有存储位置的历史,其中水平轴代表地址空间(即每个存储位置由该轴上的一个点表示),而垂直轴代表时间(我们将看到,总的来说,没有普遍的时间观念)。 因此,每个存储器位置所保存的值的历史记录由该存储器地址处的垂直列表示。 每个值的变化都是由于其中一个线程向该位置写入新值。 通过内存映像 ,我们将指特定线程 在特定时间可观察到的所有内存位置值的集合/组合。

“内存一致性和缓存一致性入门”引用

直观的(也是最具限制性的)内存模型是顺序一致性(SC),其中多线程执行看起来像是每个组成线程的顺序执行的交错,就好像线程在单核处理器上时间复用一样。

全局记忆顺序可以从程序的一次运行到另一次不同,并且可能不会事先知道。 SC的特征是地址空间 - 时间图中表示同时性平面 (即存储器图像)的一组水平切片。 在给定的平面上,其所有事件(或存储器值)都是同时发生的。 有绝对时间的概念,其中所有线程都同意哪些存储器值是同时存在的。 在SC中,在每个时刻,所有线程只共享一个内存映像。 也就是说,在任何时刻,所有处理器都会同意内存映像(即内存的聚合内容)。 这不仅意味着所有线程都查看所有内存位置的相同值序列,而且所有处理器都观察到所有变量的相同组合 。 这与所有线程的所有内存操作(在所有内存位置上)都以相同的顺序进行观察是一样的。

在宽松的内存模型中,每个线程都会以自己的方式切分地址空间时间,唯一的限制是每个线程的切片不能相互交叉,因为所有线程必须同意每个单独内存位置的历史(当然,不同线程的切片可以并且将会彼此交叉)。 没有通用的方法来分割它(没有地址空间时间的特权化)。 切片不必是平面的(或线性的)。 它们可以是弯曲的,这可以使得线程读取由另一个线程写入的值,而不是写入它们的顺序。不同存储器位置的历史可以在由任何特定线程查看时相对于彼此任意地滑动(或拉伸) 。 每个线程对于哪些事件(或等价的存储器值)是同时存在不同的意义。 与一个线程同时发生的一组事件(或内存值)不同时发生。 因此,在宽松的内存模型中,所有线程仍然观察每个内存位置的相同历史(即值序列)。 但他们可能观察到不同的记忆图像(即,所有记忆位置的值的组合)。 即使两个不同的存储位置是由同一个线程按顺序写入的,这两个新写入的值可能会被其他线程以不同的顺序观察到。

[来自维基百科的图片]

熟悉爱因斯坦的相对论的读者会注意到我所指的是什么。 将Minkowski的话翻译成内存模型领域:地址空间和时间是地址空间时间的阴影。 在这种情况下,每个观察者(即线程)都会将事件(即内存存储/加载)的阴影投影到他自己的世界线(即他的时间轴)和他自己的同时性平面(他的地址空间轴) 。 C ++ 11内存模型中的线程对应于在狭义相对论中彼此相对移动的观察者 。 序贯一致性对应于伽利略时空 (即所有观察者都同意事件的一个绝对秩序和全局同时性感)。

记忆模型和狭义相对论之间的相似之处源于这两个事实,它们都定义了一组部分有序的事件,通常称为因果集。 某些事件(即记忆存储)可能会影响(但不会受其他事件影响)。 一个C ++ 11线程(或物理观察者)不过是一个链(即一个完全有序的集)的事件(例如,内存加载和存储到可能不同的地址)。

在相对论中,一些秩序恢复到部分有序事件的看似混沌的画面,因为所有观察者都认同的唯一时间顺序是“时间性”事件之间的排序(即那些原则上可以被任何粒子走向较慢的事件所连接的事件比光在真空中的速度)。 只有时间般的相关事件是不变的命令。 物理时间,克雷格卡伦德

在C ++ 11内存模型中,类似的机制(获取 - 释放一致性模型)用于建立这些局部因果关系

为了提供内存一致性的定义和放弃SC的动机,我将引用“内存一致性和高速缓存一致性入门”

对于共享内存机器,内存一致性模型定义其内存系统的架构可见行为。 单个处理器核心的正确性标准在“ 一个正确的结果 ”和“ 许多不正确的替代方案 ”之间划分行为。 这是因为处理器的体系结构要求线程的执行将给定的输入状态转换为单一明确定义的输出状态,即使是在无序的内核上。 然而,共享内存一致性模型涉及多线程的加载和存储,并且通常允许许多正确的执行,而不允许许多(更多)不正确的执行。 多次正确执行的可能性是由于ISA允许多个线程同时执行,通常来自不同线程的许多可能的合法交错指令。

宽松弱的内存一致性模型是由强模型中的大多数内存排序是不必要的。 如果一个线程更新十个数据项然后一个同步标志,程序员通常不关心数据项是否按照彼此的顺序更新,而只是在更新标志之前更新所有数据项(通常使用FENCE指令实现)。 宽松的模型试图捕获这种增加的订购灵活性,并且只保留程序员“ 要求 ”的订单,以获得更高的性能和SC的正确性。 例如,在某些体系结构中,每个内核使用FIFO写缓冲区来保存已提交(退役)存储的结果,然后将结果写入缓存。 这种优化增强了性能,但违反了SC。 写入缓冲区隐藏了服务商店未命中的延迟。 因为商店很常见,所以能够避免大部分停滞是一个重要的好处。 对于单核处理器,通过确保地址A的加载将最新存储的值返回给A,即使存储器A中的一个或多个存储位于写入缓冲区中,也可以使体系结构不可见。 这通常是通过将最近存储到A的值绕过来自A的加载来完成的,其中“最近的”由程序顺序确定,或者如果存储到A的存储在写入缓冲器中则阻止A的加载。 当使用多个内核时,每个内核都有自己的旁路写入缓冲区。 如果没有写入缓冲区,硬件是SC,但是使用写入缓冲区并非如此,因此在多核处理器中可以在架构上显示写入缓冲区。

如果一个内核有一个非FIFO写入缓冲区,允许商店以不同于它们输入顺序的顺序离开,那么可能会发生存储 - 存储重新排序。 如果第一个商店在高速缓存中未命中,而第二个命中或第二个商店可能与先前的商店合并(即在第一个商店之前),则可能会发生这种情况。 负载重新排序也可能发生在动态调度的内核上,这些内核执行程序顺序之外的指令。 这可以像对另一个核心上的商店重新排序一样行事(你能想出两个线程之间的示例交错?)。 使用较晚的存储重新排序较早的加载(加载存储重新排序)可能会导致许多不正确的行为,例如在释放锁定后加载一个值(如果存储为解锁操作)。 请注意,即使内核按程序顺序执行所有指令,也可能由于在通常实现的FIFO写缓冲区中进行本地旁路而引起存储器加载重新排序。

由于缓存一致性和内存一致性有时会混淆,因此也有这样的引用是有益的:

与一致性不同, 缓存一致性对于软件来说既不可见也不需要。 Coherence试图使共享内存系统的缓存在功能上不可见,就像单核系统中的缓存一样。 通过分析加载和存储的结果,正确的一致性可确保程序员无法确定系统是否以及在何处具有高速缓存。 这是因为正确的一致性确保缓存永远不会启用新的或不同的功能行为(程序员仍然可以使用时间信息推断可能的缓存结构)。 高速缓存一致性协议的主要目的是维护每个内存位置的单写多读器(SWMR)不变量。 一致性和一致性之间的一个重要区别是,在每个内存位置基础上指定了一致性,而针对所有内存位置指定了一致性。

继续我们的心理图像,SWMR不变量对应于物理要求,即最多只有一个粒子位于任何一个位置,但可以有任意位置的无限数量的观察者。

Question

C ++ 11引入了标准化的内存模型,但究竟是什么意思? 它将如何影响C ++编程?

这篇文章 (由Gavin Clarke引用Herb Sutter )说,

内存模型意味着C ++代码现在拥有一个标准化的库来调用,无论编译器是谁制作的以及它运行在哪个平台上。 有一种标准的方法来控制不同线程与处理器内存的对话。

Sutter说:“当你谈论的是跨标准的不同内核分割代码时,我们正在谈论内存模型。我们将会优化它,而不会破坏人们在代码中所做的下列假设。

好吧,我可以记住在线提供的这个和类似的段落(因为我从出生开始就有自己的记忆模型:P),甚至可以回答其他人提出的问题,但说实话,我并不完全明白这一点。

所以,我基本上想知道的是,C ++程序员甚至在之前就开发了多线程应用程序,因此,如果它是POSIX线程,Windows线程或C ++ 11线程,它又有什么关系? 有什么好处? 我想了解低级细节。

我也感觉到C ++ 11内存模型与C ++ 11多线程支持有某种关系,因为我经常将这两者结合在一起。 如果是这样,究竟如何? 他们为什么要相关?

由于我不知道多线程的内部工作原理,以及一般的内存模型意味着什么,请帮助我理解这些概念。 :-)




如果您使用互斥锁来保护您的所有数据,那么您确实不需要担心。 互斥体一直提供足够的订购和可视性保证。

现在,如果您使用原子或无锁算法,则需要考虑内存模型。 内存模型准确地描述了原子提供排序和可视性保证的时间,并为手工编码保证提供了便携式围栏。

以前,原子将使用编译器内在函数或一些更高级别的库来完成。 使用特定于CPU的指令(内存屏障)可以完成围栏。




这意味着标准现在定义了多线程,并且它定义了在多线程环境中发生的事情。 当然,人们使用不同的实现,但这就像问我们为什么应该有一个std::string当我们都可以使用一个home-rolled string类时。

当您谈论POSIX线程或Windows线程时,实际上您正在讨论x86线程时,这有点虚幻,因为它是一个可同时运行的硬件功能。 无论您使用的是x86还是ARM,还是MIPS ,或其他任何您可以想到的,C ++ 0x内存模型都可以保证。






Related