《Memory Barriers a Hardware View for Software Hackers》阅读笔记

  CPU 设计者引入内存屏障(memory barriers)是为了应对在多处理器系统(SMP)中,内存引用重排序可能导致的同步问题。尽管重排序可以提高性能,但在某些情况下(如同步原语),正确的操作依赖于有序的内存引用,因此需要使用内存屏障来强制执行顺序。

  要深入理解这个问题,需要了解 CPU 缓存的工作原理,尤其是如何使缓存有效工作。以下是相关内容的概述:

  1. 缓存结构:介绍缓存的基本结构和工作机制。
  2. 缓存一致性协议:描述如何通过缓存一致性协议确保各个 CPU 对内存中每个位置的值达成一致。
  3. 存储缓冲区与失效队列:概述存储缓冲区和失效队列如何帮助缓存和缓存一致性协议实现高性能。

1 缓存结构


  现代 CPU 的速度远远超过现代内存系统的速度。例如,一款 2006 年的 CPU 可能每纳秒能够执行十条指令,而从主内存中获取数据则需要数十纳秒。这种速度差异导致了现代 CPU 中存在多兆字节的缓存,这些缓存与 CPU 关联,通常在几个周期内可以访问。

  缓存的基本概念

  • 缓存行:数据在 CPU 的缓存和内存之间以固定长度的块(称为“缓存行”)流动,通常大小为 16 到 256 字节。
  • 缓存未命中:当 CPU 首次访问某个数据项时,如果该数据不在缓存中,则会发生缓存未命中(cache miss),CPU 需要等待数百个周期才能从内存中获取数据。获取后,该数据会被加载到 CPU 的缓存中,以便后续访问。

  缓存的填充与替换

  • 容量未命中:当缓存填满后,新的未命中请求会导致旧数据从缓存中驱逐,以释放空间。
  • 关联未命中:由于缓存实现为硬件哈希表,缓存行的替换可能导致现有的行被驱逐,这种情况称为关联未命中(associativity miss)。

  写操作的处理
  在进行写操作之前,CPU 必须先使其他 CPU 的缓存中的数据失效,以确保所有 CPU 对数据项的值达成一致。这一过程称为“失效”。如果数据项在 CPU 的缓存中是只读的,写入时会发生“写未命中”(write miss)。因为不同 CPU 使用数据项进行通信(如互斥锁),当其他 CPU 尝试访问被写入的数据项时,可能会出现缓存未命中,这种情况称为“通信未命中”(communication miss)。

  由于 CPU 之间的数据一致性至关重要,必须小心管理缓存中的数据,以避免数据丢失或不同 CPU 之间的值冲突。这些问题通过缓存一致性协议来防止,确保所有 CPU 维护一致的数据视图。

2 缓存一致性协议

  缓存一致性协议用于管理缓存行的状态,以防止数据不一致或丢失。这些协议可能相当复杂,但在这里我们只关注四状态的 MESI 缓存一致性协议。

2.1 MESI 状态

  MESI 代表“修改”(Modified)、“独占”(Exclusive)、“共享”(Shared)和“无效”(Invalid),每个缓存行在该协议中可以处于这四个状态。使用 MESI 协议的缓存会在每个缓存行上维护一个两位的状态标签,以及该行的物理地址和数据。

  • 修改状态(Modified):该行已被对应 CPU 最近存储修改,且该数据在其他任何 CPU 的缓存中都不存在。这意味着该缓存行是这个 CPU 所“拥有”的,只有它拥有最新的数据副本。此时,缓存必须在重用该行存储其他数据之前,将数据写回内存或交给其他缓存。
  • 独占状态(Exclusive):该状态与修改状态相似,唯一的区别是缓存行尚未被对应 CPU 修改,因此内存中的数据副本是最新的。尽管如此,CPU 仍然可以在不咨询其他 CPU 的情况下对该行进行存储,因此该行依然被视为对应 CPU 所拥有。此时,缓存可以在不写回内存的情况下丢弃该数据。
  • 共享状态(Shared):该行可能在至少一个其他 CPU 的缓存中被复制,因此该 CPU 不能在未咨询其他 CPU 的情况下对该行进行存储。与独占状态一样,内存中的数据副本是最新的,缓存可以丢弃该数据,而无需写回内存或交给其他 CPU。
  • 无效状态(Invalid):该行为空,不持有任何数据。当新数据进入缓存时,优先放入状态为“无效”的缓存行。这种做法是首选,因为替换其他状态的行可能导致在未来引用被替换行时出现昂贵的缓存未命中。
      由于所有 CPU 必须保持对缓存行中数据的统一视图,因此缓存一致性协议提供了消息机制,以协调缓存行在系统中的移动。

