usar thread library how ejemplo algorithms c++ multithreading x86

library - thread linux c++



Atomicity en x86 (2)

La señal LOCK # (pin del paquete / socket de la CPU) se usó en chips antiguos (para operaciones atómicas con prefijo LOCK ), ahora hay un bloqueo de caché. Y para operaciones atómicas más complejas, como .exchange o .fetch_add , funcionará con el prefijo LOCK o algún otro tipo de instrucción atómica (cmpxchg / 8/16?).

El mismo manual, parte de la Guía de programación del sistema:

En los procesadores de familia Pentium 4, Intel Xeon y P6, la operación de bloqueo se maneja con un bloqueo de caché o bloqueo de bus. Si el acceso a la memoria es almacenable en caché y afecta solo a una sola línea de caché, se invoca un bloqueo de caché y el bus del sistema y la ubicación real de la memoria en la memoria del sistema no se bloquean durante la operación

Puede consultar documentos y libros de Paul E. McKenney: * Pedidos de memoria en microprocesadores modernos , 2007 * Barreras de memoria: una vista de hardware para piratas informáticos , 2010 * perfbook , " ¿Es difícil la programación paralela, y si es así, qué puede hacer usted acerca de ¿Es?

Y * Libro blanco de solicitud de memoria Intel 64 Architecture , 2007.

Se necesita una barrera de memoria para x86 / x86_64 para evitar que las cargas se reordenen. Del primer trabajo:

x86 (..AMD64 es compatible con x86 ..) Dado que las CPU x86 proporcionan "orden de proceso" para que todas las CPU acuerden el orden de las escrituras de una CPU determinada en la memoria, la primitiva smp_wmb() es smp_wmb() para la CPU [7]. Sin embargo, se requiere una directiva de compilación para evitar que el compilador realice optimizaciones que darían lugar a un reordenamiento en la primitiva smp_wmb() .

Por otro lado, las CPU x86 tradicionalmente no han otorgado garantías de pedido para las cargas, por lo que las smp_mb() y smp_rmb() expanden para lock;addl . Esta instrucción atómica actúa como una barrera tanto para las cargas como para las tiendas.

Qué lee la barrera de la memoria (del segundo documento):

El efecto de esto es que una barrera de memoria de lectura solo ordena cargas en la CPU que la ejecuta, de modo que todas las cargas que preceden a la barrera de memoria de lectura aparecerán completadas antes de cualquier carga que siga a la barrera de memoria de lectura.

Por ejemplo, del "Libro blanco de solicitud de memoria de Intel 64 Architecture"

La ordenación de la memoria Intel 64 garantiza que para cada una de las siguientes instrucciones de acceso a la memoria, la operación de memoria constitutiva parece ejecutarse como un único acceso de memoria independientemente del tipo de memoria: ... Instrucciones que leen o escriben una doble palabra (4 bytes) cuya dirección es alineado en un límite de 4 bytes.

La ordenación de memoria Intel 64 obedece a los siguientes principios: 1. Las cargas no se reordenan con otras cargas. ... 5. En un sistema multiprocesador, el ordenamiento de la memoria obedece a la causalidad (el orden de la memoria respeta la visibilidad transitiva). ... La ordenación de la memoria Intel 64 garantiza que las cargas se vean en el orden del programa

Además, la definición de mfence : http://www.felixcloutier.com/x86/MFENCE.html

Realiza una operación de serialización en todas las instrucciones de carga desde la memoria y de almacenamiento en memoria que se emitieron antes de la instrucción MFENCE. Esta operación de serialización garantiza que cada instrucción de carga y almacenamiento que precede a la instrucción MFENCE en orden de programa se vuelva globalmente visible antes de cualquier instrucción de carga o almacenamiento que siga a la instrucción MFENCE.

8.1.2 Bloqueo de bus

Los procesadores Intel 64 e IA-32 proporcionan una señal de BLOQUEO # que se afirma automáticamente durante ciertas operaciones de memoria críticas para bloquear el bus del sistema o el enlace equivalente. Mientras se afirma esta señal de salida, las solicitudes de otros procesadores o agentes de bus para el control del bus se bloquean. El software puede especificar otras ocasiones en las que la semántica de LOCK debe ir precedida del prefijo LOCK a una instrucción.

Viene de Intel Manual, Volumen 3

Parece que las operaciones atómicas en la memoria se ejecutarán directamente en la memoria (RAM). Estoy confundido porque no veo "nada especial" cuando analizo el resultado de ensamblaje. Básicamente, el resultado del ensamblado generado para std::atomic<int> X; X.load() std::atomic<int> X; X.load() pone solo una defensa "extra". Pero, es responsable de la ordenación correcta de la memoria, no de una atomicidad. Si entiendo correctamente, X.store(2) es solo mov [somewhere], $2 . Y eso es todo. Parece que no "omite" el caché. Sé que el movimiento alineado (por ejemplo, ints) a la memoria es atómico. Sin embargo, estoy confundido.

