Java 8 Unsafe: xxxFence () instrucciones


En Java 8 se agregaron tres instrucciones de barrera de memoria a Unsafe class (source):

/**
 * Ensures lack of reordering of loads before the fence
 * with loads or stores after the fence.
 */
void loadFence();

/**
 * Ensures lack of reordering of stores before the fence
 * with loads or stores after the fence.
 */
void storeFence();

/**
 * Ensures lack of reordering of loads or stores before the fence
 * with loads or stores after the fence.
 */
void fullFence();

Si definimos la barrera de memoria de la siguiente manera (que considero más o menos fácil de entender):

Considere X e Y como tipos/clases de operaciones que están sujetos a reordenamiento,

X_YFence() es una instrucción de barrera de memoria que asegura que todas las operaciones de tipo X antes de la barrera completadas antes de cualquier operación de tipo Y después la barrera ha comenzado.

Ahora podemos "mapear" nombres de barreras de Unsafe a esta terminología:

  • loadFence() se convierte en load_loadstoreFence();
  • storeFence() se convierte en store_loadStoreFence();
  • fullFence() se convierte en loadstore_loadstoreFence();

Finalmente, mi pregunta es - ¿por qué no tenemos load_storeFence(), store_loadFence(), store_storeFence() y load_loadFence()?

Mi conjetura sería - no son realmente necesarios, pero no entiendo por qué en este momento. Por lo tanto, me gustaría saber las razones para no añadirlos. Adivinar acerca de que son bienvenidos también (espero que esto no hace que esta pregunta sea offtopic como opinión basada, sin embargo).

Gracias de antemano.

Author: Alexey Malev, 2014-05-12

3 answers

Resumen

Los núcleos de CPU tienen búferes especiales para ordenar la memoria para ayudarlos con la ejecución fuera de orden. Estos pueden ser (y típicamente son) separados para cargar y almacenar: lóbulos para búferes de orden de carga y sollozos para búferes de orden de tienda.

Las operaciones de esgrima elegidas para la API insegura se seleccionaron en función de la siguiente suposición: los procesadores subyacentes tendrán búferes de orden de carga separados (para reordenar cargas), búferes de orden de tienda (para reordenar almacenar).

Por lo tanto, basado en esta suposición, desde un punto de vista de software, puede solicitar una de tres cosas de la CPU:

  1. Empty the LOBs (loadFence): significa que ninguna otra instrucción comenzará a ejecutarse en este núcleo, hasta que TODAS las entradas de los LOBs hayan sido procesadas. En x86 esto es un LFENCE.
  2. Empty the SOBs (storeFence): significa que ninguna otra instrucción comenzará a ejecutarse en este núcleo, hasta que TODAS las entradas en los SOBs hayan sido procesadas. En x86 esto es un SFENCE.
  3. Vaciar ambos lóbulos y sollozos(fullFence): significa ambos de los anteriores. En x86 esto es un MFENCE.

En realidad, cada arquitectura de procesador específica proporciona diferentes garantías de ordenamiento de memoria, que pueden ser más estrictas o más flexibles que las anteriores. Por ejemplo, la arquitectura SPARC puede reordenar secuencias load-store y store-load, mientras que x86 no lo hará. Además, existen arquitecturas donde los LOBs y los sollozos no se pueden controlar individualmente (es decir, solo la valla completa es posible). No obstante, en ambos casos:

  • Cuando la arquitectura es más flexible, la API simplemente no proporciona acceso a las combinaciones de secuenciación" laxas " como una cuestión de elección

  • Cuando la arquitectura es más estricta, la API simplemente implementa la garantía de secuenciación más estricta en todos los casos (por ejemplo, todas las 3 llamadas en realidad y hasta se implementan como una valla completa)

La razón de la API en particular choices se explica en el PEC según la respuesta que proporciona asilias, que es 100% en el lugar. Si conoces el orden de la memoria y la coherencia de la caché, la respuesta de asilias debería ser suficiente. Creo que el hecho de que coincidan con la instrucción estandarizada en la API de C++ fue un factor importante (simplifica mucho la implementación de JVM): http://en.cppreference.com/w/cpp/atomic/memory_order Con toda probabilidad, la implementación real llamará a la respectiva API de C++ en lugar de usar alguna instrucción.

