内存屏障是CPU执行的指令还是仅仅是一个标记?

24

我试图理解什么是内存屏障。

根据我目前所知,内存屏障(例如:mfence)用于防止指令在内存屏障之前或之后重新排序。

以下是内存屏障的一个示例:

instruction 1
instruction 2
instruction 3
mfence
instruction 4
instruction 5
instruction 6

现在我的问题是: mfence 指令仅仅是告诉CPU执行指令的顺序的标记吗?还是它像执行其他指令(例如:mov)一样被CPU实际执行的指令呢?


9
这是CPU执行的指令,没有其他类型的指令。 - Ross Ridge
2
请注意,编译器内存屏障如std :: atomic_signal_fence()或GNU C asm("" :::“memory”)仅是源代码中的标记,并且编译为零条指令。它们存在于编译时阻止重新排序,并且在目标架构具有比源语言更强的内存模型时特别有用(例如C ++ - > x86汇编)。 http://preshing.com/20120625/memory-ordering-at-compile-time/ 了解更多信息。 - Peter Cordes
4
我想知道你对那份赏金有什么期望。你得到的答案很清楚。如果你有进一步的问题,请确保明确陈述它们!没有人能猜出哪部分答案让你不满意。 - fuz
1
请注意,您正在建立“指令”和“标记”的可能虚假二分法。为什么不能两者兼备呢?是的,它无可否认地是一条指令,但为什么它不能是一条主要用作“标记”的指令呢? - BeeOnRope
4个回答

26

在CPU遇到的代码中,每个字节序列都是CPU执行的指令,没有其他类型的指令。

您可以在英特尔指令集参考mfence的特定页面中清楚地看到这一点。

MFENCE
将在MFENCE指令之前发出的所有从内存读取和写入内存的指令进行序列化操作。此序列化操作确保在任何MFENCE指令之后的任何加载或存储指令之前,在程序顺序中在MFENCE指令之前的所有加载和存储指令成为全局可见。

MFENCE指令与所有加载和存储指令、其他MFENCE指令、任何LFENCE和SFENCE指令以及任何序列化指令(例如CPUID指令)有序。MFENCE不序列化指令流。 弱排序的内存类型可用于通过诸如乱序发射、推测性读取、写组合和写折叠等技术实现更高的处理器性能。消费者识别或知道数据是弱排序的程度因应用程序而异,并且可能对此数据的生产者不知情。MFENCE指令提供了一种性能高效的方式,可确保在生成弱有序结果的例程和消耗该数据的例程之间进行加载和存储排序。

处理器可以自由地从使用WB、WC和WT内存类型的系统内存区域中推测性地获取和缓存数据。这种推测性获取可以在任何时候发生,不与指令执行绑定。因此,它与MFENCE指令的执行没有顺序关系;数据可以在执行MFENCE指令之前、期间或之后被推测性地带入缓存。

正如您从摘录中所看到的那样,MFence 指令会执行相当多的工作,而不仅仅是一种标记。

23
我将解释mfence对流水线流程的影响。以Skylake流水线为例。考虑以下指令序列:
inst1
store1
inst2
load1
inst3
mfence
inst4
store2
load2
inst5

指令按程序顺序解码为一系列uop,然后所有uop按顺序传递给调度器。通常情况下,在没有栅栏的情况下,所有uop都会无序地发出执行。然而,当调度器接收到mfence uop时,它需要确保在所有上游内存uop变得全局可见之前(这意味着存储已退役并且加载至少已完成),不会执行任何下游内存uop。这适用于所有内存访问,无论所访问的区域的内存类型如何。可以通过使调度器不向存储或加载缓冲区发出任何下游存储或加载uop直到缓冲区被排空,或通过发出下游存储或加载uop并标记它们以便与缓冲区中的所有现有内存uop区分开来来实现此目的。栅栏上方或下方的所有非内存uop仍然可以无序执行。在本例中,一旦store1退役并且load1完成(通过接收数据并将其保存在某个内部寄存器中),则认为mfence指令已完成执行。我认为mfence可能会占用后端(ROB或RS)中的任何资源,并且它可能会转换为多个uop。
英特尔在1999年提交了一项专利,描述了 mfence 的工作原理。由于这是一个非常古老的专利,实现可能已经改变或者在不同的处理器中可能有所不同。在此我将对该专利进行总结。 mfence 被解码成三个μops。不幸的是,目前还不清楚这些μops具体用于什么。然后从保留站分配条目来保存μops,同时从加载和存储缓冲区中分配条目。这意味着加载缓冲区可以容纳真正的加载请求或栅栏(基本上是虚假的加载请求)的条目。同样,存储缓冲区可以容纳真正的存储请求和栅栏的条目。只有在所有先前的加载或存储μops(在各自的缓冲区中)已被退役后,mfence μop才会被调度。当发生这种情况时,mfence μop本身将作为内存请求发送到L1高速缓存控制器。控制器检查是否所有先前的请求都已完成。如果是,则它将被视为NOP,并且μop将从缓冲区中释放。否则,高速缓存控制器将拒绝mfence μop。

