为什么SFENCE + LFENCE是否等同于MFENCE?

17
我们知道从之前回答的问题中(处理器x86/x86_64中指令LFENCE有任何意义吗?),我们不能使用SFENCE代替MFENCE来提供顺序一致性。
那里的一个答案表明MFENCE= SFENCE+LFENCE,也就是说,LFENCE会做一些事情,没有它我们无法提供顺序一致性。 LFENCE可以防止重排序:
SFENCE
LFENCE
MOV reg, [addr]

-- To -->

MOV reg, [addr]
SFENCE
LFENCE

例如,由Store Buffer提供的机制可以重新排序MOV [addr],regLFENCE-->LFENCEMOV [addr],reg以提高性能,并且因为LFENCE不会阻止它。而SFENCE 禁用了这个机制
是什么机制禁用了LFENCE,使得重新排序变得不可能(x86没有Invalidate-Queue机制)?
SFENCEMOV reg,[addr]-->MOV reg,[addr]SFENCE的重新排序只是理论上可能还是在现实中也可能?如果在现实中可能,那么是哪些机制,它是如何工作的?

我猜测 L/S/M FENCE 是由 内存控制器 强制执行的。Fences 用于协调系统内存和缓存内存。而我认为这种缓存一致性是 内存控制器 的责任。 - Peng Zhang
@Peng Zhang MOESI/MESIF cc-协议会提供缓存一致性,而且是自动的,并且更具体地提供获取-释放一致性。据我所知, L/S/MFENCE与缓存一致性无关,因为 SFENCE仅清除与缓存一致性无关的存储缓冲区。在某些CPU(例如非x86)上,Load FENCE可以刷新Invalidate-Queue,但x86没有此功能。在互联网上,我发现LFENCE对于x86处理器没有意义,也就是说它什么都不做。那么, SFENCE MOV reg,[addr] --> MOV reg,[addr]的重排理论上可能存在,但实际上却不可能,这是真的吗? - Alex
3个回答

27

x86屏障指令可以简要描述如下:

  • MFENCE可以防止任何后续的加载或存储在先前的加载或存储之前成为全局可观察到的。它会在后续的加载1执行之前清空存储缓冲区。

  • LFENCE阻塞指令分派(英特尔的术语),直到所有较早的指令退役。目前,这是通过在后续指令进入后端之前清空ROB(ReOrder Buffer)来实现的。

  • SFENCE仅对存储器进行排序,即防止NT存储器从存储器缓冲区中提前提交。但除此之外,SFENCE就像一个普通的存储器一样通过存储器缓冲区移动。可以将其视为在杂货店结账传送带上放置隔板,以防止NT存储器被提前抓取。它不一定强制存储器缓冲区在从ROB退役之前被清空,因此在其后面放置LFENCE并不等同于MFENCE。

  • 像CPUID(和IRET等)这样的“序列化指令”会在后续指令进入后端之前清空所有内容(ROB、存储器缓冲区),并且会丢弃前端。MFENCE + LFENCE也可以完成后端部分,但真正的序列化指令还会丢弃获取的机器代码,因此可以用于交叉修改代码。(例如,一个加载看到一个标志,你运行cpuid或新的serialize,然后跳转到一个缓冲区,在那里另一个线程在标志上发布存储器之前存储了代码。代码获取保证可以获得新的指令。与数据加载不同,代码获取不遵守x86的常规LoadLoad排序规则。)

在具体订购哪些操作方面,这些描述有点模糊,不同厂商之间存在一些差异(例如,AMD上的SFENCE更强),甚至是来自同一厂商的处理器也有所不同。请参考Intel的手册和规格更新以及AMD的手册和修订指南以获取更多信息。还有很多其他关于这些指令的讨论,可以在SO和其他地方找到。但首先要阅读官方来源。我认为以上描述是跨厂商最小指定的纸面行为。

脚注1:后期存储的OoO exec不需要被MFENCE阻塞;执行它们只是将数据写入存储缓冲区。按顺序提交已经将它们排序在早期存储之后,并在退休后根据负载排序(因为x86要求负载完成,而不仅仅是开始,在它们可以退休之前,作为确保负载排序的一部分)。 请记住,x86硬件构建时禁止重新排序,除了StoreLoad。

Intel手册第2卷号码325383-072US描述了SFENCE指令,它“确保在SFENCE之前的每个存储在SFENCE之后的任何存储变得全局可见之前都是全局可见的。”第3节第11.10节说,在使用SFENCE时会清空存储缓冲区。这个声明的正确解释与第2卷中的早期声明完全相同。因此,可以说SFENCE在某种意义上排空了存储缓冲区。在SFENCE的生命周期中,没有保证早期存储何时达到GO。对于任何早期存储,它可能发生在SFENCE退役之前、期间或之后。关于GO的目的,这取决于几个因素。这超出了问题的范围。请参见:为什么“movnti”后跟“sfence”可以保证持久排序?

