为什么(或者不是?)SFENCE + LFENCE相当于MFENCE?

时间:2014-12-23 21:04:58

标签: assembly x86 x86-64 memory-barriers memory-fences

正如我们之前对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

例如重新排序MOV [addr], reg LFENCE - > {strong>机制 - 存储缓冲区提供了LFENCE MOV [addr], reg,它会重新排序存储 - 负载以提高性能,因为LFENCE不会阻止它。 SFENCE 会停用此机制

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

正在重新排序SFENCE MOV reg, [addr] - > MOV reg, [addr] SFENCE只能在理论上或现实中实现吗?如果可能,实际上,什么机制,它是如何工作的?

3 个答案:

答案 0 :(得分:9)

SFENCE + LFENCE不会阻止StoreLoad重新排序,因此对于顺序一致性来说还不够。只有mfence(或lock ed操作或真正的序列化指令(如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)不包括刷新商店缓冲区。

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

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

顺便说一下,除了NT商店外,SFENCE是无操作的;它就正常(释放)商店命令他们。但不是关于负载或LFENCE。只有在通常弱订购的CPU上,商店商店隔离才能做任何事情。

真正关心的是StoreLoad在商店和负载之间重新排序,而不是在商店和障碍之间重新排序,所以你应该看一个商店的案例,然后是障碍,然后是负载。< / p>

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 :(得分:3)

一般来说,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;
}

答案 2 :(得分:1)

  

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

来自英特尔手册,第2-4卷,第3-464页LFENCE指令的文档:

  

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

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