2.2 MESI 协议消息

  MESI 协议的许多状态转换需要 CPU 之间的通信。如果 CPU 位于单一共享总线上,以下消息就足够了:

  • 读取(Read):包含要读取的缓存行的物理地址。
  • 读取响应(Read Response):包含之前“读取”消息请求的数据,可能由内存或其他缓存提供。如果某个缓存中的数据处于“修改”状态,该缓存必须提供“读取响应”消息。
  • 失效(Invalidate):包含要失效的缓存行的物理地址。所有其他缓存必须从其缓存中移除相应的数据并作出响应。
  • 失效确认(Invalidate Acknowledge):接收到“失效”消息的 CPU 必须在从缓存中移除指定数据后,发送“失效确认”消息。
  • 读取失效(Read Invalidate):包含要读取的缓存行的物理地址,同时指示其他缓存移除该数据。这是“读取”和“失效”的组合消息,要求同时返回“读取响应”和一组“失效确认”消息。
  • 写回(Writeback):包含要写回内存的地址和数据(并可能“窥探”其他 CPU 的缓存)。该消息允许缓存根据需要驱逐“修改”状态的行,以腾出空间存放其他数据。

  共享内存的多处理器系统在底层实际上是一个消息传递计算机。这意味着使用分布式共享内存的 SMP 机器集群在系统架构的两个不同层次上都使用消息传递来实现共享内存。

  1. 如果两个 CPU 同时尝试使同一缓存行失效,会发生什么?
    结论:可能会导致数据不一致性,具体处理依赖于缓存一致性协议。
  1. 当“失效”消息在大型多处理器中出现时,每个 CPU 都必须给予“失效确认”响应。这不会导致“失效确认”响应的风暴完全饱和系统总线吗?
    结论:是的,这种情况可能会导致总线拥塞,因此在设计中需要优化以减少这种情况的发生。
  2. 如果 SMP 机器已经在使用消息传递,为什么还要使用 SMP?
    结论:SMP 提供了更简单的编程模型和更高效的共享内存访问,相比于纯消息传递的系统,SMP 能够更好地利用缓存和处理器之间的直接数据共享。

2.3 MESI 状态图

  MESI 协议中,缓存行的状态会随着协议消息的发送和接收而变化。下面是每个状态转移的简要说明:

  • 转移 (a):缓存行被写回内存,但 CPU 保留该行在缓存中的副本,并且仍然可以修改。此转移需要一个“写回”消息。
  • 转移 (b):CPU 对已独占访问的缓存行进行写操作。这一转移不需要发送或接收任何消息。
  • 转移 ©:CPU 收到针对已修改缓存行的“读取失效”消息。CPU 必须使其本地副本失效,并同时发送“读取响应”和“失效确认”消息。
  • 转移 (d):CPU 对不在其缓存中的数据项执行原子读取-修改-写操作。它发送“读取失效”消息,并通过“读取响应”接收数据,必须在收到所有“失效确认”响应后才能完成转移。
  • 转移 (e):CPU 对之前在缓存中为只读的数据项执行原子读取-修改-写操作,必须发送“失效”消息,并等待所有“失效确认”响应。
  • 转移 (f):其他 CPU 读取该缓存行,数据由该 CPU 的缓存提供,CPU 发送“读取响应”消息。
  • 转移 (g):其他 CPU 读取缓存行的数据项,数据可能来自该 CPU 的缓存或内存,该 CPU 保留只读副本,并发送“读取响应”消息。
  • 转移 (h):CPU 意识到将需要写入缓存行中的数据,因此发送“失效”消息,直到收到所有“失效确认”响应后才能完成转移。
  • 转移 (i):其他 CPU 对仅在该 CPU 的缓存中持有的数据项执行原子读取-修改-写操作,CPU 接收到“读取失效”消息后,将其从缓存中失效,并发送“读取响应”和“失效确认”消息。
  • 转移 (j):CPU 对不在其缓存中的数据项执行存储操作,发送“读取失效”消息,直到收到“读取响应”和所有“失效确认”消息后才能完成转移。
  • 转移 (k):CPU 加载不在其缓存中的数据项,发送“读取”消息,等待相应的“读取响应”后完成转移。
  • 转移 (l):其他 CPU 对此缓存行中的数据项进行存储操作,但由于其他 CPU 持有该行,当前 CPU 只能保持只读状态。接收到“失效”消息后,CPU 发送“失效确认”消息。

硬件如何处理上述延迟的状态转移?
结论:硬件通过使用缓冲机制(如存储缓冲区和失效队列)来处理延迟的状态转移。它允许 CPU 在等待回复消息时继续执行其他操作,从而提高性能。此外,硬件还可以在状态转移过程中保持状态的一致性,确保所有相关的缓存行在完成操作时处于正确的状态。