MFENCE需要防止NT存储与其他存储重新排序,因此它必须包括SFENCE的所有内容,并排空存储缓冲区。此外,还需要防止来自WC内存的弱序SSE4.1 NT加载重新排序,这更加困难,因为通常免费获取加载排序的规则不再适用于这些规则。保证这一点是为什么Skylake微码更新加强了(并减慢了)MFENCE以像LFENCE一样排空ROB。如果有硬件支持可选地强制执行管道中的NT加载排序,则MFENCE仍可能比那更轻量级。


SFENCE + LFENCE不等于MFENCE的主要原因是,SFENCE + LFENCE不能阻止StoreLoad重排序,所以它对于顺序一致性来说是不足够的。只有mfence(或者一个lock操作,或者一个真正的序列化指令,例如cpuid)才能做到这一点。请参见Jeff Preshing的Memory Reordering Caught in the Act,了解只有完整屏障才足够的情况。


来自Intel的指令集参考手册sfence条目

在SFENCE之前的每个存储都在SFENCE之后的任何存储变为全局可见之前,处理器确保它们已经全局可见。

但是

它与内存加载或LFENCE指令的顺序无关。


LFENCE 会强制之前的指令“本地完成”(即从乱序部分退役),但对于存储或 SFENCE,这只意味着将数据或标记放入内存顺序缓冲区,而不是刷新它,以使存储变为全局可见。即 SFENCE 的“完成”(从 ROB 退役)不包括刷新存储器缓冲区。

这就像 Preshing 在 Memory Barriers Are Like Source Control Operations 中所描述的那样,其中 StoreStore 屏障不是“即时”的。在该文章的后面,他解释了为什么 #StoreStore + #LoadLoad + #LoadStore 屏障不等于 #StoreLoad 屏障。(x86 LFENCE 具有一些额外的指令流串行化,但由于它不刷新存储器缓冲区,因此推理仍然成立)。

LFENCE不能像cpuid一样完全序列化 (它只是LoadLoad + LoadStore障碍,加上一些执行序列化的内容,可能最初是一种实现细节,但现在已经成为Intel CPU上至少保证的东西,就像mfencelocked指令一样强的内存屏障)。它对于rdtsc很有用,并且可以避免分支推测以减轻Spectre漏洞。


顺便提一句,对于WB(正常)存储,SFENCE是无操作的。

它会将WC存储(例如movnt或存储到视频RAM的存储器)与任何存储器相关联,但不会与加载或LFENCE相关。只有在通常为弱序的CPU上,存储-存储屏障才会对正常存储器起作用。除非您正在使用NT存储器或将内存区域映射为WC,否则您不需要SFENCE。如果确保在可退休之前排空存储器缓冲区,则可以使用SFENCE + LFENCE构建MFENCE,但这并非适用于英特尔系统。


真正的问题是存储和加载之间的StoreLoad重排序,而不是存储和屏障之间的重排序,因此您应该查看一个存储,然后是一个屏障,最后是一个加载的情况

mov  [var1], eax
sfence
lfence
mov   eax, [var2]

可以按照以下顺序变为全局可见(即提交到L1d缓存):

lfence
mov   eax, [var2]     ; load stays after LFENCE

mov  [var1], eax      ; store becomes globally visible before SFENCE
sfence                ; can reorder with LFENCE

1
@pveentjer:是的,但您还需要指定在等待时被阻止的内容。对于LFENCE,它是前端问题阶段。对于MFENCE,根据实现方式,可能仅执行后续加载,而ALU工作的乱序执行会继续进行。对于包含lock指令的完整障碍,情况也是如此。对于其他实现(例如带有微代码更新的Skylake处理器),似乎MFENCE在排空SB + ROB时会阻止前端,例如lock xor + LFENCE。请参见 此答案 的末尾。 - Peter Cordes
1
太好了,我会仔细看一下。一旦我意识到等待SB被排空并不等同于等待ROB被排空,这就开始有意义了。 - pveentjer
1
@pveentjer:确实,我不知道为什么我在我的答案中没有一开始就说出那些概念;也许两年前它们在我的脑海中并不是那么清晰。编辑添加了一个新的部分在顶部。 - Peter Cordes
1
@pveentjer:是的,阻塞前端直到ROB和存储缓冲区被排空,并隐藏流水线的所有效果。这就是在x86手册中作为技术术语所指的“串行化”。只有很少的指令可以保证如此,包括cpuidiret - Peter Cordes
1
这就是为什么LFENCE不是一个完全序列化的指令,它只等待ROB被排空而不是SB。 - pveentjer
显示剩余16条评论

6
一般来说,MFENCE != SFENCE + LFENCE。例如下面的代码,如果使用-DBROKEN编译,在一些Westmere和Sandy Bridge系统上会失败,但在Ryzen上似乎可以正常工作。实际上,在AMD系统上只需要一个SFENCE就足够了。
#include <atomic>
#include <thread>
#include <vector>
#include <iostream>
using namespace std;

