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