【问题标题】:Does a memory barrier ensure that the cache coherence has been completed?内存屏障是否确保缓存一致性已完成?
【发布时间】:2017-08-02 11:24:12
【问题描述】:

假设我有两个线程来操作全局变量x。每个线程(或者我想的每个核心)都会有一个x 的缓存副本。

现在说Thread A 执行以下指令:

set x to 5
some other instruction

现在当set x to 5被执行时,x的缓存值将被设置为5,这将导致缓存一致性协议起作用并使用@987654329的新值更新其他内核的缓存@。

现在我的问题是:当xThread A 的缓存中实际设置为5 时,是否在执行some other instruction 之前更新了其他内核的缓存?还是应该使用内存屏障来确保这一点?:

set x to 5
memory barrier
some other instruction

注意:假设指令是按顺序执行的,也假设当set x to 5被执行时,5立即被放到线程A的缓存(因此指令没有放在队列中或稍后执行的东西中)。

【问题讨论】:

  • 只是猜测:没有。 IMO 更新其他内核的缓存需要一个以上的周期,因此您必须在集合上使用lock,等待它并使其正确分发。如果没有锁,线程 B 可能会看到部分修改,甚至部分覆盖x(甚至完全覆盖它或看到完整的旧值)。并且内存屏障变体将 IMO 无济于事,如果两个线程都写入变量,没有锁定 w/屏障,当每个线程将写入它的不同部分时,您仍然可能以来自两个线程的组合值结束。
  • 您是在问同步方法是否确保缓存在其他处理器中得到更新?
  • @Tony Tannous 是的。例如:当Thread A 解锁一个互斥锁时,解锁代码是否包含一个内存屏障,以确保在实际使互斥锁可用于其他线程锁定之前已更新其他内核的现金?所以当Thread B锁定互斥锁时,Thread B可以确定Thread A对全局变量所做的所有修改都会被Thread B看到?
  • 非常硬件的东西,可能是特定于实现的(一代 x86 可能有不同的答案),但都应该有据可查。在您的系统中,核心在哪里聚集在一起? L1、L2、系统内存?对于每个未共享的层,为了将这些项目推出一层,文档说了什么?最重要的是,当您尝试或未尝试这些事情时发生了什么,它对您有用吗?
  • FWIW,缓存一致性通常不会按照您的建议工作。修改值的 CPU 通常不会在每次修改时将该值“推送”到其他 CPU 的缓存中。相反,在修改该值之前,其他 CPU 缓存中的副本无效(如果有的话),然后 CPU 可以随意修改该值,直到其他 CPU 需要该值为止。然后,other CPU 触发缓存一致性事务以获取修改后的值……至少在大多数类似 MESI 的系统中。是拉,不是推。

标签: assembly x86 operating-system cpu-cache memory-barriers


【解决方案1】:

如果其他处理器的缓存中没有 X,则在处理器 A 上执行 x=5 将不会更新任何其他处理器中的缓存。如果处理器 B 读取变量 X,处理器 A 将检测到读取(这称为窥探)并将在总线上为处理器 B 提供数据 5。现在处理器 B 将在其缓存中具有值 5。如果没有其他处理器读取变量 X,那么它们的缓存将永远不会更新为新值 5。

【讨论】:

  • 这是一个真正具有误导性的描述。 x=5 存储将在修改其副本之前使行的任何其他缓存副本无效(即,它获得独占所有权);这就是其他内核如何知道他们需要重新获取该值而不是使用本地缓存的值的方式。您听起来好像它们仍然具有旧的缓存值(使用 MESI 缓存一致性的连贯缓存不可能),但不知何故,它们仍然发出写入核心可以看到的请求。
  • 在一个核心使所有其他核心的行副本无效之后,是的,如果没有其他核心读取x,那么他们将不会为它缓存新的(或任何)值。跨度>
  • 谢谢彼得,没错。我假设没有其他处理器缓存了 X。将编辑和澄清。
  • 理论上这是缓存工作的一种可能方式(en.wikipedia.org/wiki/MESI_protocol 是这样描述的),但这是一个关于内存屏障指令的 x86 问题。您的回答没有提到存储缓冲区或障碍。该窥探模型与 CPU 的实际工作方式不匹配。让每个核心窥探所有其他核心完成的核心外负载根本无法扩展。它们并非都连接到单个共享总线到内存(或 L3),它们都自然地看到所有其他请求。例如Intel CPU 在内核之间使用环形总线,使用 L3 标签作为监听过滤器。
  • 在我所说的 Intel CPU 上,它在 L1 然后 L2 中丢失,然后通过环形总线发送一条消息以从 L3 请求该线路。如果另一个核心已经写回,它可能会到达那里,但如果没有,它会在 L3 中丢失,并且 L3 标签指示哪个核心拥有该行的修改副本,因此 L3 缓存控制器可以通过环形总线发送共享请求到该核心将该高速缓存行写回 L3,并满足该其他核心的负载。这就是我所说的 L3 标签充当窥探过滤器的意思:不是所有核心都在窥探自己,而是有一个共享缓存知道谁拥有什么
