¿Cuál es el propósito de la instrucción "PAUSA" en x86?


Estoy tratando de crear una versión tonta de un bloqueo de giro. Navegando por la web, me encontré con una instrucción de ensamblaje llamada "PAUSA" en x86 que se utiliza para dar pista a un procesador de que un spin-lock se está ejecutando actualmente en esta CPU. El manual de intel y otra información disponible indican que

El procesador utiliza esta sugerencia para evitar la violación del orden de memoria en la mayoría de las situaciones, lo que mejora en gran medida el rendimiento del procesador. Para por esta razón, se recomienda que una PAUSA la instrucción se colocará en todos los bucles spin-wait. La documentación también menciona que " espera (algunos delay) " es la pseudo implementación de la instrucción.

La última línea del párrafo anterior es intuitiva. Si no tengo éxito en agarrar la cerradura, debo esperar un tiempo antes de agarrar la cerradura de nuevo.

Sin embargo, ¿qué entendemos por violación de orden de memoria en caso de un bloqueo de giro? ¿"violación de orden de memoria" significa la incorrecta especulativa carga / almacenamiento de las instrucciones después de spin-lock?

La pregunta spin-lock se ha hecho en Stack overflow antes, pero la pregunta de violación del orden de memoria permanece sin respuesta (al menos para mi comprensión).

Author: Flow, 2012-10-15

2 answers

Imagínese, cómo el procesador ejecutaría un bucle spin-wait típico:

1 Spin_Lock:
2    CMP lockvar, 0   ; Check if lock is free
3    JE Get_Lock
4    JMP Spin_Lock
5 Get_Lock:

Después de unas cuantas iteraciones, el predictor de ramificaciones predecirá que la rama condicional (3) nunca será tomada y la canalización se llenará con instrucciones CMP (2). Esto continúa hasta que finalmente otro procesador escribe un cero en lockvar. En este punto, tenemos la tubería llena de instrucciones de CMP especulativas (es decir, aún no comprometidas), algunas de las cuales ya leyeron lockvar e informaron un (incorrecto) distinto de cero resultado a la siguiente rama condicional (3) (también especulativa). Esto es cuando ocurre la violación del orden de memoria. Cada vez que el procesador "ve" una escritura externa (una escritura de otro procesador), busca en su canalización instrucciones que especulativamente accedieron a la misma ubicación de memoria y aún no confirmaron. Si se encuentra alguna de estas instrucciones, el estado especulativo del procesador no es válido y se borra con un flujo de canalización.

Desafortunadamente este escenario será (muy probable) repita cada vez que un procesador esté esperando un bloqueo giratorio y haga que estos bloqueos sean mucho más lentos de lo que deberían ser.

Introduzca la instrucción de PAUSA:

1 Spin_Lock:
2    CMP lockvar, 0   ; Check if lock is free
3    JE Get_Lock
4    PAUSE            ; Wait for memory pipeline to become empty
5    JMP Spin_Lock
6 Get_Lock:

La instrucción PAUSE "des-canalizará" la memoria lee, de modo que la canalización no se llene con instrucciones especulativas CMP (2) como en el primer ejemplo. (Es decir, podría bloquear la canalización hasta que se confirmen todas las instrucciones de memoria antiguas.) Debido a que las instrucciones CMP (2) se ejecutan secuencialmente es poco probable (i. e. la ventana de tiempo es mucho más corta) que una escritura externa ocurre después de que la instrucción CMP (2) lea lockvar pero antes de que se confirme la CMP.

Por supuesto, "de-pipelining" también desperdiciará menos energía en el spin-lock y en caso de hyperthreading no desperdiciará recursos que el otro hilo podría usar mejor. Por otro lado, todavía hay una predicción errónea de rama esperando que ocurra antes de cada salida de bucle. La documentación de Intel no sugiere que PAUSE elimine ese flujo de canalización, pero quién sabe...

 67
Author: Mackie Messer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-16 10:06:49

Como dice @Mackie, la canalización se llenará con cmps. Intel tendrá que vaciar esos cmps cuando otro núcleo escriba, lo cual es una operación costosa. Si la CPU no lo descarga, entonces usted tiene una violación de orden de memoria. Un ejemplo de tal violación sería el siguiente:

