¿Adquisición / liberación atómica C11 y falta de coherencia x86_64 de carga / almacenamiento?

10

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->readya 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.

Chris Hall
fuente
55
En x86, todas las tiendas ordinarias (no temporales) tienen semántica de lanzamiento. Intel® 64 e IA-32 Arquitecturas Desarrollador de Software Manual Volumen 3 (3A, 3B, 3C y 3D): Sistema de Guía de Programación , 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.
EOF
Gracias ! (Me estaba volviendo loco). FWIW Recomiendo el enlace , particularmente la sección 3, el "Modelo del programador". Pero para evitar el error en el que caí, tenga en cuenta que en "3.1 The Abstract Machine" hay "subprocesos de hardware", cada uno de los cuales es "un único flujo de ejecución de instrucciones en orden " (mi énfasis se agregó). Ahora puedo volver a tratar de entender el Estándar C11 ... con menos disonancia cognitiva :-)
Chris Hall

Respuestas:

1

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:

Manual del desarrollador de software de arquitecturas Intel® 64 e IA-32 Volumen 3 (3A, 3B, 3C y 3D): Guía de programación del sistema, 8.2.3.3 Las tiendas no se reordenan con cargas anteriores.


Nota 1: ignorar las tiendas NT mal ordenadas; Es por eso que normalmente sfencedespués de hacer NT almacena. Las implementaciones de C11 / C ++ 11 suponen que no está utilizando tiendas NT. Si es así, úselo _mm_sfenceantes de una operación de lanzamiento para asegurarse de que respeta sus tiendas NT. (En general , no use _mm_mfence/ _mm_sfenceen otros casos ; por lo general, solo necesita bloquear el reordenamiento en tiempo de compilación. O, por supuesto, solo use stdatomic).

Peter Cordes
fuente
Encuentro el x86-TSO: un modelo de programador riguroso y utilizable para multiprocesadores x86 más legible que la Descripción formal (relacionada) a la que hizo referencia. Pero mi verdadera ambición es comprender completamente las secciones 5.1.2.4 y 7.17.3 del Estándar C11 / C18. En particular, creo que obtengo Release / Acquire / Acquire + Release, pero memory_order_seq_cst se define por separado y estoy luchando por ver cómo encajan :-(
Chris Hall
@ ChrisHall: descubrí que me ayudó a darme cuenta exactamente cuán débil puede ser acq / rel, y para eso necesitas mirar máquinas como POWER que pueden hacer reordenamiento IRIW. (que seq-cst prohíbe pero acq / rel no). ¿Se verán dos escrituras atómicas en diferentes ubicaciones en diferentes hilos siempre en el mismo orden por otros hilos? . ¿ También cómo lograr una barrera StoreLoad en C ++ 11? tiene una discusión acerca de cuán poco el estándar garantiza formalmente sobre los pedidos fuera de los casos de sincronización con o todo-seq-cst.
Peter Cordes
@ChrisHall: Lo principal que hace seq-cst es bloquear el reordenamiento de StoreLoad. (En x86, eso es lo único que hace más allá de acq / rel). preshing.com/20120515/memory-reordering-caught-in-the-act usa asm, pero es equivalente a seq-cst vs. acq / rel
Peter Cordes