13

正如我们从之前对处理器 x86/x86_64 中的指令 LFENCE 是否有意义的回答中所知道的那样?我们不能用它SFENCE来代替MFENCE顺序一致性。

那里的答案表明MFENCE= SFENCE+ LFENCE,即它LFENCE做了一些我们无法提供顺序一致性的事情。

LFENCE无法重新排序:

SFENCE
LFENCE
MOV reg, [addr]

-- 到 -->

MOV reg, [addr]
SFENCE
LFENCE

例如,由机制 - Store BufferMOV [addr], reg LFENCE提供的--> 重新排序,它重新排序 Store - Loads 以提高性能,因为不会阻止它。并禁用此机制LFENCE MOV [addr], regLFENCESFENCE

什么机制禁用了LFENCE不可能的重新排序(x86 没有机制 - Invalidate-Queue)?

SFENCE MOV reg, [addr]对-->的重新排序是否MOV reg, [addr] SFENCE仅在理论上或实际上可能?如果可能的话,实际上是什么机制,它是如何工作的?

4

3 回答 3

22

x86围栏指令可以简单描述如下:

  • MFENCE 防止任何以后的加载或存储在任何较早的加载或存储之前成为全局可观察的。它在以后的加载1可以执行之前耗尽存储缓冲区。

  • LFENCE 会阻止指令调度(英特尔的术语),直到所有早期指令都退出。这目前是通过在后面的指令可以发布到后端之前排空 ROB(重新排序缓冲区)来实现的。

  • SFENCE 仅针对其他存储对存储进行排序,即防止 NT 存储在 SFENCE 本身之前从存储缓冲区提交。但除此之外,SFENCE 就像一个在存储缓冲区中移动的普通存储。可以把它想象成在杂货店结账传送带上放置一个分隔器,以防止 NT 商店被提早抢购。它不一定强制存储缓冲区在它从 ROB 中退出之前被耗尽,因此将 LFENCE 放在它之后并不等于 MFENCE。

  • 像 CPUID(和 IRET 等)这样的“序列化指令”会在后面的指令发出到后端之前耗尽所有内容(ROB、存储缓冲区)。MFENCE + LFENCE 也会这样做,但真正的序列化指令也可能有其他效果,我不知道。

这些描述在确切订购什么样的操作方面有点模棱两可,并且供应商之间存在一些差异(例如,SFENCE 在 AMD 上更强)甚至来自同一供应商的处理器。有关详细信息,请参阅 Intel 的手册和规范更新以及 AMD 的手册和修订指南。在其他其他地方也有很多关于这些说明的其他讨论。但请先阅读官方资料。上面的描述是我认为跨供应商的最低规定的纸上行为。

脚注 1:后面的商店的 OoO exec不需要被 MFENCE 阻止;执行它们只是将数据写入存储缓冲区。有序提交已经在较早的商店之后订购它们,并在退休订单之后提交。加载(因为 x86 需要加载完成,而不仅仅是开始,然后才能退休,作为确保加载顺序的一部分)。请记住,x86 硬件被构建为禁止除 StoreLoad 之外的重新排序。

英特尔手册第 2 卷编号 325383-072US 将 SFENCE 描述为“确保 SFENCE 之前的每个存储在 SFENCE 之后的任何存储变得全局可见之前都是全局可见的”。第 3 卷第 11.10 节说,使用 SFENCE 时存储缓冲区已耗尽。该语句的正确解释正是第 2 卷中较早的语句。因此,从这个意义上说,SFENCE 可以说是耗尽了存储缓冲区。无法保证在 SFENCE 的生命周期内,较早的商店在什么时候实现 GO。对于任何较早的商店,它可能发生在 SFENCE 退休之前、之时或之后。关于 GO 的意义是什么,这取决于几个因素。这超出了问题的范围。请参阅:为什么“movnti”后跟“sfence”保证持久排序?</a>。

MFENCE确实必须防止 NT 存储与其他存储重新排序,因此它必须包含 SFENCE 所做的任何事情,以及耗尽存储缓冲区。并且还从 WC 内存重新排序弱排序的 SSE4.1 NT 负载,这更难,因为免费获得负载排序的正常规则不再适用于那些。保证这就是为什么 Skylake 微码更新会增强(并减慢)MFENCE以像 LFENCE 一样消耗 ROB。MFENCE 仍然可能比使用硬件支持更轻的重量,以选择性地强制执行管道中 NT 负载的排序。


SFENCE + LFENCE 不等于 MFENCE 的主要原因是因为 SFENCE + LFENCE 不会阻止 StoreLoad 重新排序,因此不足以实现顺序一致性。只有mfence(或locked 操作,或真正的序列化指令,如cpuid)才能做到这一点。请参阅 Jeff Preshing 的Memory Reordering Caught in the Act以了解只有完整屏障就足够的情况。


来自英特尔的指令集参考手册条目sfence

处理器确保在 SFENCE 之后的任何存储变得全局可见之前,SFENCE 之前的每个存储都是全局可见的。

它没有按照内存负载或 LFENCE 指令排序。


LFENCE 强制较早的指令“在本地完成”(即从内核的无序部分退出),但对于存储或 SFENCE 来说,这只是意味着将数据或标记放入内存顺序缓冲区,而不是刷新它商店变得全球可见。即SFENCE “完成”(从 ROB 中退出)不包括刷新存储缓冲区。

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

LFENCE 没有像 ed 指令那样完全序列化cpuid一个强大的内存屏障mfencelock)。它只是 LoadLoad + LoadStore 屏障,加上一些执行序列化的东西,这些东西可能作为实现细节开始,但现在至少在 Intel CPU 上被奉为保证。它对rdtsc, 和避免分支推测以减轻 Spectre 很有用。


顺便说一句,SFENCE 对 WB(普通)商店来说是无操作的。

它相对于任何存储对 WC 存储(例如 movnt,或存储到视频 RAM)进行排序,但不考虑加载或 LFENCE。只有在通常是弱排序的 CPU 上,store-store 屏障才能为正常的存储做任何事情。除非您使用 NT 存储或映射 WC 的内存区域,否则您不需要 SFENCE。如果它确实保证在它退休之前耗尽存储缓冲区,那么您可以使用 SFENCE+LFENCE 构建 MFENCE,但英特尔并非如此。


真正关心的是 StoreLoad 在 store 和 load 之间重新排序,而不是在 store 和 barrier 之间重新排序,因此您应该查看一个包含 store、barrier 和 load 的案例

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
于 2018-05-14T02:26:37.250 回答
5

一般来说 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;
}
于 2018-05-14T01:57:53.973 回答
3

什么机制使 LFENCE 无法重新排序(x86 没有机制 - Invalidate-Queue)?

来自英特尔手册,第 2A 卷,第 3-464 页的说明文档LFENCE

LFENCE 直到所有先前的指令在本地完成后才执行,并且在 LFENCE 完成之前没有后面的指令开始执行

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

于 2015-04-10T02:16:08.163 回答