Adquirir / liberar semántica con tiendas no temporales en x64

Tengo algo como:

if (f = acquire_load() == ) { ... use Foo } 

y:

 auto f = new Foo(); release_store(f) 

Es fácil imaginar una implementación de acquire_load y release_store que use atomic con load (memory_order_acquire) y store (memory_order_release). Pero ahora, ¿qué ocurre si release_store se implementa con _mm_stream_si64, una escritura no temporal, que no está ordenada con respecto a otras tiendas en x64? ¿Cómo obtener la misma semántica?

Creo que el siguiente es el mínimo requerido:

 atomic gFoo; Foo* acquire_load() { return gFoo.load(memory_order_relaxed); } void release_store(Foo* f) { _mm_stream_si64(*(Foo**)&gFoo, f); } 

Y úsalo así:

 // thread 1 if (f = acquire_load() == ) { _mm_lfence(); ... use Foo } 

y:

 // thread 2 auto f = new Foo(); _mm_sfence(); // ensures Foo is constructed by the time f is published to gFoo release_store(f) 

¿Es eso correcto? Estoy bastante seguro de que la defensa es absolutamente necesaria aquí. ¿Pero qué hay de la cerca? ¿Es necesario o sería una simple barrera de comstackción suficiente para x64? por ejemplo, asm volátil (“”::: “memoria”). De acuerdo con el modelo de memoria x86, las cargas no se vuelven a ordenar con otras cargas. Por lo tanto, a mi entender, acquire_load () debe ocurrir antes de cualquier carga dentro de la sentencia if, siempre que haya una barrera de comstackción.

Podría estar equivocado sobre algunas cosas en esta respuesta (¡lectura de prueba de bienvenida de personas que saben esto!). Se basa en leer los documentos y el blog de Jeff Preshing, no en experiencias o pruebas recientes.

Linus Torvalds recomienda enfáticamente no tratar de inventar su propio locking, porque es muy fácil equivocarse. Es más un problema cuando escribo código portátil para el kernel de Linux, en lugar de algo que es solo x86, por lo que me siento lo suficientemente valiente como para tratar de resolver las cosas para x86.


La forma normal de usar tiendas NT es hacer un montón de ellas seguidas, como parte de un memset o memcpy, luego un SFENCE , luego una tienda de lanzamiento normal a una variable de indicador compartido: done_flag.store(1, std::memory_order_release) .

Usar una tienda movnti a la variable de sincronización dañará el rendimiento. Es posible que desee utilizar tiendas NT en el Foo que apunta, pero desalojar el puntero desde el caché es perverso. ( movnt tiendas movnt desalojan la línea de la memoria caché si estaba en la memoria caché , ver vol1 ch 10.4.6.2 Almacenamiento en memoria caché de datos temporales vs. no temporales ).

El objective de las tiendas NT es utilizarlo con datos no temporales, que no se usarán de nuevo (por ningún hilo) durante mucho tiempo, si es que lo hacen alguna vez. Los lockings que controlan el acceso a los almacenamientos intermedios compartidos, o los indicadores que los productores / consumidores utilizan para marcar los datos como leídos, se espera que sean leídos por otros núcleos.

Los nombres de tus funciones tampoco reflejan realmente lo que estás haciendo.

El hardware x86 está extremadamente optimizado para hacer tiendas de lanzamiento normales (no NT), porque cada tienda normal es una tienda de lanzamiento. El hardware tiene que ser bueno para que x86 funcione rápido.

El uso de tiendas / cargas normales solo requiere un viaje a la memoria caché L3, no a DRAM, para la comunicación entre subprocesos en las CPU Intel. La gran caché L3 inclusiva de Intel funciona como un tope para el tráfico de coherencia de caché. Al sondear las tags L3 en una falla de un núcleo se detectará el hecho de que otro núcleo tiene la línea de caché en el estado Modificado o Exclusivo . Las tiendas NT requerirían que las variables de sincronización lleguen a DRAM y vuelvan para que otro núcleo las vea.


Pedidos de memoria para tiendas de transmisión NT

movnt tiendas movnt se pueden reordenar con otras tiendas, pero no con lecturas antiguas.

Manual de x86 de Intel, volumen 3, capítulo 8.2.2 (Pedidos de memoria en P6 y familias de procesadores más recientes) :

  • Las lecturas no se reordenan con otras lecturas.
  • Las escrituras no se reordenan con lecturas antiguas . (tenga en cuenta la falta de excepciones).
  • Las escrituras en la memoria no se reordenan con otras escrituras, con las siguientes excepciones:
    • almacenes de transmisión (escrituras) ejecutados con las instrucciones de movimiento no temporal (MOVNTI, MOVNTQ, MOVNTDQ, MOVNTPS y MOVNTPD); y
    • operaciones de cuerda (ver Sección 8.2.4.1). (Nota: A partir de mi lectura de los documentos, las cadenas rápidas y las operaciones de ERMSB aún tienen implícitamente una barrera StoreStore al principio / final . Solo hay un posible reordenamiento entre las tiendas dentro de una única rep movs o rep stos ).
  • … cosas sobre clflushopt y las instrucciones de la valla

