assembly x86 x86-64 memory-barriers memory-fences

assembly - ¿Por qué es(o no es) SFENCE+LFENCE equivalente a MFENCE?



x86 x86-64 (3)

¿Qué mecanismo deshabilita LFENCE para hacer un reordenamiento imposible (x86 no tiene mecanismo - Invalidate-Queue)?

De los manuales de Intel, volumen 2A, página 3-464, documentación para la instrucción LFENCE :

LFENCE no se ejecuta hasta que todas las instrucciones anteriores se hayan completado localmente, y ninguna instrucción posterior comienza a ejecutarse hasta que LFENCE se complete

Entonces, sí, su reordenamiento de ejemplo está explícitamente impedido por la instrucción LFENCE . Su segundo ejemplo que involucra solo instrucciones de SFENCE ES un reordenamiento válido, ya que SFENCE no tiene impacto en las operaciones de carga.

Como sabemos por una respuesta anterior a ¿Tiene algún sentido la instrucción LFENCE en los procesadores x86 / x86_64? que no podemos usar SFENCE lugar de MFENCE para la coherencia secuencial.

Una respuesta allí sugiere que MFENCE = SFENCE + LFENCE , es decir, que LFENCE hace algo sin lo cual no podemos proporcionar coherencia secuencial.

LFENCE hace imposible reordenar:

SFENCE LFENCE MOV reg, [addr]

- A ->

MOV reg, [addr] SFENCE LFENCE

Por ejemplo, reordenamiento de MOV [addr], reg LFENCE -> LFENCE MOV [addr], reg proporcionado por mecanismo - Store Buffer , que reordena Store - Loads para aumentar el rendimiento, y LFENCE no lo impide. Y SFENCE deshabilita este mecanismo .

¿Qué mecanismo deshabilita LFENCE para hacer un reordenamiento imposible (x86 no tiene mecanismo - Invalidate-Queue)?

¿Y es posible reordenar SFENCE MOV reg, [addr] -> MOV reg, [addr] SFENCE solo en teoría o quizás en la realidad? Y si es posible, en realidad, ¿qué mecanismos, cómo funciona?


En general MFENCE! = SFENCE + LFENCE. Por ejemplo, el código siguiente, cuando se compila con -DBROKEN , falla en algunos sistemas Westmere y Sandy Bridge, pero parece funcionar en Ryzen. De hecho, en los sistemas AMD, solo un SFENCE parece ser suficiente.

#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; }


SFENCE + LFENCE no bloquea el reordenamiento de StoreLoad, por lo que no es suficiente para la coherencia secuencial . Solo mfence (o una operación de lock o una instrucción de serialización real como cpuid ) lo hará. Vea el Reordenamiento de la memoria de Jeff Preshing atrapado en la Ley para un caso donde solo una barrera completa es suficiente.

De la entrada del manual de referencia del conjunto de instrucciones de sfence para sfence :

El procesador garantiza que cada tienda antes de SFENCE sea visible globalmente antes de cualquier tienda después de que SFENCE sea visible globalmente.

pero

No está ordenado con respecto a las cargas de memoria o la instrucción LFENCE.

LFENCE obliga a las instrucciones anteriores a "completarse localmente" (es decir, retirarse de la parte fuera de servicio del núcleo), pero para una tienda o SFENCE eso solo significa poner datos o un marcador en el búfer de orden de memoria, no enjuagarlo así la tienda se vuelve globalmente visible. es decir, la "finalización" de SFENCE (retiro del ROB) no incluye vaciar el búfer de la tienda.

Esto es como lo describe Preshing en Las barreras de memoria son como las operaciones de control de origen , donde las barreras de StoreStore no son "instantáneas". Más adelante en ese artículo, explica por qué una #StoreStore + #LoadLoad + una barrera #LoadStore no se suma a una barrera #StoreLoad. (x86 LFENCE tiene una serialización adicional de la secuencia de instrucciones, pero dado que no vacía el búfer de la tienda, el razonamiento aún se mantiene).

LFENCE no está serializando completamente como cpuid ( que es una barrera de memoria tan fuerte como mfence o una instrucción lock ). Es solo la barrera LoadLoad + LoadStore, más algunas cosas de serialización de ejecución que quizás comenzaron como un detalle de implementación pero ahora están consagradas como una garantía, al menos en las CPU Intel. Es útil con rdtsc y para evitar la especulación de ramas para mitigar Spectre.

Por cierto, SFENCE es un no-op, excepto para las tiendas NT; los ordena con respecto a las tiendas normales (de lanzamiento). Pero no con respecto a cargas o LFENCE. Solo en la CPU que normalmente está ordenada débilmente, una barrera tienda-tienda hace algo.

La verdadera preocupación es el reordenamiento de StoreLoad entre una tienda y una carga, no entre una tienda y barreras, por lo que debe mirar un caso con una tienda, luego una barrera, luego una carga .

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

puede hacerse globalmente visible (es decir, comprometerse con el caché L1d) en este orden:

lfence mov eax, [var2] ; load stays after LFENCE mov [var1], eax ; store becomes globally visible before SFENCE sfence ; can reorder with LFENCE