2.4 MESI 协议示例

  在一个四 CPU 系统中,我们将从一个缓存行的数据的角度,观察其如何通过各个单行直接映射的缓存。以下是该数据流的描述:

  • 初始状态:所有 CPU 的缓存行处于“无效”(Invalid)状态,内存中的数据有效。
  • 操作序列:
    • 1: CPU 0 加载地址 0 的数据,状态变为“共享”(Shared),内存中的数据仍有效。
    • 2: CPU 3 也加载地址 0 的数据,状态在 CPU 0 和 CPU 3 的缓存中均为“共享”,内存中的数据仍有效。
    • 3: CPU 0 加载地址 8 的数据,迫使地址 0 的数据通过写回(Writeback)被驱逐。
    • 4: CPU 2 从地址 0 加载数据,发送“读取失效”消息以获得独占副本,失效 CPU 3 的缓存中的数据(内存中的副本仍有效)。
    • 5: CPU 2 进行存储操作,状态变为“修改”(Modified),此时内存中的副本失效。
    • 6: CPU 1 进行原子增量操作,使用“读取失效”从 CPU 2 的缓存中获取数据并使其失效,CPU 1 的缓存状态变为“修改”,内存中的副本仍失效。
    • 7: CPU 1 读取地址 8 的缓存行,使用“写回”消息将地址 0 的数据写回内存。

3 存储操作导致不必要的停滞

3.1 Store Buffers

  尽管所示的缓存结构在多个重复读取和写入操作中表现良好,但对于某个缓存行的首次写入,性能却相对较差。以下是相关内容的概述:

  1. 写操作的性能问题
      在 CPU 0 对存储在 CPU 1 缓存中的缓存行进行写操作时,CPU 0 必须等待该缓存行到达后才能进行写入。这导致 CPU 0 出现较长时间的停滞。尽管 CPU 0 实际上会无条件覆盖 CPU 1 缓存中的任何数据,但仍然需要等待,这种设计显得不够高效。
  2. 存储缓冲区的引入
      为了防止不必要的写入停滞,可以在每个 CPU 和其缓存之间添加“存储缓冲区”。添加存储缓冲区后,CPU 0 可以将写入记录在其存储缓冲区中,并继续执行其他操作,而无需等待缓存行的到达。

  操作流程:CPU 0 将写入操作记录在存储缓冲区中。一旦 CPU 1 的缓存行传输到 CPU 0,数据将从存储缓冲区移动到缓存行。
  需要解决的复杂问题:尽管存储缓冲区能够提高写入性能,但引入它们会带来一些复杂性。存储缓冲区的有效管理和同步机制是确保数据一致性和避免潜在错误的关键。

3.2 存储转发

  存储转发是一种解决自一致性违例的机制。考虑以下代码,其中变量“a”和“b”最初均为零,且包含变量“a”的缓存行最初由 CPU 1 拥有,而包含“b”的缓存行则由 CPU 0 拥有:

1 a = 1;
2 b = a + 1;
3 assert(b == 2);

  在正常情况下,断言不会失败。然而,如果使用简单架构(如图 5 所示),则可能会出现意外的结果。以下是可能的事件序列:

  • CPU 0 开始执行 a = 1。
  • CPU 0 查找“a”在缓存中,发现缺失。
  • CPU 0 发送“读取失效”消息以获取包含“a”的缓存行的独占权。
  • CPU 0 在其存储缓冲区中记录对“a”的写入。
  • CPU 1 接收到“读取失效”消息,响应并传输缓存行,同时从其缓存中移除该缓存行。
  • CPU 0 开始执行 b = a + 1。
  • CPU 0 收到来自 CPU 1 的缓存行,此时“a”的值仍为零。
  • CPU 0 从缓存中加载“a”,发现其值为零。
  • CPU 0 将其存储队列中的条目应用于新到达的缓存行,将缓存中“a”的值设置为一。
  • CPU 0 将零的值加一,并存储到包含“b”的缓存行中(假设该缓存行已由 CPU 0 拥有)。
  • CPU 0 执行 assert(b == 2),该断言失败。
    问题分析
      问题在于存在两个“a”的副本,一个在缓存中,另一个在存储缓冲区中。这种情况违背了一个重要的保证,即每个 CPU 总是会看到自己的操作仿佛按照程序顺序发生。这种保证对软件开发者来说极具反直觉,因此硬件设计者引入了“存储转发”机制。
      存储转发机制:存储转发允许每个 CPU 在执行加载时同时参考其存储缓冲区和缓存。具体来说:每个 CPU 的存储直接转发到其后续的加载操作,而无需通过缓存。通过引入存储转发,事件序列中的第 8 步会找到存储缓冲区中“a”的正确值 1,从而确保最终的“b”的值为 2,如预期的那样。这种机制有效解决了自一致性问题,确保了 CPU 在操作顺序上的一致性。

3.3 存储缓冲区与内存屏障

  在多处理器系统中,存储缓冲区和内存屏障的引入是为了处理全局内存排序的违例。让我们考虑以下代码序列,其中变量“a”和“b”最初均为零:

void foo(void) {
   
    a = 1;
    b = 1
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值