assembly 为什么是(或不是?)SFENCE + LFENCE等于MFENCE?

bvjveswy  于 2023-06-06  发布在  其他
关注(0)|答案(3)|浏览(207)

正如我们从之前对Does it make any sense instruction LFENCE in processors x86/x86_64?的回答中所知道的,我们不能使用SFENCE代替MFENCE来实现顺序一致性。
这里的答案是MFENCE = SFENCE + LFENCE,即LFENCE做了一些事情,没有这些事情我们就不能提供顺序一致性。
LFENCE无法重新排序:

SFENCE
LFENCE
MOV reg, [addr]

--到-->

MOV reg, [addr]
SFENCE
LFENCE

例如,mechanism - Store Buffer提供的MOV [addr], regLFENCE--> LFENCEMOV [addr], reg的重新排序,该机制重新排序Store - Loads以提高性能,因为LFENCE不会阻止它。SFENCE禁用此机制
什么机制禁用了LFENCE,使其无法重新排序(x86没有机制- Invalidate-Queue)?
SFENCEMOV reg, [addr]--> MOV reg, [addr]SFENCE的重新排序是否仅在理论上或现实中可能?如果可能的话,在现实中,什么机制,它是如何工作的?

6qftjkof

6qftjkof1#

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

  • MFENCE防止任何稍后的加载或存储在任何较早的加载或存储之前变成全局可观察的。它在后面的loads 1可以 * 执行 * 之前耗尽存储缓冲区。
  • LFENCE会阻止指令分派(英特尔的术语),直到所有早期指令退出。这目前是通过在后面的指令可以 * 发出 * 到后端之前耗尽ROB(重新排序缓冲区)来实现的。
  • SFENCE仅针对其他存储点订购存储点,即防止NT存储在SFENCE本身之前从存储缓冲区提交。但除此之外,SFENCE就像一个在存储缓冲区中移动的普通存储。把它想象成在杂货店结账传送带上放一个分隔器,阻止NT商店过早被抓住。它 * 不 * 必须强制存储缓冲区在从ROB中退出之前耗尽,因此将LFENCE放在它之后并不等于MFENCE。
  • 像CPUID(和IRET等)这样的“序列化指令”在后续指令可以发送到后端之前耗尽所有内容(ROB,存储缓冲区),并丢弃前端。MFENCE + LFENCE也会做后端部分,但真正的序列化指令也会丢弃获取的机器码,因此可以用于交叉修改代码。(例如,加载看到一个标志,您运行cpuid或新的serialize,然后跳转到另一个线程在释放标志之前存储代码的缓冲区。代码提取保证获得新指令。与数据加载不同,代码提取不遵守x86通常的LoadLoad排序规则。

这些描述在确切地排序什么类型的操作方面有点模糊,并且在供应商之间存在一些差异(例如SFENCE在AMD上更强),甚至在同一供应商的处理器上也更强。有关详细信息,请参阅英特尔手册和规格更新以及AMD手册和修订指南。在SO其他其他地方也有很多关于这些指示的其他讨论。首先阅读官方来源。我认为上面的描述是供应商之间的最低书面行为。

脚注1:后续存储的OoO exec 不需要被MFENCE阻止;执行它们只是将数据写入存储缓冲区。按顺序提交已经在较早的存储之后对它们进行排序,并且在引退命令wrt之后提交。加载(因为x86要求加载在退出之前完成,而不仅仅是开始,作为确保加载顺序的一部分)。请记住,x86硬件不允许除StoreLoad之外的其他重新排序。

英特尔手册第2卷编号325383- 072 US将SFENCE描述为“确保SFENCE之前的每个存储在SFENCE之后的任何存储变得全局可见之前全局可见”的指令。第3卷第11.10节指出,当使用SFENCE时,存储缓冲区被耗尽。对这句话的正确解释正是第二卷中较早的那句话。因此,可以说SFENCE在这个意义上耗尽了存储缓冲区。在SFENCE的生命周期中,无法保证早期商店在什么时候实现GO。对于任何较早的商店,它可能发生在SFENCE退役之前、之时或之后。关于GO的意义是什么,这取决于几个因素。这超出了问题的范围。参见:Why “movnti” followed by an “sfence” guarantees persistent ordering?
MFENCE * 确实 * 必须防止NT存储与其他存储重新排序,因此它必须包括SFENCE所做的任何事情,以及耗尽存储缓冲区。还有从WC内存中重新排序弱序SSE4.1 NT加载,这更难,因为免费获得加载排序的正常规则不再适用于这些。确保此is why a Skylake microcode update strengthened (and slowed) MFENCE也像LFENCE一样消耗ROB。MFENCE仍然有可能比具有HW支持的MFENCE更轻,用于 * 可选地 * 强制执行管道中NT加载的排序。

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

Intel's instruction-set reference manual entry for sfence
处理器确保在SFENCE之后的任何存储变得全局可见之前,SFENCE之前的每个存储全局可见。
但是

它不是根据内存加载或LFENCE指令排序的。

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

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

LFENCE没有像cpuid那样完全序列化which is as strong a memory barrier as mfence or a lock ed instruction)。它只是LoadLoad + LoadStore屏障,加上一些执行序列化的东西,这些东西可能开始是一个实现细节,但现在被奉为一个保证,至少在Intel CPU上是这样。它对rdtsc很有用,并且可以避免分支推测以减轻Spectre。

顺便说一句,SFENCE是WB(普通)商店的无操作。
它针对任何存储器而不是针对加载或LFENCE来命令WC存储器(例如移动或存储到视频RAM)。只有在通常是弱有序的CPU上,存储-存储屏障才能对正常存储做任何事情。除非使用NT存储或MapWC的内存区域,否则不需要SFENCE。如果它确实保证在退役之前耗尽存储缓冲区,那么您可以使用SFENCE+LFENCE构建MFENCE,但英特尔的情况并非如此。
真实的的问题是StoreLoad在store和load之间的重新排序,而不是在store和barrier之间的重新排序,所以您应该查看一个store,然后是barrier,然后是load的案例

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

可以成为 * 全局可见 *(即提交到L1 d缓存):

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

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

1hdlvixo2#

在一般的仁慈!= SFENCE + LFENCE例如,下面的代码,当用-DBROKEN编译时,在一些韦斯特米尔和桑迪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;
}
2nbm6dog

2nbm6dog3#

什么机制禁用LFENCE,使其无法重新排序(x86没有机制-无效队列)?
从英特尔手册第2A卷第3-464页的LFENCE指令文档中:
直到所有先前的指令都在本地完成之后,才执行LFENCE,并且直到LFENCE完成之后,才开始执行后续指令
所以,是的,LFENCE指令明确阻止了示例中的重新排序。您的第二个示例只涉及SFENCE指令,这是一个有效的重新排序,因为SFENCE对加载操作没有影响。

相关问题