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

Estoy tratando de crear una versión tonta de un locking de giro. Al navegar por la web, encontré una instrucción de ensamble llamada “PAUSE” en x86 que se usa para dar pistas 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 usa esta sugerencia para evitar la violación de orden de memoria en la mayoría de las situaciones, lo que mejora enormemente el rendimiento del procesador. Por esta razón, se recomienda que se coloque una instrucción PAUSE en todos los bucles spin-wait. La documentación también menciona que “esperar (un poco de retraso)” es la pseudo implementación de la instrucción.

La última línea del párrafo anterior es intuitiva. Si no logro agarrar la cerradura, debo esperar un tiempo antes de volver a agarrar la cerradura.

Sin embargo, ¿qué queremos decir con violación de orden de memoria en caso de un locking de giro? ¿La “violación de orden de memoria” significa la carga / almacenamiento especulativo incorrecto de las instrucciones después del locking de giro?

La pregunta de locking de giro se ha preguntado sobre el desbordamiento de stack antes, pero la pregunta de violación de orden de memoria permanece sin respuesta (al menos para mi comprensión).

Solo imagínense cómo el procesador ejecutaría un ciclo típico de girar-esperar:

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 algunas iteraciones, el predictor de bifurcación predecirá que la bifurcación condicional (3) nunca se tomará y la tubería se completará con las instrucciones de CMP (2). Esto continúa hasta que, finalmente, otro procesador escribe un cero en lockvar. En este punto, tenemos la cartera llena de instrucciones CMP especulativas (es decir, aún no comprometidas), algunas de las cuales ya han leído lockvar e informado un resultado (incorrecto) distinto de cero a la siguiente twig condicional (3) (también especulativa). Esto es cuando ocurre la violación de orden de memoria. Cada vez que el procesador “ve” una escritura externa (una escritura de otro procesador), busca en su canalización las instrucciones que especulativamente accedieron a la misma ubicación de memoria y aún no se han confirmado. Si se encuentran tales instrucciones, entonces el estado especulativo del procesador no es válido y se borra con una descarga de tubería.

Desafortunadamente, este escenario (muy probablemente) se repetirá cada vez que un procesador esté esperando un locking de giro y hará que estos lockings sean mucho más lentos de lo que deberían.

Ingrese la instrucción PAUSE:

 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 “eliminará” las lecturas de la memoria, de modo que la tubería no se llene con instrucciones CMP (2) especulativas como en el primer ejemplo. (Es decir, podría bloquear la tubería hasta que se hayan confirmado todas las instrucciones anteriores). Debido a que las instrucciones CMP (2) se ejecutan secuencialmente, es poco probable (es decir, la ventana de tiempo es mucho más corta) que se produzca una escritura externa después de leer la instrucción CMP (2) lockvar pero antes de que el CMP esté comprometido.

Por supuesto, el “desentrelazado” también desperdiciará menos energía en el locking de giro 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 bifurcación esperando a ocurrir antes de que salga cada bucle. La documentación de Intel no sugiere que PAUSE elimine ese flujo de tuberías, pero quién sabe …

Como dice @Mackie, la tubería se llenará con cmp s. Intel tendrá que enjuagar esos cmp cuando otro núcleo escriba, lo cual es una operación costosa. Si la CPU no lo vacía, entonces 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)

Tema 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: 

Tema 2:

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

Primero, considere el Tema 1:

si cmp lock1, 0; jne spin cmp lock1, 0; jne spin branch predice que lock1 no es cero, agrega cmp lock3, 0 a la canalización.

En la tubería, cmp lock3, 0 lee lock3 y descubre que es igual a 1.

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

 lock3 = 0 lock1 = 0 

Ahora, volvamos al Tema 1:

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

Este comando se compromete y no se vacía nada. Corregir la predicción de bifurcación significa que no se vacía nada, incluso con lecturas fuera de orden, ya que el procesador dedujo que no hay una 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 compromete.

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

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


Ahora, la solución que Intel toma para evitar ese comportamiento no válido es descargar. Cuando lock3 = 0 ejecutó en el subproceso 2, fuerza al subproceso 1 a vaciar las instrucciones que usan lock3. Vaciar en este caso significa que el Subproceso 1 no agregará instrucciones a la interconexión hasta que se hayan confirmado todas las instrucciones que usan lock3. Antes de que el cmp lock3 Thread 1 pueda confirmar, el cmp lock1 debe confirmar. Cuando el cmp lock1 intenta confirmar, se lee que lock1 es realmente igual a 1, y que la predicción de bifurcación fue un error. Esto hace que el cmp sea ​​expulsado. Ahora que el Tema 1 está enrojecido, la ubicación del lock3 3 en el caché del Tema 1 se establece en 0 , y luego el Tema 1 continúa la ejecución (En espera en el lock1 ). El hilo 2 ahora recibe una notificación de que todos los demás núcleos han descargado el uso de lock3 y han actualizado sus cachés, por lo que el Thread 2 continúa la ejecución (Mientras tanto, habrá ejecutado sentencias independientes, pero la siguiente instrucción fue otra escritura por lo que probablemente deba colgarse, a menos que los otros núcleos tengan una cola para contener el lock1 = 0 pendiente1 lock1 = 0 escritura).

Todo este proceso es costoso, de ahí la PAUSA. PAUSE ayuda a Thread 1, que ahora puede recuperarse de la inminente bifurcación de twigs incorrecta al instante, y no tiene que vaciar su tubería antes de la bifurcación correctamente. La PAUSA ayuda de manera similar al Subproceso 2, que no tiene que esperar el enjuague del Subproceso 1 (Como dije antes, no estoy seguro de este detalle de implementación, pero si el Subproceso 2 intenta escribir lockings usados ​​por muchos otros núcleos, el Subproceso 2 eventualmente tendrá que esperar en las descargas).

Una comprensión importante es que, si bien en mi ejemplo, se requiere la descarga, en el ejemplo de Mackie, no lo es. Sin embargo, la CPU no tiene manera de saber (no analiza el código en absoluto, aparte de verificar dependencias de sentencias consecutivas, y una caché de predicción de bifurcación), por lo que la CPU borrará las instrucciones que acceden a lockvar en el ejemplo de Mackie tal como lo hace en la mía , para garantizar la corrección.