actualización: también hay una nota (en 8.1.2.2 Bloqueo de bus controlado por software ) que dice:

No implemente semáforos utilizando el tipo de memoria WC. No realice tiendas no temporales en una línea de caché que contenga una ubicación utilizada para implementar un semáforo.

Esto puede ser solo una sugerencia de rendimiento; no explican si puede causar un problema de corrección. Sin embargo, tenga en cuenta que las tiendas NT no son coherentes con la caché (los datos pueden permanecer en el búfer de relleno de línea incluso si hay datos conflictivos para la misma línea en otro lugar del sistema o en la memoria). Tal vez podría usar tiendas NT de forma segura como una tienda de lanzamiento que se sincroniza con cargas regulares, pero podría tener problemas con operaciones RMW atómicas como lock add dword [mem], 1 .


La semántica de la versión evita la reordenación de la memoria del write-release con cualquier operación de lectura o escritura que lo preceda en el orden del progtwig.

Para bloquear el reordenamiento con tiendas anteriores, necesitamos una instrucción SFENCE , que es una barrera StoreStore incluso para tiendas NT. (Y también es una barrera para algunos tipos de reordenamiento en tiempo de comstackción, pero no estoy seguro si bloquea las cargas anteriores al cruzar la barrera.) Las tiendas normales no necesitan ningún tipo de instrucción de barrera para ser tiendas de liberación, por lo que solo necesita SFENCE cuando usa tiendas NT.

Para cargas: el modelo de memoria x86 para memoria WB (write-back, es decir, “normal”) ya impide el reordenamiento de LoadStore incluso para tiendas poco ordenadas, por lo que no necesitamos un LFENCE para su efecto de barrera LoadStore , solo una barrera comstackdora LoadStore antes de la tienda NT En la implementación de gcc al menos, std::atomic_signal_fence(std::memory_order_release) es una barrera de comstackción incluso para cargas / almacenes no atómicos, pero atomic_thread_fence es solo una barrera para atomic<> loads / stores atomic<> (incluido mo_relaxed ). El uso de atomic_thread_fence aún le permite al comstackdor más libertad para reordenar cargas / almacenamientos a variables no compartidas. Vea este Q & A para más .

 // The function can't be called release_store unless it actually is one (ie includes all necessary barriers) // Your original function should be called relaxed_store void NT_release_store(const Foo* f) { // _mm_lfence(); // make sure all reads from the locked region are already globally visible. Not needed: this is already guaranteed std::atomic_thread_fence(std::memory_order_release); // no insns emitted on x86 (since it assumes no NT stores), but still a compiler barrier for earlier atomic<> ops _mm_sfence(); // make sure all writes to the locked region are already globally visible, and don't reorder with the NT store _mm_stream_si64((long long int*)&gFoo, (int64_t)f); } 

Esto almacena la variable atómica (nótese la falta de desreferenciación &gFoo ). Tu función almacena al Foo que apunta, lo cual es súper raro; IDK qué sentido tenía eso. También tenga en cuenta que comstack como código C ++ 11 válido .

Al pensar en lo que significa una tienda de lanzamiento, piense en ello como la tienda que libera el locking en una estructura de datos compartida. En su caso, cuando la tienda de lanzamiento se vuelve globalmente visible, cualquier hilo que la vea debería poder desreferenciarla de manera segura.


Para hacer una carga de adquisición, simplemente dígale al comstackdor que quiere una.

x86 no necesita ninguna instrucción de barrera, pero al especificar mo_acquire lugar de mo_relaxed obtendrá la barrera comstackdora necesaria. Como beneficio adicional, esta función es portátil: obtendrá todas las barreras necesarias en otras architectures:

 Foo* acquire_load() { return gFoo.load(std::memory_order_acquire); } 

No dijiste nada sobre el almacenamiento de gFoo en una gFoo de WC (memoria de escritura no combinable) débilmente ordenada. Probablemente sea realmente difícil organizar el segmento de datos de su progtwig en la memoria WC … Sería mucho más fácil para gFoo simplemente señalar a la memoria WC, después de mapear algo de RAM de video WC o algo así. Pero si desea adquirir cargas de la memoria WC, probablemente necesite LFENCE . IDK. Haga otra pregunta al respecto, porque esta respuesta asume que está utilizando la memoria WB.

