跳转至内容

并行计算与计算机集群/内存

来自 Wikibooks,开放世界中的开放书籍

在早期单 CPU 机器中,CPU 通常位于一个专用的系统总线之间,连接自身与内存。每次内存访问都将通过总线,并直接从 RAM 返回。随着 CPU 速度的提高,RAM 和总线速度也随之提高。由于涉及到的电子元件,内存访问和 RAM 响应周期成为瓶颈,迫使 CPU 浪费宝贵的计算周期等待响应返回(称为延迟)。

为了克服这种延迟,一些设计涉及在系统总线上放置一个内存控制器,它接收来自 CPU 的请求并返回结果 - 内存控制器会在本地保留最近访问的内存部分的副本(一个缓存),因此能够更快地响应许多涉及顺序(例如程序代码)或局部分散的请求(例如程序代码定期访问的变量)。

随着 CPU 变得更快,即使从内存控制器缓存区域检索 RAM 所涉及的延迟也变得非常昂贵。下一阶段的发展将看到内存控制器被放置在 CPU 本身芯片的铸造中(在许多情况下,将内存控制器保留在总线上作为副本),从而诞生了 CPU 缓存。由于 CPU 缓存位于 CPU 芯片上,因此 CPU 内核和 CPU 内存控制器之间的总线长度大大减少,内存控制器可以更快地响应,从而减少 CPU 的延迟,直到需要非缓存元素为止。

后来,进一步的工程改进意味着 CPU 缓存的速度开始与 CPU 本身的速度相匹配,允许每个指令周期都无需来自其缓存的等待状态。不幸的是,这种设计的成本很高,因此第二层或二级缓存被放置在芯片上,并使用更便宜(更旧)的设计制成:CPU 现在有了一级二级缓存。由于二级缓存更便宜,因此可以比一级缓存以更大的数量进行芯片铸造,并且在保持合理的生产成本和节省普通 RAM 访问速度的同时,也节省了与 RAM 访问速度相比从二级缓存填充有限的一级缓存的成本。

RAM 和多个 MPU

[编辑 | 编辑源代码]

在最简单的多 MPU 设计中,每个 MPU 没有缓存,并直接位于总线上,允许访问 RAM(直接 RAM 访问或通过内存控制器)。从单 CPU 设计扩展而来,一个仅读取信息的 MPU 多设计可以简单地设计:每个 MPU 在总线上发送其请求,但需要一种方法来识别请求来自何处。为此,必须以某种方式扩展请求以标记请求,最明显的标识是 MPU 所在的插槽(如 CPU 启动过程中 BIOS 所指示)。返回到 MPU 的响应,如果没有其 ID,则可以安全地忽略。识别过程通常由一个控制器执行,该控制器不是多 MPU 的一部分,而是在总线上一个单独的专业 MPU,称为可编程中断控制器 (PIC)。

不幸的是,无法写入任何数据的系统在灵活性方面将非常有限,因此必须能够将数据写回 RAM。写回 RAM 的机制再次成为单 CPU 系统的简单扩展:写回 RAM 指示请求来自哪个 MPU 插槽,任何确认响应都可以以相同的方式正确识别,允许任何单个 MPU 知道其写入请求是否成功。

唉,有一个重大的缺陷:如果两个(或更多)MPU 同时处理同一块数据会发生什么?每个 MPU 从 RAM 读取数据,根据各个 MPU 正在处理的程序代码处理数据,并将结果写回 RAM。问题在于哪个 MPU 最后执行了写入请求,因为正是该 MPU 的结果将驻留在 RAM 中,而不是第一个。为了更清楚地说明这个问题,请考虑以下情况,其中活动引用同一个内存位置(一个数字)。

活动 MPU 1 MPU 2
从程序代码读取内存 被指示增加 RAM 位置 0001 被指示减少 RAM 位置 0001
读取代码引用的内存数据 接收值为 1 接收值为 1
处理指令 计算结果为 2 计算结果为 0
从程序代码读取内存 被指示将结果放入 RAM 位置 0002 被指示将结果放入 RAM 位置 0002
处理指令 将值 2 放入 RAM 位置 0002 将值 0 放入 RAM 位置 0002

