Estoy luchando con la Sección 5.1.2.4 del Estándar C11, en particular la semántica de Release / Acquire. Observo que https://preshing.com/20120913/acquire-and-release-semantics/ (entre otros) establece que:
... La semántica de liberación evita el reordenamiento de memoria de la liberación de escritura con cualquier operación de lectura o escritura que la preceda en orden de programa.
Entonces, para lo siguiente:
typedef struct test_struct
{
_Atomic(bool) ready ;
int v1 ;
int v2 ;
} test_struct_t ;
extern void
test_init(test_struct_t* ts, int v1, int v2)
{
ts->v1 = v1 ;
ts->v2 = v2 ;
atomic_store_explicit(&ts->ready, false, memory_order_release) ;
}
extern int
test_thread_1(test_struct_t* ts, int v2)
{
int v1 ;
while (atomic_load_explicit(&ts->ready, memory_order_acquire)) ;
ts->v2 = v2 ; // expect read to happen before store/release
v1 = ts->v1 ; // expect write to happen before store/release
atomic_store_explicit(&ts->ready, true, memory_order_release) ;
return v1 ;
}
extern int
test_thread_2(test_struct_t* ts, int v1)
{
int v2 ;
while (!atomic_load_explicit(&ts->ready, memory_order_acquire)) ;
ts->v1 = v1 ;
v2 = ts->v2 ; // expect write to happen after store/release in thread "1"
atomic_store_explicit(&ts->ready, false, memory_order_release) ;
return v2 ;
}
donde se ejecutan esos:
> in the "main" thread: test_struct_t ts ;
> test_init(&ts, 1, 2) ;
> start thread "2" which does: r2 = test_thread_2(&ts, 3) ;
> start thread "1" which does: r1 = test_thread_1(&ts, 4) ;
Por lo tanto, esperaría que el hilo "1" tenga r1 == 1 y el hilo "2" tenga r2 = 4.
Esperaría eso porque (siguiendo los párrafos 16 y 18 de la sección 5.1.2.4):
- todas las lecturas y escrituras (no atómicas) se "secuencian antes" y, por lo tanto, "suceden antes" de la escritura / liberación atómica en el hilo "1",
- que "entre hilos pasa antes" la lectura / adquisición atómica en el hilo "2" (cuando dice 'verdadero'),
- que a su vez está "secuenciado antes" y, por lo tanto, "sucede antes" de las lecturas y escrituras (no atómicas) (en el hilo "2").
Sin embargo, es totalmente posible que no haya entendido el estándar.
Observo que el código generado para x86_64 incluye:
test_thread_1:
movzbl (%rdi),%eax -- atomic_load_explicit(&ts->ready, memory_order_acquire)
test $0x1,%al
jne <test_thread_1> -- while is true
mov %esi,0x8(%rdi) -- (W1) ts->v2 = v2
mov 0x4(%rdi),%eax -- (R1) v1 = ts->v1
movb $0x1,(%rdi) -- (X1) atomic_store_explicit(&ts->ready, true, memory_order_release)
retq
test_thread_2:
movzbl (%rdi),%eax -- atomic_load_explicit(&ts->ready, memory_order_acquire)
test $0x1,%al
je <test_thread_2> -- while is false
mov %esi,0x4(%rdi) -- (W2) ts->v1 = v1
mov 0x8(%rdi),%eax -- (R2) v2 = ts->v2
movb $0x0,(%rdi) -- (X2) atomic_store_explicit(&ts->ready, false, memory_order_release)
retq
Y siempre que R1 y X1 sucedan en ese orden, esto da el resultado que espero.
Pero mi comprensión de x86_64 es que las lecturas ocurren en orden con otras lecturas y las escrituras ocurren en orden con otras escrituras, pero las lecturas y escrituras pueden no ocurrir en orden entre sí. Lo que implica que es posible que X1 suceda antes de R1, e incluso que X1, X2, W2, R1 sucedan en ese orden, creo. [Esto parece desesperadamente improbable, pero si R1 se retrasó por algunos problemas de caché?]
Por favor: ¿qué no estoy entendiendo?
Observo que si cambio las cargas / tiendas de ts->ready
a memory_order_seq_cst
, el código generado para las tiendas es:
xchg %cl,(%rdi)
lo cual es consistente con mi comprensión de x86_64 y dará el resultado que espero.
fuente
8.2.3.3 Stores Are Not Reordered With Earlier Loads
. Por lo tanto, su compilador está traduciendo correctamente su código (qué sorprendente), de modo que su código sea efectivamente completamente secuencial y no ocurra nada interesante al mismo tiempo.Respuestas:
El modelo de memoria de x86 es básicamente de consistencia secuencial más un búfer de tienda (con reenvío de tienda). Entonces cada tienda es una tienda de lanzamiento 1 . Es por eso que solo las tiendas seq-cst necesitan instrucciones especiales. ( C / C ++ 11 asignaciones atómicas a asm ). Además, https://stackoverflow.com/tags/x86/info tiene algunos enlaces a documentos x86, incluida una descripción formal del modelo de memoria x86-TSO (básicamente ilegible para la mayoría de los humanos; requiere leer muchas definiciones).
Como ya estás leyendo la excelente serie de artículos de Jeff Preshing, te señalaré otro que entra en más detalles: https://preshing.com/20120930/weak-vs-strong-memory-models/
El único reordenamiento permitido en x86 es StoreLoad, no LoadStore , si estamos hablando en esos términos. (El reenvío de la tienda puede hacer cosas extra divertidas si una carga solo se superpone parcialmente a una tienda; instrucciones de carga globalmente invisibles , aunque nunca obtendrá eso en el código generado por el compilador
stdatomic
).@EOF comentó con la cita correcta del manual de Intel:
Nota 1: ignorar las tiendas NT mal ordenadas; Es por eso que normalmente
sfence
después de hacer NT almacena. Las implementaciones de C11 / C ++ 11 suponen que no está utilizando tiendas NT. Si es así, úselo_mm_sfence
antes de una operación de lanzamiento para asegurarse de que respeta sus tiendas NT. (En general , no use_mm_mfence
/_mm_sfence
en otros casos ; por lo general, solo necesita bloquear el reordenamiento en tiempo de compilación. O, por supuesto, solo use stdatomic).fuente