A continuación tengo una explicación detallada con ejemplos basados en x86, que proporcionará todo el contexto necesario para entender estas cosas. De hecho, la sección demarcated (a continuación responde a otra pregunta: "¿Puede proporcionar ejemplos básicos de cómo funcionan las cercas de memoria para controlar la coherencia de la caché en la arquitectura x86?"

La razón de esto es que yo mismo (viniendo de un desarrollador de software y no diseñador de hardware) tenía problemas para entender lo que reordenar la memoria es, hasta que aprendí ejemplos específicos de cómo la coherencia de caché realmente funciona en x86. Esto proporciona un contexto invaluable para discutir cercas de memoria en general (también para otras arquitecturas). Al final discuto SPARC un poco usando el conocimiento obtenido de los ejemplos x86

La referencia [1] es una explicación aún más detallada y tiene una sección separada para discutir cada uno de: x86, SPARC, ARM y PowerPC, por lo que es una excelente lectura si está interesado en más detalles.


Ejemplo de arquitectura X86

X86 proporciona 3 tipos de instrucciones de esgrima: LFENCE (valla de carga), SFENCE (valla de tienda) y MFENCE (valla de carga-tienda), por lo que se asigna 100% a la API de Java.

Esto se debe a que x86 tiene búferes de orden de carga (LOBs) y búferes de orden de tienda (SOBs) separados, por lo que las instrucciones LFENCE/SFENCE se aplican al búfer respectivo, mientras que MFENCE se aplica a ambos.

Los SOBs se utilizan para almacenar un valor saliente (del procesador a cache system), mientras que el protocolo de coherencia de caché funciona para obtener permiso para escribir en la línea de caché. Los LOBs se utilizan para almacenar solicitudes de invalidación de modo que la invalidación pueda ejecutarse de forma asíncrona (reduce el estancamiento en el lado receptor con la esperanza de que el código que se ejecuta allí no necesite realmente ese valor).

Tiendas fuera de servicio y SFENCE

Supongamos que tiene un sistema de procesador dual con sus dos CPU, 0 y 1, ejecutando las rutinas a continuación. Considere el caso donde la línea de caché que sostiene failure es inicialmente propiedad de CPU 1, mientras que la línea de caché que sostiene shutdown es inicialmente propiedad de CPU 0.

// CPU 0:
void shutDownWithFailure(void)
{
  failure = 1; // must use SOB as this is owned by CPU 1
  shutdown = 1; // can execute immediately as it is owned be CPU 0
}
// CPU1:
void workLoop(void)
{
  while (shutdown == 0) { ... }
  if (failure) { ...}
}

En ausencia de una valla de almacenamiento, la CPU 0 puede indicar un apagado debido a un fallo, pero la CPU 1 saldrá del bucle y NO entrará en el bloque if de manejo de fallos.

Esto se debe a que CPU0 escribirá el valor 1 para failure en un búfer de orden de tienda, también enviando un mensaje de coherencia de caché para adquirir acceso exclusivo a la línea de caché. Será entonces proceda a la siguiente instrucción (mientras espera el acceso exclusivo) y actualice la bandera shutdown inmediatamente (esta línea de caché ya es propiedad exclusiva de CPU0, por lo que no es necesario negociar con otros núcleos). Finalmente, cuando más tarde reciba un mensaje de confirmación de invalidación de CPU1 (con respecto a failure) procederá a procesar el SOB para failure y escribirá el valor en la caché (pero el orden ya está invertido).

Insertar una storeFence() arreglará las cosas:

// CPU 0:
void shutDownWithFailure(void)
{
  failure = 1; // must use SOB as this is owned by CPU 1
  SFENCE // next instruction will execute after all SOBs are processed
  shutdown = 1; // can execute immediately as it is owned be CPU 0
}
// CPU1:
void workLoop(void)
{
  while (shutdown == 0) { ... }
  if (failure) { ...}
}

Un final aspecto que merece mención es que x86 tiene store-forwarding: cuando una CPU escribe un valor que se queda atascado en un SOB (debido a la coherencia de caché), puede intentar posteriormente ejecutar una instrucción de carga para la misma dirección ANTES de que el SOB sea procesado y entregado a la caché. Por lo tanto, las CPU consultarán los SOBs ANTES de acceder a la caché, por lo que el valor recuperado en este caso es el último valor escrito desde el SOB. esto significa que las tiendas de ESTE núcleo nunca se pueden reordenar con cargas posteriores de ESTE núcleo no importa qué .

Cargas fuera de orden y LFENCE

Ahora, supongamos que tiene la cerca de la tienda en su lugar y está feliz de que shutdown no puede adelantar a failure en su camino a la CPU 1, y centrarse en el otro lado. Incluso en presencia de la cerca de la tienda, hay escenarios donde sucede lo incorrecto. Considere el caso donde failure está en ambos cachés (compartidos) mientras que shutdown solo está presente y es propiedad exclusiva de la caché de CPU0. Las cosas malas pueden suceda de la siguiente manera:

  1. CPU0 escribe 1 a failure; También envía un mensaje a CPU1 para invalidar su copia de la línea de caché compartida como parte del protocolo de coherencia de caché .
  2. CPU0 ejecuta el SFENCE y stalls, esperando a que se confirme el SOB usado para failure.
  3. CPU1 comprueba shutdown debido al bucle while y (dándose cuenta de que falta el valor) envía un mensaje de coherencia de caché para leer el valor.
  4. CPU1 recibe el mensaje de CPU0 en el paso 1 a invalidar failure, enviando un acuse de recibo inmediato. NOTA: esto se implementa usando la cola de invalidación, por lo que de hecho simplemente ingresa una nota (asigna una entrada en su LOB) para luego hacer la invalidación, pero en realidad no la realiza antes de enviar el acuse de recibo.
  5. CPU0 recibe el acuse de recibo para failure y procede más allá de la SFENCE a la siguiente instrucción
  6. CPU0 escribe 1 para apagar sin usar un SOB, porque ya posee el línea de caché exclusivamente. no se envía ningún mensaje adicional para invalidación, ya que la línea de caché es exclusiva de CPU0
  7. CPU1 recibe el valor shutdown y lo confirma en su caché local, procediendo a la siguiente línea.
  8. CPU1 comprueba el valor failure para la instrucción if, pero como la cola invalidate (nota LOB) aún no está procesada, utiliza el valor 0 de su caché local (no ingresa el bloque if).
  9. CPU1 procesa la cola invalidar y actualiza failure a 1, pero es ya es demasiado tarde...

A lo que nos referimos como búferes de orden de carga, es actaully la cola de solicitudes de invalidación, y lo anterior se puede arreglar con:

// CPU 0:
void shutDownWithFailure(void)
{
  failure = 1; // must use SOB as this is owned by CPU 1
  SFENCE // next instruction will execute after all SOBs are processed
  shutdown = 1; // can execute immediately as it is owned be CPU 0
}
// CPU1:
void workLoop(void)
{
  while (shutdown == 0) { ... }
  LFENCE // next instruction will execute after all LOBs are processed
  if (failure) { ...}
}

Su pregunta sobre x86

Ahora que sabes lo que hacen los sollozos/LÓBULOS, piensa en las combinaciones que mencionaste: {[26]]}