【解决方案2】:

,内存屏障并不能确保缓存一致性已经“完成”。它通常不涉及一致性操作根本,并且可以推测性地执行或作为无操作执行。

它只强制执行屏障中描述的排序语义。例如,一个实现可能只是在存储队列中放置一个标记,这样对于比该标记更早的存储就不会发生存储到加载转发。

尤其是英特尔,已经为正常加载和存储(编译器生成并在汇编中使用的类型)提供了强大的内存模型,其中唯一可能的重新排序是稍后加载通过较早的存储。在 SPARC 内存屏障的术语中,除了StoreLoad 之外的每个屏障都已经是no-op

在实践中,x86 上的有趣屏障附加到LOCKed 指令,并且这样的指令的执行根本不需要涉及任何缓存一致性。如果行已经处于独占状态,CPU 可以简单地执行指令,确保在操作进行期间(即在读取参数和回写结果之间)不释放行的独占状态然后只处理防止存储到加载转发破坏LOCK 指令附带的总排序。目前他们通过排空存储队列来做到这一点,但在未来的处理器中,即使这也可能是推测性的。

内存屏障或屏障+操作的作用是确保其他代理以遵守屏障所有限制的相对顺序看到该操作。当然,这通常不涉及将结果推送到其他 CPU,作为您问题所暗示的一致性操作。