5
Skylake将mfence解码为4个微操作(融合和未融合),这些微操作在p2/p3和p4(AGU和store-data端口)上运行。至少在文本上,mfence不必停止后续存储器的执行(并将存储数据放入存储器缓冲区),它只需停止这些存储器在栅栏之后成为全局可见(提交到L1d缓存)。然而,mfence确实需要一种机制来阻止在所有先前的加载/存储器全部变为全局可见之前,后续加载的执行。如果英特尔的实现更加严格,那么这只是一种设计选择。 - Peter Cordes
@PeterCordes 你知道这4个uops的目的吗?只是好奇。 - Hadi Brais
2
顺便问一下,memory-fencesmemory-barriers标签不应该是同义词吗? - Hadi Brais
1
mfence uops与Agner Fog的表格更新:他可能在SKL079的微码修复之前进行了测试,这使得mfence变得更加昂贵。请参见Are loads and stores the only instructions that gets reordered?底部,我最近更新了该内容,因为我偶然发现了mfence如此昂贵的原因解释。也许实现强制停顿一切解决方案的uops不同,但以前只有4个未融合域uops来阻止内存重排序。 - Peter Cordes
1
如果我没记错的话,memory-order 标签应该是关于 ISA 内存模型的,而 memory-model 标签则是关于语言内存模型的。但似乎 memory-order 已经成为了 memory-barriers 的同义词,但实际上不应该是这样的。memory-barriers 标签只应该是 memory-fences 的同义词。memory-order 标签可以是 memory-ordering 的同义词。 - Hadi Brais
显示剩余21条评论

5

mfence是一条指令。

在Linux上使用它:

1/ 编写一个名为mfence.c的文件。

#include <stdio.h>

int main(){
    printf("Disass me\n");
    asm volatile ("mfence" ::: "memory");
    return 0;
}

2/ 编译

gcc mfence.c mfence

3/ 反汇编

objdump -d mfence | grep -A 10 "<main>:"

(注:本文涉及IT技术,建议读者具备相关基础知识。)
000000000000063a <main>:
 63a:   55                      push   %rbp
 63b:   48 89 e5                mov    %rsp,%rbp
 63e:   48 8d 3d 9f 00 00 00    lea    0x9f(%rip),%rdi        # 6e4 <_IO_stdin_used+0x4>
 645:   e8 c6 fe ff ff          callq  510 <puts@plt>
 64a:   0f ae f0                mfence 
 64d:   b8 00 00 00 00          mov    $0x0,%eax
 652:   5d                      pop    %rbp
 653:   c3                      retq   
 654:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
 65b:   00 00 00 

4/注意,在第64行a处,mfence 是一个(3位)指令(0f ae f0)。

因此,它是一种CPU指令(如mov):处理器需要在到达该指令之前解码先前的指令,否则它无法猜测它的对齐方式。

例如,0f ae f0可以出现在地址中,因此CPU无法将其用作标记。

最后,这只是一条老式指令,在其管道中的执行点上,在执行下一条指令之前,它将在管道中进一步同步内存访问。


注意:在Windows中,请使用宏_ReadWriteBarrier来产生一个mfence。


4
您的问题存在错误假设。MFENCE指令不能防止指令重排序(请参见高亮引用)。例如,如果有一串1000个操作寄存器的指令流,并且在中间放置了一个MFENCE指令,则它对CPU重新排序这些指令的方式没有任何影响。
“MFENCE指令与所有加载和存储指令、其他MFENCE指令、任何LFENCE和SFENCE指令以及任何序列化指令(例如CPUID指令)都有顺序关系。MFENCE不会序列化指令流。”
相反,MFENCE指令防止将加载和存储到缓存和主内存的操作重新排序。

1
我认为x86只能合并相邻的存储,因为它必须按顺序将它们提交到L1d缓存中。(x86的内存模型不允许StoreStore重新排序)。但我猜这可以作为一个例子来阻塞后面的加载(但不是后面的ALU指令),直到MFENCE之前的最后一个存储变成全局可见。 - Peter Cordes
@PeterCordes,你是对的。我会删除最后一段。 - Nam San
嗯,这是答案中比较有趣的部分。我们有一些证据表明Skylake确实将相邻的存储合并到同一个缓存行中,但很难测量,因为存储端口吞吐量每个时钟周期只有1个。这个答案没有详细解释停止内存重排序意味着什么,也没有提到存储缓冲区,这是某些人在这里误解的关键(内存屏障既作为标记又作为指令吗?x86 CPU是否重新排序指令?)。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接