Почему 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], reg
LFENCE
-> LFENCE
MOV [addr], reg
обеспечивается механизмом - Store Buffer, который переупорядочивает Store - загружает для увеличения производительности и beacause LFENCE
не мешает этому. А также SFENCE
отключает этот механизм.
Какой механизм отключает LFENCE
сделать невозможным переупорядочение (у x86 нет механизма - Invalidate-Queue)?
И переупорядочивает SFENCE
MOV 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
не влияет на нагрузку.