¿Las cargas y las tiendas son las únicas instrucciones que se reordenan?

He leído muchos artículos sobre pedidos de memoria, y todos ellos solo dicen que una CPU reordena las cargas y las almacena.

¿Una CPU (estoy específicamente interesada en una CPU x86) solo reordena las cargas y almacena, y no reordena el rest de las instrucciones que tiene?

La ejecución fuera de orden conserva la ilusión de ejecutar en orden de progtwig para un solo hilo / núcleo . Esto es como la regla de optimización as-if de C / C ++: haga lo que desee internamente siempre que los efectos visibles sean los mismos.

Los hilos separados solo pueden comunicarse entre sí a través de la memoria, por lo que el orden global de las operaciones de memoria (cargas / almacenamientos) es el único efecto secundario externamente visible de la ejecución 1 .

Incluso las CPU en orden pueden tener sus operaciones de memoria visibles globalmente fuera de servicio. (por ejemplo, incluso una simple tubería RISC con un buffer de tienda tendrá StoreLoad reordenando, como x86). Una CPU que comienza a cargar / almacenar en orden pero que les permite completar fuera de servicio (para ocultar la latencia de falta de caché) también podría reordenar las cargas si no las evita específicamente (o al igual que el x86 moderno, ejecute agresivamente fuera de ordene pero pretenda que no lo hace siguiendo el orden de la memoria con cuidado).


Un simple ejemplo: dos cadenas de dependencias ALU pueden superponerse

