assembly - 内存屏障是否确保缓存一致性已经完成?

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

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

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

set x to 5
some other instruction

现在当set x to 5执行,缓存值x将设置为 5 ,这将导致缓存一致性协议(protocol)采取行动并使用新值 x 更新其他内核的缓存。 .

现在我的问题是:何时 x实际上设置为 5Thread A的缓存,其他内核的缓存是否在 some other instruction 之前更新被执行?还是应该使用内存屏障来确保?:
set x to 5
memory barrier
some other instruction

注:假设指令是按顺序执行的,还假设当set x to 5被执行,5立即放置在线程 A 的缓存中(因此该指令未放置在队列中或稍后执行的内容中)。

最佳答案

x86 架构上存在的内存屏障 - 但一般来说这是正确的 - 不仅保证所有先前的加载或存储在执行任何后续加载或存储之前完成 - 它们还保证存储已成为 全局可见

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

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

然而,要了解障碍,值得一看当前的实现。
请注意,英特尔可以随意将现代实现颠倒过来,只要它保持可见行为正确即可。

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

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

在特定事件(如序列化事件、异常、屏障的执行或缓冲区耗尽)时,CPU 会刷新存储缓冲区。
冲洗总是按顺序排列 - 先入先出。

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

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

  • 缓存类型仍然影响可见性。
    全局可见并不意味着在内存中可见,它意味着从其他内核的负载可以看到它的地方可见。
    如果内存区域是 WB 可缓存的,则加载可能会在缓存中结束,因此它在那里是全局可见的 - 仅对于知道缓存存在的代理。 (但请注意,现代 x86 上的大多数 DMA 都是缓存一致的)。
  • 这也适用于非连贯的 WC 缓冲区。
    WC 不保持连贯性 - 它的目的是将存储合并到顺序无关紧要的内存区域,例如帧缓冲区。这还不是真正全局可见的,只有在写入组合缓冲区被刷新后,内核之外的任何东西才能看到它。
  • sfence 正是这样做的:WAITING所有先前的存储在本地完成,然后排空存储缓冲区。
    由于存储缓冲区中的每个存储都可能丢失,因此您会看到此类指令有多么繁重。 (但包括稍后加载的乱序执行可以继续。只有 mfence 会阻止稍后加载在全局可见(从 L1d 缓存中读取),直到存储缓冲区完成提交到缓存之后。)

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

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

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

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

    如果 L 最初未处于 S 状态,则相应地更改协议(protocol)(例如,如果 L 处于 Exclusive 状态,则总线上没有事务完成 [1])。

    此时写入完成,sfence 完成。

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

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

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

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

    我不得不再次强调缓存一致性、全局可见性和内存排序是三个不同的概念。
    第一个保证第二个,由第三个强制执行。
    Memory ordering -- enforces --> Global visibility -- needs -> Cache coherency
    '.______________________________'_____________.'                            '
                     Architectural  '                                           '
                                     '._______________________________________.'
                                                 micro-architectural
    

    脚注:
  • 按程序顺序。
  • 那是一种简化。在 Intel CPU 上,mov [eax+ebx*2+4], ecx 解码为两个独立的 uops:store-address 和 store-data。 store-address uop 必须等待 eaxebx 准备就绪,然后将其分派(dispatch)到能够计算其地址的执行单元。该执行单元 writes the address into the store buffer ,因此稍后的加载(按程序顺序)可以检查存储转发。

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

    这可能发生在地址已知之前或之后,因为存储缓冲区条目可能按程序顺序保留,因此一旦最终知道所有内容的地址,存储缓冲区(又名内存顺序缓冲区)可以跟踪加载/存储顺序,并检查重叠。 (对于最终违反 x86 内存排序规则的推测性加载,如果另一个内核使它们在架构上允许加载的最早点之前加载的缓存行无效。这会导致 a memory-order mis-speculation pipeline clear 。)
  • 关于assembly - 内存屏障是否确保缓存一致性已经完成?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42746793/

    相关文章:

    ubuntu - 为 x86 构建 FreeRTOS

    linux - 如何在/mnt/中创建文件读/写权限?

    operating-system - 操作系统如何创建唯一的文件句柄?

    c++ - 调查 visual studio 程序集输出

    java - 从 Java 确定是 x86 还是 x64 系统

    c - 我们如何通过键盘端口重新启动

    c - gcc 8.2+ 在调用 x86 之前并不总是对齐堆栈?

    c - asm ("nop");作品?

    c - linux中的signal,int 0x80是谁调用的?

    c - C 中检查两个整数中至少有一个为零的最有效方法是什么?