Почему SFENCE + LFENCE (или нет?) Эквивалентны MFENCE?

Как мы знаем из предыдущего ответа, имеет ли смысл инструкция LFENCE в процессорах x86/x86_64? что мы не можем использовать SFENCE вместо MFENCE для последовательной последовательности.

Ответ там предполагает, что MFENCE знак равно SFENCE+LFENCEто есть LFENCE делает то, без чего мы не можем обеспечить последовательную согласованность.

LFENCE делает невозможным переупорядочение:

SFENCE
LFENCE
MOV reg, [addr]

- К ->

MOV reg, [addr]
SFENCE
LFENCE

Например, изменение порядка MOV [addr], regLFENCE -> LFENCEMOV [addr], reg обеспечивается механизмом - Store Buffer, который переупорядочивает Store - загружает для увеличения производительности и beacause LFENCE не мешает этому. А также SFENCE отключает этот механизм.

Какой механизм отключает LFENCE сделать невозможным переупорядочение (у x86 нет механизма - Invalidate-Queue)?

И переупорядочивает SFENCEMOV reg, [addr] -> MOV reg, [addr]SFENCE возможно только в теории или возможно в реальности? И если возможно, на самом деле, какие механизмы, как это работает?

3 ответа

SFENCE + LFENCE не блокирует переупорядочение StoreLoad, поэтому его недостаточно для последовательной согласованности. Только mfence (или lock Операция ed или реальная инструкция сериализации, такая как cpuid) сделаю это. См. " Переупорядочение памяти Джеффа Прешинга", зафиксированное в Акте, для случая, когда достаточно только полного барьера.


Из набора справочных руководств Intel для sfence:

Процессор гарантирует, что каждый магазин до SFENCE виден глобально, прежде чем любой магазин после SFENCE станет глобально видимым.

но

Он не упорядочен относительно загрузок памяти или инструкции LFENCE.


LFENCE вынуждает более ранние инструкции "выполнять локально" (т. Е. Удалять из неупорядоченной части ядра), но для хранилища или SFENCE это просто означает помещение данных или маркера в буфер порядка памяти, а не их очистку магазин становится глобально видимым. т. е. "завершение" SFENCE (удаление из ROB) не включает очистку буфера хранилища.

Это как Preshing описывает в барьерах памяти как операции управления исходным кодом, где барьеры StoreStore не являются "мгновенными". Позже в этой статье он объясняет, почему барьер #StoreStore + #LoadLoad + #LoadStore не добавляется к барьеру #StoreLoad. (x86 LFENCE имеет некоторую дополнительную сериализацию потока команд, но, поскольку он не очищает буфер хранилища, рассуждения все еще сохраняются).

LFENCE не полностью сериализуется, как cpuid ( который является таким же сильным барьером памяти, как mfence или lock инструкция). Это просто барьер LoadLoad + LoadStore, плюс некоторые вещи сериализации выполнения, которые, возможно, были начаты как подробности реализации, но теперь закреплены в качестве гарантии, по крайней мере, для процессоров Intel. Это полезно с rdtsc и для того, чтобы избежать спекуляций в ветвях, чтобы смягчить Spectre

Кстати, SFENCE не работает, кроме магазинов NT; это заказывает их относительно нормальных (выпускают) магазины. Но не в отношении нагрузок или LFENCE. Только на процессоре, который обычно слабо упорядочен, барьер магазина-магазина делает что-нибудь.


Реальная проблема заключается в переупорядочении 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

В общем 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;
}

Какой механизм отключает LFENCE, чтобы сделать невозможным переупорядочение (у x86 нет механизма - Invalidate-Queue)?

Из руководств Intel, том 2А, страница 3-464 документации для LFENCE инструкция:

LFENCE не выполняется, пока все предыдущие инструкции не будут выполнены локально, и никакие более поздние инструкции не начнут выполняться, пока LFENCE не завершит

Так что да, ваш пример переупорядочения явно предотвращен LFENCE инструкция. Ваш второй пример, включающий только SFENCE инструкция является действительным изменением порядка, так как SFENCE не влияет на нагрузку.

Другие вопросы по тегам