Tenga en cuenta que usar un puntero en lugar de un indicador crea una dependencia de datos. Creo que deberías poder usar gFoo.load(std::memory_order_consume) , que no requiere barreras ni siquiera en CPU débilmente ordenadas (que no sean Alpha). Una vez que los comstackdores están lo suficientemente avanzados como para asegurarse de que no interrumpen la dependencia de los datos, en realidad pueden hacer un mejor código (en lugar de promocionar mo_consume a mo_acquire . Lea esto antes de usar mo_consume en el código de producción, y especialmente tenga cuidado de observar probarlo correctamente es imposible porque se espera que los comstackdores futuros den garantías más débiles que los comstackdores actuales en la práctica.


Inicialmente estaba pensando que necesitábamos LFENCE para obtener una barrera de LoadStore. (“Las escrituras no pueden pasar las instrucciones anteriores de LFENCE, SFENCE y MFENCE”. Esto, a su vez, evita que pasen (se vuelven globalmente visibles antes) lecturas que están antes de LFENCE).

Tenga en cuenta que LFENCE + SFENCE es aún más débil que un MFENCE completo, porque no es una barrera StoreLoad. La propia documentación de SFENCE dice que está ordenada. LFENCE, pero esa tabla del modelo de memoria x86 de Intel manual vol3 no menciona eso. Si SFENCE no puede ejecutarse hasta después de un LFENCE, entonces sfence / lfence podría en realidad ser un equivalente más lento a mfence , pero lfence / sfence / movnti daría semántica de lanzamiento sin una barrera completa. Tenga en cuenta que la tienda NT podría volverse globalmente visible después de algunas cargas / tiendas siguientes, a diferencia de una tienda x86 fuertemente ordenada normal).


Relacionado: cargas NT

En x86, cada carga tiene una semántica de adquisición, a excepción de las cargas de la memoria de WC. SSE4.1 MOVNTDQA es la única instrucción de carga no temporal, y no está débilmente ordenada cuando se usa en la memoria normal (WriteBack). También es una carga de adquisición (cuando se usa en la memoria WB).

Tenga en cuenta que movntdq solo tiene una forma de tienda, mientras que movntdqa solo tiene una forma de carga. Pero aparentemente Intel no podía simplemente llamarlos storentdqa y loadntdqa . Ambos tienen un requisito de alineación 16B o 32B, por lo que dejar de usar el a no tiene mucho sentido para mí. Creo que SSE1 y SSE2 ya habían introducido algunas tiendas NT que ya usaban el mnemónico mov... ( movntps ), pero no cargas hasta años después en SSE4.1. (2nd-gen Core2: 45nm Penryn).

Los documentos dicen que MOVNTDQA no cambia la semántica de ordenamiento para el tipo de memoria en que se usa .

… Una implementación también puede hacer uso de la sugerencia no temporal asociada con esta instrucción si la fuente de memoria es de tipo WB (write back).

La implementación de un procesador de la sugerencia no temporal no anula la semántica del tipo de memoria efectiva , pero la implementación de la sugerencia depende del procesador. Por ejemplo, una implementación de procesador puede elegir ignorar la sugerencia y procesar la instrucción como un MOVDQA normal para cualquier tipo de memoria.

En la práctica, las CPU actuales Intel mainsream (Haswell, Skylake) parecen ignorar la sugerencia de cargas PREFETCHNTA y MOVNTDQA de la memoria WB . Consulte ¿Las architectures x86 actuales admiten cargas no temporales (desde la memoria “normal”)? , y también cargas no temporales y el precaptador de hardware, ¿funcionan juntos? para más detalles.


Además, si lo está usando en la memoria de WC (por ejemplo, copia de RAM de video, como en esta guía de Intel ):

Como el protocolo WC usa un modelo de coherencia de memoria débilmente ordenado, se debe usar una instrucción MFENCE o bloqueada junto con instrucciones MOVNTDQA si varios procesadores pueden hacer referencia a las mismas ubicaciones de memoria WC o para sincronizar lecturas de un procesador con escrituras de otros agentes en el sistema.

Sin embargo, eso no explica cómo debería usarse. Y no estoy seguro de por qué dicen MFENCE en lugar de LFENCE para leer. Tal vez están hablando de una situación de memoria de escritura a dispositivo, lectura desde el dispositivo, donde las tiendas deben ordenarse con respecto a las cargas (barrera de StoreLoad), no solo entre sí (barrera de StoreStore).

Busqué en Vol3 para movntdqa , y no movntdqa ningún golpe (en todo el pdf). 3 éxitos para movntdq : toda la discusión sobre ordenación débil y tipos de memoria solo habla de tiendas. Tenga en cuenta que LFENCE se introdujo mucho antes que SSE4.1. Presumiblemente es útil para algo, pero IDK qué. Para el pedido de carga, probablemente solo con memoria WC, pero no he leído cuando sería útil.


LFENCE parece ser más que solo una barrera LoadLoad para cargas débilmente ordenadas: también ordena otras instrucciones. (No la visibilidad global de las tiendas, sin embargo, solo su ejecución local).

Del manual de insn ref de Intel:

Específicamente, LFENCE no se ejecuta hasta que todas las instrucciones anteriores se hayan completado localmente, y ninguna instrucción posterior comience a ejecutarse hasta que se complete LFENCE.

Las instrucciones que siguen a un LFENCE se pueden obtener de la memoria antes del LFENCE, pero no se ejecutarán hasta que se complete el LFENCE.

La entrada para rdtsc sugiere usar LFENCE;RDTSC para evitar que se ejecute antes de las instrucciones anteriores, cuando RDTSCP no está disponible (y la garantía de orden más débil está bien: rdtscp no deja de seguir las instrucciones de ejecución antes). ( CPUID es una sugerencia común para serializar la secuencia de instrucciones alrededor de rdtsc ).