#define ITERATIONS (10000000)
class minircu {
        public:
                minircu() : rv_(0), wv_(0) {}
                class lock_guard {
                        minircu& _r;
                        const std::size_t _id;
                        public:
                        lock_guard(minircu& r, std::size_t id) : _r(r), _id(id) { _r.rlock(_id); }
                        ~lock_guard() { _r.runlock(_id); }
                };
                void synchronize() {
                        wv_.store(-1, std::memory_order_seq_cst);
                        while(rv_.load(std::memory_order_relaxed) & wv_.load(std::memory_order_acquire));
                }
        private:
                void rlock(std::size_t id) {
                        rab_[id].store(1, std::memory_order_relaxed);
#ifndef BROKEN
                        __asm__ __volatile__ ("mfence;" : : : "memory");
#else
                        __asm__ __volatile__ ("sfence; lfence;" : : : "memory");
#endif
                }
                void runlock(std::size_t id) {
                        rab_[id].store(0, std::memory_order_release);
                        wab_[id].store(0, std::memory_order_release);
                }
                union alignas(64) {
                        std::atomic<uint64_t>           rv_;
                        std::atomic<unsigned char>      rab_[8];
                };
                union alignas(8) {
                        std::atomic<uint64_t>           wv_;
                        std::atomic<unsigned char>      wab_[8];
                };
};

minircu r;

std::atomic<int> shared_values[2];
std::atomic<std::atomic<int>*> pvalue(shared_values);
std::atomic<uint64_t> total(0);

void r_thread(std::size_t id) {
    uint64_t subtotal = 0;
    for(size_t i = 0; i < ITERATIONS; ++i) {
                minircu::lock_guard l(r, id);
                subtotal += (*pvalue).load(memory_order_acquire);
    }
    total += subtotal;
}

void wr_thread() {
    for (size_t i = 1; i < (ITERATIONS/10); ++i) {
                std::atomic<int>* o = pvalue.load(memory_order_relaxed);
                std::atomic<int>* p = shared_values + i % 2;
                p->store(1, memory_order_release);
                pvalue.store(p, memory_order_release);

                r.synchronize();
                o->store(0, memory_order_relaxed); // should not be visible to readers
    }
}

int main(int argc, char* argv[]) {
    std::vector<std::thread> vec_thread;
    shared_values[0] = shared_values[1] = 1;
    std::size_t readers = (argc > 1) ? ::atoi(argv[1]) : 8;
    if (readers > 8) {
        std::cout << "maximum number of readers is " << 8 << std::endl; return 0;
    } else
        std::cout << readers << " readers" << std::endl;

    vec_thread.emplace_back( [=]() { wr_thread(); } );
    for(size_t i = 0; i < readers; ++i)
        vec_thread.emplace_back( [=]() { r_thread(i); } );
    for(auto &i: vec_thread) i.join();

    std::cout << "total = " << total << ", expecting " << readers * ITERATIONS << std::endl;
    return 0;
}

似乎没有产生任何效果。 - Alexander Korobka
Alexander,只是提醒一下,StackExchange要求您在语言提示和代码块之间加入换行符,原因不明,请参阅修订历史记录以获取更多信息,Ross Ridge已经处理了此问题。 - jrh
由于某种原因,AMD将sfence定义为完全屏障,在后续加载执行之前清空存储缓冲区。我认为这在AMD CPU上是官方记录的,而不仅仅是实现细节,例如sfence恰好在从ROB中退出之前清空SB。 - Peter Cordes

3
从英特尔手册第2A卷第3-464页文档中可以得知,LFENCE指令的说明如下:

在所有先前的指令完成本地执行之前,LFENCE不会执行,并且在LFENCE完成之前不会开始执行任何后续指令。

因此,LFENCE指令明确防止了您所示例的重新排序。由于SFENCE对加载操作没有影响,所以您的第二个示例涉及的仅有SFENCE指令是有效的重排序。

谢谢!但我并不认为MFENCE=LFENCE+SFENCE,我认为MFENCE=SFENCE+LFENCE - 屏障的顺序很重要,您可以看到我们的讨论:https://dev59.com/7mIj5IYBdhLWcg3wYkNY#G0wQoYgBc1ULPQZFXI-F SFENCE + LFENCE不能被重新排序为LFENCE + SFENCE,因此,2 mov [mem],reg不能在SFENCE之后执行,而3 mov reg,[mem]不能在LFENCE之前执行,不能重新排序:1 mov reg,[mem] 2 mov [mem],reg SFENCE LFENCE 3 mov reg,[mem] 4 mov [mem],reg - Alex
@Alex,你说得完全正确,对我的错误表示抱歉。我已经删除了我的回答中的那部分内容。我想更详细地调查这个问题,完成写作后我会在这里发布链接。 - Myles Hathcock
好的,不用担心,我也犯了同样的错误,在那个链接上的讨论开始时 :) 或许这不是一个简单的问题。 - Alex

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