Instrucciones de carga globalmente invisibles
¿Es posible que algunas de las instrucciones de carga nunca sean visibles globalmente debido al reenvío de carga de la tienda? Para decirlo de otra manera, si una instrucción de carga obtiene su valor del búfer de almacenamiento, nunca tiene que leer del caché.
Como generalmente se afirma que una carga es globalmente visible cuando lee desde el caché L1D, las que no leen desde el L1D deberían hacerla globalmente invisible.
El concepto de visibilidad global para cargas es complicado, porque una carga no modifica el estado global de la memoria y otros subprocesos no pueden observarla directamente .
Pero una vez que el polvo se asienta después de una ejecución especulativa/fuera de orden, podemos saber qué valor obtuvo la carga si el subproceso la almacena en algún lugar o se bifurca en función de él. Este comportamiento observable del hilo es lo importante. (O podríamos observarlo con un depurador y/o simplemente razonar sobre qué valores podría ver una carga, si un experimento es difícil).
Al menos en CPU fuertemente ordenadas como x86, todas las CPU pueden acordar que un orden total de tiendas se vuelva globalmente visible , actualizando el único estado coherente+caché+memoria coherente. En x86, donde no se permite el reordenamiento de StoreStore , este TSO (Pedido total de la tienda) coincide con el orden del programa de cada subproceso. (Es decir, el orden total es un entrelazado del orden del programa de cada hilo). SPARC TSO también tiene un orden tan estricto.
(Observar correctamente el orden globalmfence
de sus propias tiendas en relación con otras tiendas requiere o similar: de lo contrario, el reenvío de tiendas significa que puede ver sus propias tiendas de inmediato, antes de que sean visibles para otros núcleos. x86 TSO es básicamente orden de programa más tienda- reenvío.)
(Para los almacenes que omiten el caché, la visibilidad global es cuando se vacían desde los buffers privados de combinación de escritura a la DRAM. Los Intel Line Fill Buffers o cualquier mecanismo privado equivalente de combinación de escritura donde los datos del almacén todavía no son visibles para otras CPU son efectivamente parte de el buffer de la tienda para nuestros propósitos de reordenamiento.)
En una ISA débilmente ordenada, es posible que los subprocesos A y B no coincidan en el orden de las tiendas X e Y realizadas por los subprocesos C y D, incluso si los subprocesos de lectura utilizan cargas de adquisición para asegurarse de que sus propias cargas no se reordenen. es decir, puede que no haya ningún orden global de tiendas, y mucho menos que no sea el mismo que el orden del programa.
IBM POWER ISA es así de débil, al igual que el modelo de memoria C++ 11 (¿ otros subprocesos siempre verán dos escrituras atómicas en diferentes ubicaciones en diferentes subprocesos en el mismo orden? ). Pero el mecanismo en la práctica en POWER es que las tiendas (retiradas, también conocidas como graduadas) se vuelven visibles para algunos otros núcleos antes de que se vuelvan visibles globalmente al comprometerse con la caché L1d. El caché en sí es realmente coherente incluso en sistemas POWER, como todas las CPU normales, y permite recuperar la coherencia secuencial con barreras. Estos efectos de orden múltiple solo ocurren debido a que SMT (múltiples CPU lógicas en una CPU física) proporciona una forma de ver los almacenes de otros núcleos lógicos sin pasar por el caché.
(Un posible mecanismo es permitir que otros subprocesos lógicos husmeen en las tiendas no especulativas del búfer de la tienda incluso antes de que se comprometan con L1d, manteniendo únicamente las tiendas aún no retiradas privadas de un subproceso lógico. Esto podría reducir ligeramente la latencia entre subprocesos. x86 No puedo hacer esto porque rompería el modelo de memoria fuerte; el HT de Intel divide estáticamente el búfer de almacenamiento cuando hay dos subprocesos activos en un núcleo. Pero como comenta @BeeOnRope, un modelo abstracto de qué reordenaciones están permitidas es probablemente un mejor enfoque para razonamiento sobre la corrección. El hecho de que no se pueda pensar en un mecanismo de HW para provocar un reordenamiento no significa que no pueda suceder ) .
Sin embargo, las ISA débilmente ordenadas que no son tan débiles como POWER (en la práctica y/o en papel) aún se reordenan en el buffer de almacenamiento local de cada núcleo, si no se utilizan barreras o almacenes de liberación. En muchas CPU existe un orden global para todas las tiendas, pero no se trata de un orden entrelazado de programas. Las CPU OoO tienen que rastrear el orden de la memoria, por lo que un solo subproceso no necesita barreras para ver sus propias tiendas en orden, pero permitir que las tiendas se comprometan desde el búfer de la tienda a L1d fuera del orden del programa ciertamente podría mejorar el rendimiento (especialmente si hay varias tiendas). pendiente para la misma línea, pero el orden del programa expulsaría la línea de un caché asociativo entre cada tienda (por ejemplo, un patrón de acceso a histograma desagradable).
Hagamos un experimento mental sobre de dónde provienen los datos de carga.
Lo anterior sigue siendo sólo sobre la visibilidad de la tienda, no sobre las cargas. ¿Podemos explicar que el valor visto por cada carga se lee desde la memoria/caché global en algún momento (sin tener en cuenta las reglas de orden de carga)?
Si es así, entonces todos los resultados de la carga se pueden explicar poniendo todos los almacenes y cargas de todos los subprocesos en algún orden combinado, leyendo y escribiendo un estado global coherente de memoria.
Resulta que no, no podemos, el búfer de la tienda rompe esto : el reenvío parcial de tienda a carga nos da un contraejemplo (en x86, por ejemplo). Un almacén estrecho seguido de una carga amplia puede fusionar datos del búfer del almacén con datos de la caché L1d antes de que el almacén se vuelva globalmente visible. Las CPU x86 reales realmente hacen esto y tenemos experimentos reales para demostrarlo.
Si solo observa el reenvío de almacén completo, donde la carga solo toma sus datos de un almacén en el búfer de almacenamiento, se podría argumentar que el búfer de almacenamiento retrasa la carga. es decir, que la carga aparece en el orden global de carga total-tienda justo después de la tienda que hace que ese valor sea globalmente visible.
(Este orden global de almacenamiento de carga total no es un intento de crear un modelo de ordenamiento de memoria alternativo; no tiene forma de describir las reglas de ordenamiento de carga reales de x86).
El reenvío de almacenamiento parcial expone el hecho de que los datos de carga no siempre provienen del dominio de caché global coherente.
Si un almacén de otro núcleo cambia los bytes circundantes, una carga atómica amplia podría leer un valor que nunca existió y nunca existirá en el estado coherente global.
Vea mi respuesta sobre ¿Puede x86 reordenar una tienda estrecha con una carga más amplia que la contenga por completo? , y la respuesta de Alex como prueba experimental de que tal reordenamiento puede ocurrir, lo que invalida el esquema de bloqueo propuesto en esa pregunta. Un almacenamiento y luego una recarga desde la misma dirección no es una barrera de memoria StoreLoad .
Algunas personas (por ejemplo, Linus Torvalds) describen esto diciendo que el buffer de almacenamiento no es coherente . (Linus estaba respondiendo a alguien que había inventado de forma independiente la misma idea de bloqueo no válido).
Otra pregunta y respuesta sobre el búfer de almacenamiento y la coherencia: ¿Cómo configurar bits de un vector de bits de manera eficiente en paralelo? . Puede realizar algunos OR no atómicos para configurar bits y luego regresar y verificar si hay actualizaciones perdidas debido a conflictos con otros subprocesos. Pero necesita una barrera StoreLoad (por ejemplo, un x86 lock or
) para asegurarse de no ver solo sus propias tiendas cuando recarga.
Definición propuesta: una carga se vuelve globalmente visible cuando lee sus datos. Normalmente desde L1d, pero el búfer de almacenamiento o MMIO o la memoria que no se puede almacenar en caché son otras fuentes posibles.
Esta definición concuerda con los manuales de x86 que dicen que las cargas no se reordenan con otras cargas. es decir, se cargan (en el orden del programa) desde la vista de memoria del núcleo local.
La carga en sí puede volverse globalmente visible independientemente de si algún otro hilo podría cargar ese valor desde esa dirección.
Aunque quizás tendría más sentido no hablar en absoluto de "visibilidad global" de las cargas almacenables en caché , porque están extrayendo datos de algún lugar, sin hacer nada con un efecto visible. Sólo las cargas que no se pueden almacenar en caché (por ejemplo, de una región MMIO) deben considerarse efectos secundarios visibles.
(En x86, las tiendas y cargas que no se pueden almacenar en caché están muy ordenadas, por lo que creo que el reenvío de tiendas a una tienda que no se puede almacenar en caché es imposible. A menos que tal vez la tienda se haya realizado a través de una asignación de WB de la misma página física a la que accede la carga de UC).
Permítanme ampliar un poco la pregunta y analizar el aspecto correcto de implementar el reenvío de carga de la tienda. (Creo que la segunda mitad de la respuesta de Peter responde directamente a la pregunta).
El reenvío de carga almacenada cambia la latencia de la carga, no su visibilidad. A menos que se haya eliminado debido a alguna especulación errónea, la tienda eventualmente se volverá visible a nivel mundial de todos modos. Sin reenvío de carga de tienda, la carga tiene que esperar hasta que se retiren todas las tiendas en conflicto. Entonces la carga puede recuperar los datos normalmente.
(La definición exacta de un almacén en conflicto depende del modelo de ordenación de la memoria de ISA. En x86, asumiendo el tipo de memoria WB, que permite el reenvío de carga del almacén, cualquier almacén que esté más temprano en el orden del programa y cuya ubicación de memoria física de destino se superponga a esa de la carga es un almacén en conflicto).
Aunque si hay algún almacén en conflicto simultáneo de otro agente en el sistema, eso podría cambiar el valor cargado porque el almacén externo puede entrar en vigor después del almacén local pero antes de la carga local. Normalmente, el búfer del almacén no está en el dominio de coherencia, por lo que el reenvío de carga del almacén puede reducir la probabilidad de que algo así suceda. Esto depende de las limitaciones de la implementación del reenvío de carga del almacén; Por lo general, no hay garantías de que se realice el reenvío para ninguna operación de carga y almacenamiento en particular.
El reenvío de carga de tienda también puede dar lugar a pedidos de memoria global que no habrían sido posibles sin él. Por ejemplo, en el modelo fuerte de x86, se permite el reordenamiento de la carga del almacén y, junto con el reenvío de la carga del almacén, puede permitir que cada agente del sistema vea todas las operaciones de la memoria en diferentes órdenes.
En general, considere un sistema de memoria compartida con exactamente dos agentes. Sea S1(A, B) el conjunto de posibles órdenes de memoria global para las secuencias A y B con reenvío de carga de almacenamiento y sea S2(A, B) el conjunto de posibles órdenes de memoria global para las secuencias A y B sin almacenamiento -reenvío de carga. Tanto S1(A, B) como S2(A, B) son subconjuntos del conjunto de todos los órdenes de memoria globales legales S3(A, B). El reenvío de carga almacenada puede hacer que S1(A, B) no sea un subconjunto de S2(A, B). Esto significa que si S2(A, B) = S3(A, B), entonces el reenvío de carga almacenada sería una optimización ilegal.
El reenvío de carga almacenada puede cambiar la probabilidad de que se produzca cada orden de memoria global porque reduce la latencia de la carga.
Se envía una carga desde la RS (estación de reserva) y pasa a través de la AGU (Unidad de generación de direcciones) hasta la entrada del búfer de carga que se asignó para la entrada ROB (búfer de reorden) correspondiente en la etapa de asignación. Cuando se asignó la entrada del búfer de carga, se coloreó con el SBID (ID del búfer de almacenamiento) más reciente en ese momento. Coloreado significa que el número de entrada (también conocido como ID) del almacén más reciente en el búfer de almacenamiento se inserta en la entrada del búfer de carga. El búfer de almacenamiento comprende SAB (Búfer de direcciones de almacenamiento) y SDB (Búfer de datos de almacenamiento); cada tienda tiene una entrada en ambas (porque cada tienda tiene 2 uops, generalmente microfusionados) y ambas tienen el mismo índice (entrada no también conocida como SBID).
Creo que una vez que la dirección es válida, el bit válido en la entrada se establece, lo que significa que están listos para enviarse (y se borra cuando los datos finalmente se vuelven a escribir en el ROB).
También hay un predictor de desambiguación de memoria especulativa que puede estar involucrado en la configuración del bit válido para indicar que se predice que no tendrá ningún alias con ningún almacén entre el SBID con el que está coloreado y el almacén del puntero de cola en el búfer de almacenamiento ( almacenar la dirección en el SAB y los datos en el SDB). Si se predice que tendrá un alias, o en realidad lo hace (es decir, busca una dirección en el búfer de almacenamiento y utiliza la máscara de bits en el SAB para determinar si la entrada puede satisfacerla (la máscara de bits indica el nivel de privilegio del supervisor de bytes/no supervisor), y utiliza el tamaño implícito del código de operación para obtener el rango de direcciones que se almacenan mediante la operación de la tienda. Si se puede satisfacer, lee de la entrada SDB), realiza un reenvío especulativo de almacenamiento a carga. usando los datos en el SDB e inserta los datos en el buffer de carga y la carga se completa en el LB (Load Buffer), pero no se retira del LB. El reenvío de almacenamiento a carga garantiza que las lecturas nunca se puedan reordenar con escrituras más antiguas en la misma ubicación, porque la lectura siempre utilizará el reenvío de almacenamiento a carga. Creo que todas las direcciones de las tiendas antes del SBID de LFENCE deben calcularse antes de hacer una predicción en una tienda posterior a LFENCE.
Si no se predice un alias, la carga se envía (y las cargas siempre se envían en orden estricto con respecto a otras cargas, a menos que la carga tenga un impacto no temporal o sea para la memoria USWC (tipo de memoria de combinación de escritura especulativa no almacenable en caché) ( (aunque a diferencia de las tiendas no sabe si es o no USWC en este momento) La carga va al dTLB (data TLB) / L1d (caché de datos L1) en paralelo.
En cualquier momento, cuando las direcciones de almacenamiento se completan en el SAB con cualquier SBID menor o igual (teniendo en cuenta el ajuste) al SBID coloreado de la carga en cuestión, puede invalidar la predicción de desambiguación de memoria realizada y la canalización se vacía. porque la canalización ahora está usando datos obsoletos almacenados antes de la tienda con la que debería haber realizado el reenvío de tienda a carga, o está usando datos falsos de reenvío de tienda a carga de una tienda con la que en realidad no tenía ninguna dependencia. .
Cuando los datos se cargan en el registro de destino físico designado, los datos se vuelven válidos en el ROB. Cuando los datos en el ROB son válidos y un puntero de retiro apunta a la entrada, la carga deja de ser especulativa y adquiere un bit senior. Luego, la carga puede retirarse (eliminarse) del LB si se establece un bit que indica que se han calculado las direcciones de todas las tiendas entre el puntero de cola SAB y el SBID coloreado. A menos que sea una instrucción de carga senior, en cuyo caso, ahora puede ejecutarse ahora que es senior y se ha retirado del ROB.
LFENCE se envía al búfer de carga y solo se ejecuta (se envía a la caché L1d) cuando todos los uops anteriores se han retirado del ROB y cuando todas las instrucciones de carga anteriores se han retirado del ROB+LB (de acuerdo con las propiedades de serialización del flujo de instrucciones, es que se afirma tener, probablemente se retire en un ciclo por sí solo en lugar de con 1 o 2 instrucciones más antes en el ROB en el mismo ciclo). Las instrucciones de carga se retiran cuando el ROB les dice que pueden retirarse (ya no son especulativas) y los datos obtenidos son válidos y la carga ya no es especulativa en memoria. LFENCE se envía cuando está al final del búfer de carga y ROB (no puede retirarse hasta que todos los búferes de lectura sean globalmente visibles. Creo que esto significa que se asegura de que cualquier instrucción de carga superior (instrucciones que se ejecutan después del retiro del ROB y cuando se marcan como senior) como los que PREFETCH
tienen buffers de lectura asignados. Las cargas regulares asignan buffers de lectura y leen sus datos y se vuelven válidos en el buffer de carga antes de que puedan ser retirados. Globalmente visible en este caso significa todos los LFB de lectura anteriores (búferes de relleno de línea). ) han recibido notificaciones globalmente visibles desde el anillo para la línea ( que podrían venir antes de la respuesta de lectura que contiene los datos, o podrían empaquetarse en la respuesta de lectura , lo que puede significar que tiene que esperar a que se completen todas las lecturas en lugar de ser reconocido) (por supuesto, las instrucciones que se han retirado del MOB (Memory Order Buffer) ya son globalmente visibles a medida que sus datos han regresado, pero es posible que las instrucciones de carga superiores aún no hayan asignado buffers de lectura o no se les haya reconocido que son globalmente visibles) (esto es similar a la definición de tiendas globalmente visibles, donde en respuesta a una RFO (Read For Ownership), la observación global para el LFB probablemente viene en la notificación de que el núcleo tiene permiso (acceso exclusivo) de la línea y otros núcleos han sido invalidado, que vendrá antes de que los datos reales en la línea a escribir se devuelvan al núcleo, asumiendo que esto siempre se volverá a escribir antes de responder a un fisgón donde pierde el permiso en la línea). Cuando se envía LFENCE, la caché L1d lo trata como un nop y se completa, se retira en el ROB, se vuelve senior, es decir, se elimina del LB y los uops anteriores en el búfer de carga a los que se les impidió enviarse a la caché L1d ahora pueden enviarse. ser enviado.
La visibilidad global de las cargas afecta el estado de coherencia de la caché de otros núcleos, por lo que creo que es por eso que LFENCE
las cargas deben ser visibles globalmente. Una pérdida de carga en el núcleo va al LLC (caché de último nivel), que tiene un filtro de vigilancia que muestra que solo otro núcleo posee la línea. Si 1>= núcleos poseen la línea, entonces es necesario degradar ese núcleo a un estado S y hacer que vuelva a escribir los datos modificados. Los datos escritos en LLC pueden luego devolverse al núcleo solicitante con un estado S y una notificación visible globalmente. Si una pérdida de carga en el núcleo no llega a la LLC, la LLC podría enviar inmediatamente una notificación visible globalmente mientras envía la solicitud al agente local para recuperarla de la memoria (o si es un sistema multisocket, la LLC tiene que esperar el reconocimiento). del agente local que no necesita espiar otros núcleos antes de poder enviar la notificación observable globalmente al núcleo).
Creo que una carga senior es una carga que ya no es especulativa y está esperando que los datos sean devueltos y sean válidos, o ya es válido, por lo que se retira instantáneamente, mientras que una instrucción de carga senior es una instrucción que se envía después de haber sido retirada. del ROB.