如果这是一个 SMP 系统,则两个 MPU 将以相同的速度进行处理,并且两个 MPU 都将同时到达管道的每个阶段。结果,每个 MPU 都将尝试同时将其对数据的解释写回 RAM 位置 0002。之后 RAM 位置 0002 的值是多少?不幸的是,没有保证,除非首先实施其他方法来强制此竞争条件不发生。

非缓存 MPU

[编辑 | 编辑源代码]

在非缓存 MPU 中,MPU 将在总线上请求锁定一组内存范围,以防止其他处理器更新内存区域,直到锁定 MPU 完成其自身任务。这种方法效率低下,因为锁定阻止了第二个(或后续)MPU 访问内存区域。

引入缓存

[编辑 | 编辑源代码]

随着在处理器中引入缓存,问题最初变得更加普遍。回到具有缓存的单处理器体系结构以更好地说明这个问题,缓存提供了对已访问的内存区域的快速访问。MPU 从一个完全标记为无效的缓存开始,从那里,对 RAM 字节的任何请求都将自动加载 RAM 的页面到缓存中(仅填充缓存的一部分)。当处理器访问非缓存 RAM 的不同部分时,该 RAM 页面也将与已缓存的页面一起加载。该过程持续进行,直到缓存充满了有效页面。

一旦缓存已满,并且处理器需要访问尚未缓存的 RAM 页面,它必须决定在缓存的哪个位置写入此新页面:无论页面写入何处,它都将覆盖现有的缓存 RAM。那么,处理器如何决定?使用两种基本方法:保留最近访问的页面和随机选择。在第一种方法中,MPU 的内存控制器跟踪缓存页面的最近访问顺序,并选择最旧的缓存页面作为要覆盖的页面。

在特别复杂的算法中,最旧访问的缓存页面可能不是最方便删除的页面:代码可能会在 RAM 页面之间跳跃,从许多不同的页面中访问很少的数据,同时很少访问数据集(与所有程序代码相比)或反之亦然。由于在 RAM 中跳跃,缓存会丢失其缓存中的数据集(RAM 最常访问的单个页面)。在这种情况下,处理器最好丢失任何一个不常访问的程序代码页面,而不是数据集本身(请注意,有一个非常有力的论点说明任何以这种方式运行的程序要么程序编写不当,要么编译不当)。

除了缓存从 RAM 读取的数据外,还可以通过处理器类似地缓存数据写回 RAM,然后在刷新页面时将其作为整个页面写入。对于单 MPU 系统,写入缓存对系统中的其他组件没有影响,因此可以在写回任何修改的缓存页面时花费时间(请注意,在 CPU 将修改后的数据写回 RAM 之前,RAM 保持不变)。这种方法称为延迟写入

简单的缓存写入

[编辑 | 编辑源代码]

回到多处理器系统和使用缓存的问题,这个问题变得更加明显:当任何处理器写入RAM的一部分时,数据会被放入其缓存中,而不是写回RAM。如果另一个处理器尝试读取同一部分RAM,它接收到的数据将过时。必须使每个处理器缓存的内容彼此保持一致(或相干,因此称为缓存一致性)。已经设计了各种方法来确保缓存尽可能保持一致。最简单的方法是确保任何写操作都通过总线立即将数据推回RAM,体系结构中的所有处理器都在监控总线上的写操作:当在总线上看到写操作时,处理器检查其自己的缓存中是否存在同一页RAM,如果已缓存,则该页会立即从RAM中重新加载。对于使用数据公共子集的大型并行系统来说,这是一种非常昂贵的方法:当单个处理器写入数据段时,每个其他缓存了该数据的处理器都必须重新请求数据。通过其他处理器在数据写回RAM时从总线上读取数据(称为嗅探)可以大大改进该方法,从而无需潜在的n - 1个请求来读取同一RAM位置。在更高级的改进中,嗅探应用于所有总线数据包,这样任何单个读取操作都可能被缓存到体系结构中的所有处理器中。