loadFence() becomes load_loadstoreFence();

No, una valla de carga espera a que se procesen LOBs, esencialmente vaciando la cola de invalidación. Esto significa que todas las cargas posteriores verán datos actualizados (sin reordenar), como se obtendrán desde el subsistema de caché (que es coherente). Las tiendas NO pueden reordenarse con cargas posteriores, porque no pasan por el LOB. (y además store forwarding se encarga de las líneas cachce modificadas localmente) Desde la perspectiva de ESTE núcleo en particular (el que ejecuta la valla de carga), un almacén que sigue la valla de carga se ejecutará DESPUÉS de que todos los registros tengan los datos cargados. No hay forma de evitarlo.

load_storeFence() becomes ???

No hay necesidad de una load_storeFence ya que no tiene sentido. Para almacenar algo debes calcularlo usando input. Para obtener la entrada debe ejecutar cargas. Los almacenes se producirán utilizando los datos obtenidos de cargas. Si desea asegurarse de ver valores actualizados de todos los DEMÁS procesadores al cargar, utilice loadFence. Para las cargas después de la cerca de la tienda-reenvío se encarga de pedidos consistentes.

Todos los demás casos son similares.


SPARC

SPARC es aún más flexible y puede reordenar tiendas con cargas posteriores (y cargas con almacenes posteriores). No estaba tan familiarizado con SPARC, por lo que mi CONJETURA fue que no hay reenvío de tiendas (los sollozos no se consultan cuando se recarga una dirección), por lo que las "lecturas sucias" son posibles. De hecho, me equivoqué: encontré la arquitectura SPARC en [3] y la realidad es que el reenvío de tiendas está enhebrado. De la sección 5.3.4:

Todas las cargas comprueban el buffer de la tienda (solo el mismo hilo) para ver si hay peligros de lectura después de escritura (RAW). Un RAW completo se produce cuando el dword la dirección de la carga coincide con la de un almacén en el STB y todos los bytes de la carga son válidos en el búfer del almacén. Un RAW parcial ocurre cuando las direcciones dword coinciden, pero todos los bytes no son válidos en el búfer de la tienda. (Ex., un ST (word store) seguido de un LDX (dword load) a la misma dirección resulta en un RAW parcial, porque el dword completo no está en la entrada del buffer del store.)

Entonces, diferentes hilos consultan diferentes buffers de pedidos de tiendas, por lo tanto, la posibilidad de lecturas sucias después almacenar.


Referencias

[1] Barreras de memoria: una vista de Hardware para Hackers de Software, Linux Technology Center, IBM Beaverton http://www.rdrop.com/users/paulmck/scalability/paper/whymb.2010.07.23a.pdf

[2] Arquitectura Intel® 64 e IA-32 Manual del desarrollador de software, Volumen 3A http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.pdf

[3] Especificación de Microarquitectura OpenSPARC T2 Core http://www.oracle.com/technetwork/systems/opensparc/t2-06-opensparct2-core-microarch-1537749.html

 51
Author: Alexandros,
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
2016-08-10 16:48:44

Una buena fuente de información es el propio PEC 171.

Justificación:

Los tres métodos proporcionan los tres tipos diferentes de cercas de memoria que algunos compiladores y procesadores necesitan para garantizar que los accesos particulares (cargas y almacenes) no se reordenen.

Aplicación (extracto):

Para las versiones en tiempo de ejecución de C++ (en prims/unsafe.cpp), implementando a través de los métodos de OrderAccess existentes:

    loadFence:  { OrderAccess::acquire(); }
    storeFence: { OrderAccess::release(); }
    fullFence:  { OrderAccess::fence(); }

En otros palabras los nuevos métodos se relacionan estrechamente con cómo se implementan las cercas de memoria en los niveles de JVM y CPU. También coinciden con las instrucciones de barrera de memoria disponibles en C++, el lenguaje en el que se implementa hotspot.

Un enfoque más detallado probablemente habría sido factible, pero los beneficios no son obvios.

Por ejemplo, si observa la tabla de instrucciones de la cpu en el libro de cocina JSR 133 , verá que LoadStore y LoadLoad se asignan a la misma las instrucciones en la mayoría de las arquitecturas, es decir, ambas son efectivamente instrucciones Load_LoadStore. Así que tener una única instrucción Load_LoadStore (loadFence) en el nivel JVM parece una decisión de diseño razonable.

 7
Author: assylias,
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
2014-05-21 11:54:42

El doc de storeFence() es incorrecto. Véase https://bugs.openjdk.java.net/browse/JDK-8038978

LoadFence() es LoadLoad plus LoadStore, tan útil a menudo llamado acquire fence.

StoreFence() es StoreStore plus LoadStore, tan útil a menudo llamado release fence.

LoadLoad LoadStore StoreStore son cercas baratas (nop en x86 o Sparc, baratas en Potencia, quizás caras en ARM).

IA64 tiene diferentes instrucciones para adquirir y liberar semántica.

FullFence() es LoadLoad LoadStore StoreStore plus StoreLoad.

StordLoad fence es caro (en casi toda la CPU), casi tan caro como full fence.

Que justifica el diseño de la API.

 4
Author: ntysdd,
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
2015-06-02 18:30:31