(Esto comienza con lock1 = lock2 = lock3 = var = 1)

Hilo 1:

spin:
cmp lock1, 0
jne spin
cmp lock3, 0 # lock3 should be zero, Thread 2 already ran.
je end # Thus I take this path
mov var, 0 # And this is never run
end:

Hilo 2:

mov lock3, 0
mov lock1, 0
mov ebx, var # I should know that var is 1 here.

Primero, considere el Hilo 1:

Si cmp lock1, 0; jne spin la rama predice que lock1 no es cero, añade cmp lock3, 0 a la canalización.

En la canalización, cmp lock3, 0 lee lock3 y descubre que es igual a 1.

Ahora, supongamos que el Hilo 1 está tomando su tiempo dulce, y el Hilo 2 comienza a correr rápidamente:

lock3 = 0
lock1 = 0

Ahora, volvamos al Hilo 1:

Digamos que el cmp lock1, 0 finalmente lee lock1, descubre que lock1 es 0, y está contento con su capacidad de predicción de ramas.

Este comando se confirma, y nada se vacían. Medios de predicción de ramificación correctos nada se descarga, incluso con lecturas fuera de orden, ya que el procesador dedujo que no hay dependencia interna. lock3 no depende de lock1 a los ojos de la CPU, por lo que todo está bien.

Ahora, el cmp lock3, 0, que leyó correctamente que lock3 era igual a 1, se confirma.

je end no se toma, y mov var, 0 ejecuta.

En el Hilo 3, ebx es igual a 0. Esto debería haber sido imposible. Esta es la violación de la orden de memoria que Intel debe compensar para.


Ahora, la solución que Intel toma para evitar ese comportamiento no válido, es vaciar. Cuando lock3 = 0 se ejecuta en el Hilo 2, obliga al Hilo 1 a limpiar las instrucciones que usan lock3. El lavado en este caso significa que el hilo 1 no agregará instrucciones a la canalización hasta que se hayan confirmado todas las instrucciones que usan lock3. Antes de que el Hilo 1's cmp lock3 pueda confirmar, el cmp lock1 debe confirmar. Cuando el cmp lock1 intenta confirmar, lee que lock1 es realmente igual a 1, y que la rama la predicción fue un fracaso. Esto hace que el cmp a echaran. Ahora que el Hilo 1 está vaciado, la ubicación de lock3 en la caché del Hilo 1 se establece en 0, y luego el Hilo 1 continúa la ejecución (Esperando lock1). El hilo 2 ahora recibe una notificación de que todos los demás núcleos han vaciado el uso de lock3 y actualizado sus cachés, por lo que el Hilo 2 luego continúa la ejecución (Habrá ejecutado instrucciones independientes mientras tanto, pero la siguiente instrucción fue otra escritura, por lo que probablemente tenga que colgar, a menos que otros núcleos tienen una cola para mantener la escritura pendiente lock1 = 0).

Todo este proceso es costoso, de ahí la PAUSA. La PAUSA ayuda al Hilo 1, que ahora puede recuperarse de la inminente mala interpretación de la rama al instante, y no tiene que limpiar su canalización antes de ramificar correctamente. La PAUSA ayuda de manera similar al Hilo 2, que no tiene que esperar en el lavado del Hilo 1 (como se dijo antes, no estoy seguro de este detalle de implementación, pero si el Hilo 2 intenta escribir bloqueos utilizados por demasiados otros núcleos, el hilo 2 eventualmente tendrá que esperar en los rubores).

Un entendimiento importante es que mientras en mi ejemplo, el flush es requerido, en el ejemplo de Mackie, no lo es. Sin embargo, la CPU no tiene forma de saberlo (no analiza el código en absoluto, aparte de comprobar dependencias de sentencias consecutivas y una caché de predicción de ramificaciones), por lo que la CPU descargará las instrucciones que acceden a lockvar en el ejemplo de Mackie tal como lo hace en el mío, para garantizar la corrección.

 3
Author: Nicholas Pipitone,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-08-01 20:45:20