基于目录的写入

[编辑 | 编辑源代码]

无论是否使用缓存,基于目录的写入技术都涉及所有RAM由一个位于中心的内存管理器控制。所涉及的处理器只遵守内存管理器提供给它们的信息,写请求可以来自内存管理器到单个处理器,请求立即写回数据。

MOESI & 前身

[编辑 | 编辑源代码]

存在更高级的缓存一致性技术,这些技术从一组类似的功能开始。几乎普遍地,这些方法中的每一种都将所有RAM页面标记为修改、共享或无效之一。更复杂的方法引入了独占和/或所有者的标记。这些方法的名称通常以实现方法的首字母缩写来表示:MSI、MESI、MOSI和MOESI。

状态 描述
已修改 该页面被标记为已更改,并驻留在处理器的缓存中,但RAM中没有有效副本。在也使用所有者状态的方法中,其他任何处理器都不需要修改的页面。
所有者 修改状态的补充,其中一个原本已修改的页面被其他处理器需要和使用。在大规模并行系统中,体系结构的内存控制器保存有关哪些其他处理器需要该页面的信息,因此能够跨越总线边界分发必要的页面。
独占 拥有该页面的处理器是唯一缓存了该页面的处理器。没有其他处理器请求或保留该页面。
共享 标记为共享的页面表示一个或多个处理器在其缓存中拥有该页面。如果该方法使用独占,则共享表示多个处理器拥有该页面。
无效 无效的缓存页面是指不能依赖其有效数据的页面。处理器通常只以无效页面开始(一些系统在引导之前预加载处理器的缓存中一些简单的引导代码)。一些MOESI实现会在将页面写入RAM时将其标记为无效。

避免竞争条件

[编辑 | 编辑源代码]

无论使用哪种内存管理技术(并且存在比上面描述的更多变体和技术),在缓存中生成无效数据的可能性仍然存在。但是,这些可能性是由于编程技术差或理解错误以及/或编译差的性质造成的。这将在软件章节中深入介绍。

统一内存访问

[编辑 | 编辑源代码]

顾名思义,统一内存访问(UMA)描述了一种所有对内存的访问都以统一、平等的方式执行的方法。在UMA设计中,RAM通常被集中在一起,并且通常有一个内存控制器来帮助CPU访问RAM。UMA是SMP中实现RAM访问最常见的方法。

非统一内存访问

[编辑 | 编辑源代码]

在并行处理体系结构和集群中非常流行。非统一内存访问(NUMA)体系结构是一个系统(单机或集群),它包含一个分布在整个系统中的单个RAM块。例如,一个四处理器体系结构可以由两对处理器组成,每对处理器可以直接访问2GB的RAM。在这种情况下,每对处理器都连接到一个本地总线及其共享的2GB。对另一对的2GB的访问是通过总线间链接(通常是内存控制器)执行的。

NUMA系统的一个更极端的示例通常用于试图呈现单一系统映像的集群:集群中的每个节点都包含一部分系统RAM,集群软件是本地化的内存控制器。存在于集群的一个节点上的进程必须访问存储在另一个节点上的内存部分。与第一个节点通过(其本地化的集群内存控制器)访问其自己的真实RAM相比,尝试访问备用节点上的RAM部分涉及一个更复杂的情况:第一个节点必须向第二个节点发出请求,要求它通过网络发送RAM;第二个节点必须检查该部分是否已被另一个(第三个)节点修改;如果正在被修改,则第二个节点必须请求第三个节点放弃RAM访问;第三个节点将修改写回第二个节点,最后第二个节点将RAM部分释放给请求节点。如果两个节点需要频繁修改访问同一RAM部分,则结果会严重影响集群以有价值的方式处理数据的能力。值得庆幸的是,存在软件技术(与这里描述的技术非常相似)来减少这种情况的影响。

进一步阅读

[编辑 | 编辑源代码]
华夏公益教科书