Entonces, presenté mis dudas, pero la pregunta principal es:

¿Cómo implementa la CPU las operaciones atómicas internamente?


Parece que las operaciones atómicas en la memoria se ejecutarán directamente en la memoria (RAM).

No, siempre que todos los posibles observadores en el sistema vean la operación como atómica, la operación solo puede involucrar caché. Satisfacer este requisito es mucho más difícil para operaciones atómicas de lectura, modificación y escritura (como lock add [mem], eax , especialmente con una dirección no alineada), que es cuando una CPU puede afirmar la señal LOCK #. Todavía no verías nada más que eso en el asm: el hardware implementa la semántica requerida por ISA para las instrucciones de lock .

Aunque dudo de que exista un pin físico LOCK # externo en las CPU modernas donde el controlador de memoria está incorporado a la CPU, en lugar de en un chip northbridge independiente.

std::atomic<int> X; X.load() std::atomic<int> X; X.load() pone solo una defensa "extra".

Los compiladores no MIENTEN para cargas seq_cst. Creo que leí que MSVC emitió MFENCE para esto (¿tal vez para evitar el reordenamiento con tiendas NT no protegidas?), Pero ya no: acabo de probar MSVC 19.00.23026.0. Busque foo y bar en la salida asm de este programa que vuelca su propio asm en un sitio de compilación y ejecución en línea .

Creo que la razón por la que no necesitamos una valla aquí es que el modelo de memoria x86 no permite el reordenamiento LoadStore y LoadLoad. Las tiendas anteriores (no seq_cst) aún se pueden retrasar hasta después de una carga seq_cst, por lo que es diferente de usar una std::atomic_thread_fence(mo_seq_cst); antes de una X.load(mo_acquire);

Si lo entiendo correctamente, el X.store(2) es solo mov [somewhere], 2

No, las tiendas seq_cst requieren una instrucción completa de barrera de memoria, para no permitir el reordenamiento de StoreLoad, que de otro modo podría ocurrir .

El asm de MSVC para tiendas es igual que clang , usando xchg para hacer la tienda y una barrera de memoria con la misma instrucción. (En algunas CPU, especialmente AMD, una instrucción de lock como barrera puede ser más barata que MFENCE porque IIRC documenta la serialización de la serialización de la tubería adicional (para la ejecución de instrucciones, no solo para ordenar la memoria) para MFENCE).

Esta pregunta se parece a la parte 2 de su Modelo de memoria anterior en C ++: consistencia secuencial y atomicidad , donde preguntó:

¿Cómo implementa la CPU las operaciones atómicas internamente?

Como señaló en la pregunta, la atomicidad no está relacionada con el orden con respecto a otras operaciones. (es decir, memory_order_relaxed ). Simplemente significa que la operación ocurre como una única operación indivisible, de ahí el nombre , no como partes múltiples que pueden suceder parcialmente antes y parcialmente después de otra cosa.