【讨论】:

    【解决方案3】:

    x86 架构上存在的内存屏障——但通常情况下确实如此——不仅保证所有先前的1 加载或存储在执行任何后续加载或存储之前完成——他们还保证这些商店已经全球可见

    全局可见意味着其他缓存感知代理(如其他 CPU)可以看到存储。
    如果目标内存已被标记为不强制立即写入内存的缓存类型,则其他不知道缓存的代理(例如支持 DMA 的设备)通常不会看到存储。
    这与屏障本身无关,这是 x86 架构的一个简单事实:程序员可以看到缓存,而在处理硬件时,它们通常会被禁用。

    英特尔特意对障碍的描述通用,因为它不想将自己与特定的实施联系起来。
    您需要抽象地思考:全局可见意味着硬件将采取所有必要的步骤使商店全局可见。期间。

    然而,要了解障碍,有必要看看当前的实现。
    请注意,只要保持可见行为正确,英特尔就可以随意颠倒现代实施。

    x86 CPU 中的存储在内核中执行,然后放入 存储缓冲区
    例如mov DWORD [eax+ebx*2+4], ecx,一旦解码就会停止,直到eaxebxecx 准备就绪2,然后将其分派到能够计算其地址的执行单元。
    执行完成后,存储变成了一对 (address, value),被移动到 存储缓冲区
    据说商店在本地完成(在核心中)。

    存储缓冲区允许 CPU 的 OoO 部分忘记存储并认为它已完成,即使尚未尝试写入也是如此。

    在发生特定事件时,例如序列化事件、异常、屏障的执行或缓冲区耗尽,CPU 会刷新存储缓冲区。
    刷新总是有序的 - 先入先出。

    存储从存储缓冲区进入缓存领域。
    如果目标地址被标记为 WC 缓存类型,它可以被组合到另一个称为 Write Combining 缓冲区 的缓冲区中(然后绕过缓存写入内存),它可以被写入如果缓存类型是 WB 或 WT,则 L1D 缓存、L2、L3 或 LLC(如果不是前一种)。
    如果缓存类型为UC或WT,也可以直接写入内存。


    今天这就是成为全球可见的意思:离开存储缓冲区。
    请注意两件非常重要的事情:

    1. 缓存类型仍会影响可见性。
      全局可见并不意味着在内存中可见,它意味着在其他内核的负载会看到它的地方可见。
      如果内存区域是 WB 可缓存的,则加载可能会在缓存中结束,因此它在此处是全局可见的 - 只有代理知道缓存的存在。 (但请注意,现代 x86 上的大多数 DMA 都是缓存一致的)。
    2. 这也适用于不连贯的 WC 缓冲区。
      WC 没有保持连贯性 - 它的目的是将存储合并到顺序无关紧要的内存区域,例如帧缓冲区。这不是真正全局可见的,只有在写入组合缓冲区被刷新后,核心之外的任何东西才能看到它。

    sfence 正是这样做的:等待所有以前的存储在本地完成,然后清空存储缓冲区。
    由于存储缓冲区中的每个存储都可能会丢失,因此您会看到此类指令有多么繁重。 (但包括后续加载在内的乱序执行可以继续。只有mfence 会阻止以后的加载在全局可见(从 L1d 缓存中读取),直到存储缓冲区完成提交到缓存之后。)

    但是sfence 是否等待存储传播到其他缓存中?
    嗯,没有。
    因为没有传播——让我们从高级的角度来看看写入缓存意味着什么。

    缓存在所有处理器之间通过 MESI 协议保持一致(MESIF 用于多插槽 Intel 系统,MOESI 用于 AMD 系统)。
    我们只会看到 MESI。

    假设写入索引缓存行 L,并假设所有处理器在其缓存中都有此行 L 具有相同的值。
    此行的状态是Shared,在每个 CPU 中。

    当我们的存储进入缓存时,L 被标记为 Modified,并且在内部总线(或多插槽 Intel 系统的 QPI)上进行特殊事务以使其他处理器中的行 L 无效.

    如果 L 最初不是处于 S 状态,则相应地更改协议(例如,如果 L 处于状态 Exclusive 上没有事务公共汽车已经完成[1])。

    此时写入完成,sfence 完成。

    这足以保持缓存的一致性。
    当另一个 CPU 请求行 L 时,我们的 CPU 侦听该请求并将 L 刷新到内存或内部总线,因此另一个 CPU 将读取更新的版本。
    L 的状态再次设置为S

    所以基本上 L 是按需读取的 - 这是有道理的,因为将写入传播到其他 CPU 是昂贵的,并且一些架构通过将 L 写回内存来做到这一点(这是有效的,因为其他 CPU 的 L 处于状态 Invalid 所以它必须从内存中读取它)。


    最后,sfence 等通常都是无用的,这不是真的,相反,它们非常有用。
    只是通常我们并不关心其他 CPU 是如何看待我们进行存储的——但是在没有获取语义的情况下获取锁,例如在 C++ 中定义并使用栅栏实现,是完全疯了。

    您应该像英特尔所说的那样考虑障碍:它们强制执行内存访问的全局可见性顺序。
    您可以通过将障碍视为强制执行顺序或写入缓存来帮助您自己理解这一点。然后,缓存一致性将确保对缓存的写入是全局可见的。

    我不得不再次强调,缓存一致性、全局可见性和内存排序是三个不同的概念。
    第一个保证第二个,由第三个强制执行。

    Memory ordering -- enforces --> Global visibility -- needs -> Cache coherency
    '.______________________________'_____________.'                            '
                     Architectural  '                                           '
                                     '._______________________________________.'
                                                 micro-architectural
    

    脚注:

    1. 按程序顺序。
    2. 这是一个简化。在 Intel CPU 上,mov [eax+ebx*2+4], ecx 解码为两个独立的微指令:存储地址和存储数据。存储地址 uop 必须等到 eaxebx 准备好,然后将其分派到能够计算其地址的执行单元。该执行单元writes the address into the store buffer,因此以后的加载(按程序顺序)可以检查存储转发。

      ecx 准备好时,store-data uop 可以分派到store-data 端口,并将数据写入同一个存储缓冲区条目。

      这可能发生在地址已知之前或之后,因为存储缓冲区条目可能按程序顺序保留,因此存储缓冲区(也称为内存顺序缓冲区)可以跟踪加载/存储顺序一旦所有内容的地址最终知道,并检查重叠。 (对于最终违反 x86 内存排序规则的推测性负载,如果另一个内核使他们在架构上允许加载的最早点之前加载的缓存行无效。这会导致 a memory-order mis-speculation pipeline clear。)

    【讨论】:

    • @IsuruH 存储缓冲区在缓存之前。当 CPU 耗尽存储缓冲区时,它会写入缓存(如果适用),并且每次写入都有权管理 MESI(等)状态。
    • @IsuruH 它影响全局可见性。排序受存储如何进入存储缓冲区的影响。 SB按顺序排空,FIFO。
    • 您从哪里了解到障碍意味着全球可见性? IMO 障碍意味着它们被记录为强制执行的内部访问顺序,但不一定更多。全局可见性(无论您想如何定义它)完全是另一回事。处理器完全有可能执行屏障并且直到一段时间后才能看到先前的存储(我不认为“全局可见”作为读取的含义,不是吗?)。事实上,这正是SFENCE 在 x86 上经常实现的方式:它不会强制任何 store 可见(更一般地说,它在 x86 上几乎是无操作的)。
    • 使用“商店已完成”这样的术语是上述混淆的一部分。它可以解释。我可以争辩说,当存储在 ROB 中不再是推测性时,或者当它到达 DRAM,或者当它到达磁盘以获取内存映射文件等时,存储就完成了。SFENCE not耗尽存储缓冲区! SFENCE 仅适用于绕过存储缓冲区的某些“奇怪”类型的存储。存储缓冲区本身是有序的:这是它首先存在的一个重要原因(也是为了杀死投机存储)。
    • @BeeOnRope 等等...您是说第 11.10 节中英特尔使用术语“存储缓冲区”的地方实际上应该读作“WC 缓冲区”吗?感谢这些链接,我不知道 NT 移入/移出 WC 内存类型是弱排序的(除了绕过缓存)!无论如何,我没有发现任何证据证明sfence 实际上并没有耗尽 SB。当然,它对于重新排序普通商店没有用,仅此一点并不意味着sfence 没有辅助功能(即排序+可见性)我的 Fog 的 inst 表版本没有列出栅栏的延迟。老实说,我很困惑... IDK 怎么想。
    【解决方案4】:

    现在当 set x to 5 被执行时,x 的缓存值将被设置为 5、这将导致缓存一致性协议动作并更新 具有新值 x 的其他内核的缓存。

    有多个不同的 x86 CPU 具有不同的缓存一致性协议(无、MESI、MOESI),以及不同类型的缓存(未缓存、写组合、只写、直写、回写)。

    一般来说,当写入完成时(将 x 设置为 5),CPU 确定正在完成的缓存类型(来自 MTRR 或 TLB),如果缓存行可以被缓存,它会检查自己的缓存以确定什么说明缓存行在(从它自己的角度)。

    然后根据缓存的类型和缓存行的状态来判断数据是直接写入物理地址空间(绕过缓存),还是必须从别处取缓存行同时告诉其他人CPU 使旧副本无效,或者如果它在自己的缓存中具有独占访问权限,并且可以在缓存中修改它而无需告知任何内容。

    CPU 永远不会将数据“注入”到另一个 CPU 的缓存中(并且只会告诉其他 CPU 无效/丢弃他们的缓存行副本)。告诉其他 CPU 使他们的缓存行副本无效/丢弃会使他们在再次需要时获取它的当前副本。

    请注意,这些都与内存屏障无关。

    有 3 种类型的内存屏障(sfencelfencemfence),它们告诉 CPU 在允许以后的存储、加载或两者发生之前完成存储、加载或两者。因为无论如何CPU通常是缓存一致的,所以这些内存屏障/栅栏通常是没有意义的/不必要的。但是,在某些情况下,CPU 缓存不一致(包括“存储转发”、使用写入组合缓存类型时、使用非临时存储时等)。对于这些特殊/罕见的情况,需要内存屏障/栅栏来强制执行排序(如有必要)。

    【讨论】:

    • “因为无论如何 CPU 通常是缓存一致的,所以这些内存屏障/栅栏通常是没有意义的/不必要的” 但是您说内存屏障用于告诉 CPU 完成存储,加载或两者,然后允许以后的存储,加载或两者发生。我已经读过 CPU 可以将存储操作放入队列中并稍后执行它们,因此如果我们希望在继续执行其余指令之前执行它们,我们应该使用内存屏障。我错过了什么吗?
    • 您的回答指出了重点(MESI/MOESI 不会将数据推送到其他缓存中,因此 OP 问题的格式不正确 - 无需等待任何内容完成)但最后一段是错误的.您将内存排序与缓存一致性混淆了。一旦进入缓存,至少对于 x86 系统,数据是全局可见的。但是由于重新排序和存储缓冲区,存储成为全局可见的时间不是程序顺序或存储完成时 -> 因此存在障碍。
    • @Christopher:对于使用正常回写缓存的普通 RAM,CPU 的内存排序确保一切都以理智的方式排序,没有任何障碍/栅栏。 “将存储操作放入队列并稍后执行”是一种相对异常的特殊情况(涉及“写入组合缓存而不是回写”和/或非临时存储),其中故意绕过 CPU 的正常内存顺序(并导致需要屏障/栅栏,因为故意绕过正常的内存排序)。
    • 缓存确实缓存物理地址空间。我认为您试图使用广义术语来涵盖 DRAM 和 I/O 空间,但是一旦存储提交到 L1d 缓存并因此变得全局可见,它就会被写入“物理地址空间”。如果在现代 x86 上仍然可以使用非缓存一致性 DMA,则 IDK;借助集成的内存控制器,设备 DMA 可以(并且确实)在通往 DRAM 的途中正常窥探缓存。
    猜你喜欢
    • 2015-09-06
    • 2021-01-06
    • 2014-09-03
    • 2020-03-18
    • 1970-01-01
    • 2017-08-23
    • 2013-01-18
    • 2012-07-02
    • 1970-01-01
    相关资源
    最近更新 更多