(relacionado: http://blog.stuffedcow.net/2013/05/measuring-rob-capacity/ para obtener más información sobre cuán grande es la ventana para encontrar el paralelismo a nivel de instrucción, por ejemplo, si aumentó esto a times 200 vería solo superposición limitada. También relacionado: esta respuesta de principiante a nivel intermedio que escribí sobre cómo una CPU OoO como Haswell o Skylake encuentra y explota ILP.)

 global _start _start: mov ecx, 10000000 .loop: times 25 imul eax,eax ; expands to imul eax,eax / imul eax,eax / ... ; lfence times 25 imul edx,edx ; lfence dec ecx jnz .loop xor edi,edi mov eax,231 syscall ; sys_exit_group(0) 

construido (con nasm + ld ) en un ejecutable estático en x86-64 Linux, esto se ejecuta (en Skylake) en los ciclos de reloj de 750M esperados para cada cadena de 25 * 10M instrucciones de imul multiplicadas por 3 ciclos de latencia.

Comentar una de las cadenas de imul no cambia el tiempo de ejecución: todavía ciclos de 750M.

Esta es una prueba definitiva de la ejecución fuera de servicio intercalando las dos cadenas de dependencia, de lo contrario. (El rendimiento de imul es de 1 por reloj, latencia de 3 relojes. http://agner.org/optimize/ . De modo que una tercera cadena de dependencias podría mezclarse sin mucha ralentización).

Números reales del taskset -c 3 ocperf.py stat --no-big-num -etask-clock,context-switches,cpu-migrations,page-faults,cycles:u,branches:u,instructions:u,uops_issued.any:u,uops_executed.thread:u,uops_retired.retire_slots:u -r3 ./imul de taskset -c 3 ocperf.py stat --no-big-num -etask-clock,context-switches,cpu-migrations,page-faults,cycles:u,branches:u,instructions:u,uops_issued.any:u,uops_executed.thread:u,uops_retired.retire_slots:u -r3 ./imul :

  • con ambas cadenas de imul: 750566384 +- 0.1%
  • con solo la cadena EAX: 750704275 +- 0.0%
  • con una times 50 imul eax,eax cadena times 50 imul eax,eax : 1501010762 +- 0.0% (casi exactamente el doble de lento, como se esperaba).
  • con una lfence evita la superposición entre cada bloque de 25 imul : 1688869394 +- 0.0% , peor que el doble de lento. uops_issued_any y uops_retired_retire_slots son 63M, por encima de 51M, mientras que uops_executed_thread sigue siendo 51M ( lfence no usa ningún puerto de ejecución, pero aparentemente dos instrucciones de lfence cuestan 6 uops de dominio fusionado cada uno. Agner Fog solo midió 2.)

(La lfence serializa la ejecución de la instrucción , pero no almacena la memoria). Si no está utilizando cargas NT desde la memoria WC (lo que no ocurrirá por accidente), no necesita más que detener la ejecución de las instrucciones posteriores hasta que las instrucciones anteriores se hayan “completado localmente”. es decir, hasta que se hayan retirado del núcleo fuera de servicio. Esta es probablemente la razón por la que más que duplica el tiempo total: tiene que esperar a que el último imul en un bloque pase por más etapas de canalización).

lfence de Intel siempre es así, pero en AMD solo se está serializando parcialmente con la mitigación de Espectro habilitada .


Nota al pie 1 : También hay canales laterales de temporización cuando dos hilos lógicos comparten un hilo físico (hyperthreading u otro SMT). por ejemplo, ejecutar una secuencia de instrucciones de imul independientes se ejecutará a 1 por reloj en una CPU Intel reciente, si el otro hyperthread no necesita el puerto 1 para nada. De modo que puede medir la cantidad de presión del puerto 0 al sincronizar un ciclo ALU-bound en un núcleo lógico.

Otros canales laterales de micro-architecture, como los accesos de caché, son más confiables. Por ejemplo, Spectre / Meltdown es más fácil de explotar con un canal lateral de lectura de caché, en lugar de ALU.

Pero todos estos canales laterales son meticulosos y poco fiables en comparación con las lecturas / escrituras respaldadas arquitectónicamente en la memoria compartida, por lo que solo son relevantes para la seguridad. No se usan intencionalmente dentro del mismo progtwig para comunicarse entre hilos.


MFENCE en Skylake es una barrera ejecutiva de OoO como LFENCE

mfence en Skylake bloquea inesperadamente la ejecución fuera de orden de imul , como lfence , aunque no está documentado para tener ese efecto. (Para más información, vea la discusión movida al chat).

xchg [rdi], ebx (prefijo de lock implícito) no bloquea la ejecución fuera de orden de las instrucciones ALU. El tiempo total sigue siendo de 750M de ciclos cuando se reemplaza una lfence con xchg o una instrucción de lock en la prueba anterior.

Pero con mfence , el costo sube a 1500M ciclos + el tiempo para instrucciones de 2 mfence . Para hacer un experimento controlado, mantuve el recuento de instrucciones igual pero moví las instrucciones de mfence una al lado de la otra, para que las cadenas de imul pudieran reordenarse entre ellas, y el tiempo bajó a 750M + el tiempo para instrucciones de 2 mfence .

Es muy probable que este comportamiento de Skylake sea el resultado de una actualización de microcódigo para reparar la errata SKL079 , MOVNTDQA. De la memoria WC pueden pasar las instrucciones anteriores de MFENCE . La existencia de la errata muestra que solía ser posible ejecutar las instrucciones posteriores antes de que se completara, así que probablemente hicieron una corrección de fuerza bruta al agregar lfence uups” al microcódigo para ” mfence .

Este es otro factor a favor del uso de xchg para las tiendas seq-cst, o incluso lock add algo de memoria de la stack como una barrera independiente. Linux ya hace ambas cosas, pero los comstackdores todavía usan mfence para las barreras. Consulte ¿Por qué una tienda std :: atomic con consistencia secuencial usa XCHG?

(Consulte también la discusión sobre las opciones de barrera de Linux en este hilo de Google Groups , con enlaces a 3 recomendaciones separadas para usar lock addl $0, -4(%esp/rsp) lugar de mfence como una barrera independiente.

Los procesadores fuera de servicio generalmente pueden reordenar todas las instrucciones cuando hacerlo sea posible, factible y beneficioso para el rendimiento. Debido al registro de cambio de nombre, esto es transparente para el código de la máquina excepto en el caso de cargas y tiendas Es por eso que las personas generalmente solo hablan sobre la carga y el reordenamiento de la tienda, ya que es el único tipo de reordenamiento observable.


Por lo general, las excepciones de FPU también son algo en lo que se puede observar el reordenamiento. La mayoría de los procesadores fuera de servicio tienen excepciones imprecisas por esta razón, pero no x86. En x86, el procesador se asegura de que las excepciones se informen como si las operaciones de coma flotante no se reordenaron.