Obtiene la atomicidad "de forma gratuita" sin hardware adicional para cargas alineadas o almacena hasta el tamaño de las rutas de datos entre núcleos, memoria y buses de E / S como PCIe. es decir, entre los diversos niveles de caché y entre los cachés de núcleos separados. Los controladores de memoria son parte de la CPU en diseños modernos, por lo que incluso un dispositivo PCIe que accede a la memoria debe pasar por el agente del sistema de la CPU. (Esto incluso permite que eDRAM L4 de Skylake (no disponible en ninguna CPU de escritorio :() funcione como memoria caché del lado de la memoria (a diferencia de Broadwell, que lo usó como caché de víctimas para L3 IIRC), entre la memoria y todo lo demás en el sistema incluso puede almacenar en caché DMA).

Esto significa que el hardware de la CPU puede hacer lo que sea necesario para asegurarse de que una tienda o carga es atómica con respecto a cualquier otra cosa en el sistema que pueda observarla. Probablemente esto no sea mucho, en todo caso. La memoria DDR utiliza un bus de datos lo suficientemente amplio como para que una tienda alineada de 64 bits realmente transmita eléctricamente el bus de memoria a la DRAM, todo en el mismo ciclo. (Dato divertido, pero no importante. Un protocolo de bus serie como PCIe no evitaría que sea atómico, siempre y cuando un solo mensaje sea lo suficientemente grande. Y dado que el controlador de memoria es lo único que puede comunicarse directamente con la DRAM, no importa lo que haga internamente, solo el tamaño de las transferencias entre él y el resto de la CPU). Pero de todos modos, esta es la parte "gratis": no se necesita un bloqueo temporal de otras solicitudes para mantener una transferencia atómica atómica.

x86 garantiza que las cargas alineadas y las tiendas de hasta 64 bits son accesos atómicos , pero no más amplios. Las implementaciones de bajo consumo de energía son libres de dividir las cargas / almacenamientos de vectores en fragmentos de 64 bits como P6, desde PIII hasta Pentium M.

Las operaciones atómicas ocurren en la memoria caché

Recuerde que atómico simplemente significa que todos los observadores lo ven como sucedido o no sucedió, nunca ocurrió parcialmente. No es necesario que llegue a la memoria principal de inmediato (o en absoluto, si se sobrescribe pronto). La modificación atmosférica o la lectura de la memoria caché L1 es suficiente para garantizar que cualquier otro acceso a núcleo o DMA verá una tienda o carga alineada como una única operación atómica. Está bien si esta modificación ocurre mucho después de que la tienda se ejecuta (por ejemplo, se retrasa por ejecución fuera de orden hasta que la tienda se retira).

Las CPU modernas como Core2 con rutas de 128 bits en todas partes suelen tener cargas / almacenes atómicos SSE 128b, más allá de lo que garantiza el ISA x86. Pero tenga en cuenta la interesante excepción en un Opteron multi-socket probablemente debido a hypertransport. Esa es una prueba de que la modificación atómica de la memoria caché L1 no es suficiente para proporcionar atomicidad para tiendas más amplias que la ruta de datos más estrecha (que en este caso no es la ruta entre la memoria caché L1 y las unidades de ejecución).

La alineación es importante : una carga o almacén que cruza un límite de la línea de caché debe hacerse en dos accesos separados. Esto lo hace no atómico.

x86 garantiza que los accesos en caché de hasta 8 bytes son atómicos siempre que no crucen un límite de 8B en AMD / Intel. (O para Intel solo en P6 y posterior, no cruce un límite de la línea de caché). Esto implica que las líneas de caché completas (64B en las CPU modernas) se transfieren de forma atómica en Intel, aunque es más amplio que las rutas de datos (32B entre L2 y L3 en Haswell / Skylake). Esta atomicidad no es totalmente "gratuita" en hardware, y tal vez requiera cierta lógica adicional para evitar que una carga lea una línea de caché que solo se transfiere parcialmente. Aunque las transferencias de línea de caché solo suceden después de que se invalidara la versión anterior, por lo que un núcleo no debería leer la copia anterior mientras se realiza una transferencia. AMD puede romperse en la práctica en límites más pequeños, tal vez debido al uso de una extensión diferente a MESI que puede transferir datos sucios entre cachés.

Para operandos más amplios, como escribir atómicamente datos nuevos en múltiples entradas de una estructura, debe protegerlo con un bloqueo al que todos los accesos respeten. (Puede usar x86 lock cmpxchg16b con un ciclo de reintento para hacer un almacén atómico 16b. Tenga en cuenta que no hay forma de emularlo sin un mutex ).

Atomic read-modify-write es donde se hace más difícil

relacionado: mi respuesta en ¿Puede num ++ ser atómico para ''int num''? entra en más detalles sobre esto.

Cada núcleo tiene un caché L1 privado que es coherente con todos los demás núcleos (utilizando el protocolo MOESI ). Las líneas de caché se transfieren entre los niveles de caché y la memoria principal en fragmentos que varían en tamaño desde 64 bits a 256 bits. (¿Estas transferencias pueden ser atómicas en una granularidad de línea de caché completa?)

Para hacer un RMW atómico, un núcleo puede mantener una línea de caché L1 en estado Modificado sin aceptar ninguna modificación externa a la línea de caché afectada entre la carga y el almacén, el resto del sistema verá la operación como atómica. (Y, por lo tanto, es atómico, porque las reglas habituales de ejecución fuera de orden requieren que el hilo local vea que su propio código se ejecutó en el orden del programa).

Puede hacerlo sin procesar ningún mensaje de coherencia de caché mientras el RMW atómico está en vuelo (o una versión más complicada de esto que permite más paralelismo para otras operaciones).

Las operaciones de lock alinear son un problema: necesitamos otros núcleos para ver las modificaciones en dos líneas de caché como una única operación atómica. Esto puede requerir almacenar realmente en DRAM y tomar un bloqueo de bus. (El manual de optimización de AMD dice que esto es lo que sucede en sus CPU cuando un bloqueo de caché no es suficiente).