Después de leer The JSR-133 Cookbook for Compiler Writers sobre la implementación de volátiles, especialmente la sección "Interacciones con instrucciones atómicas", asumo que leer una variable volátil sin actualizarla necesita una barrera LoadLoad o LoadStore. Más abajo en la página, veo que LoadLoad y LoadStore son efectivamente no operativos en las CPU X86. ¿Significa esto que las operaciones de lectura volátil se pueden realizar sin una invalidación de caché explícita en x86, y es tan rápido como una lectura de variable normal (sin tener en cuenta las restricciones de reordenación de volátil)?
Creo que no entiendo esto correctamente. ¿A alguien le importaría iluminarme?
EDITAR: Me pregunto si hay diferencias en entornos multiprocesador. En los sistemas de CPU única, la CPU puede mirar sus propios cachés de subprocesos, como dice John V., pero en los sistemas de CPU múltiples debe haber alguna opción de configuración para las CPU de que esto no es suficiente y la memoria principal debe ser activada, lo que hace que la volatilidad sea más lenta en sistemas de múltiples CPU, ¿verdad?
PD: En mi camino para aprender más sobre esto, tropecé con los siguientes artículos geniales, y dado que esta pregunta puede ser interesante para otros, compartiré mis enlaces aquí:
Respuestas:
En Intel, una lectura volátil no contenida es bastante barata. Si consideramos el siguiente caso simple:
Usando la capacidad de Java 7 para imprimir código ensamblador, el método de ejecución se parece a lo siguiente:
Si observa las 2 referencias a getstatic, la primera implica una carga de la memoria, la segunda omite la carga ya que el valor se reutiliza de los registros en los que ya está cargado (el largo es de 64 bits y en mi computadora portátil de 32 bits utiliza 2 registros).
Si hacemos volátil la variable l, el ensamblado resultante es diferente.
En este caso, las dos referencias getstatic a la variable l implican una carga desde la memoria, es decir, el valor no se puede mantener en un registro a través de múltiples lecturas volátiles. Para garantizar que haya una lectura atómica, el valor se lee desde la memoria principal en un registro MMX, lo que
movsd 0x6fb7b2f0(%ebp),%xmm0
hace que la operación de lectura sea una sola instrucción (del ejemplo anterior vimos que el valor de 64 bits normalmente requeriría dos lecturas de 32 bits en un sistema de 32 bits).Por lo tanto, el costo total de una lectura volátil será aproximadamente equivalente a una carga de memoria y puede ser tan barato como un acceso a la caché L1. Sin embargo, si otro núcleo está escribiendo en la variable volátil, la línea de caché se invalidará, lo que requiere una memoria principal o quizás un acceso a la caché L3. El costo real dependerá en gran medida de la arquitectura de la CPU. Incluso entre Intel y AMD, los protocolos de coherencia de caché son diferentes.
fuente
En términos generales, en la mayoría de los procesadores modernos, una carga volátil es comparable a una carga normal. Una tienda volátil es aproximadamente 1/3 del tiempo de un montior-enter / monitor-exit. Esto se ve en sistemas que son coherentes en caché.
Para responder a la pregunta del OP, las escrituras volátiles son caras, mientras que las lecturas generalmente no lo son.
Sí, a veces, al validar un campo, es posible que la CPU ni siquiera llegue a la memoria principal, sino que espíe otros cachés de subprocesos y obtenga el valor de allí (explicación muy general).
Sin embargo, apoyo la sugerencia de Neil de que si tiene un campo al que acceden varios hilos, debe envolverlo como AtomicReference. Al ser una AtomicReference, ejecuta aproximadamente el mismo rendimiento para lecturas / escrituras, pero también es más obvio que varios subprocesos accederán y modificarán el campo.
Editar para responder a la edición de OP:
La coherencia de la caché es un protocolo un poco complicado, pero en resumen: las CPU compartirán una línea de caché común que se adjunta a la memoria principal. Si una CPU carga memoria y ninguna otra CPU la tenía, la CPU asumirá que es el valor más actualizado. Si otra CPU intenta cargar la misma ubicación de memoria, la CPU ya cargada se dará cuenta de esto y, de hecho, compartirá la referencia almacenada en caché con la CPU solicitante; ahora la CPU de solicitud tiene una copia de esa memoria en su caché de CPU. (Nunca tuvo que buscar en la memoria principal la referencia)
Hay un poco más de protocolo involucrado, pero esto da una idea de lo que está sucediendo. Además, para responder a su otra pregunta, con la ausencia de varios procesadores, las lecturas / escrituras volátiles pueden ser más rápidas que con varios procesadores. Hay algunas aplicaciones que de hecho se ejecutarían más rápido al mismo tiempo con una sola CPU y luego con varias.
fuente
En palabras del Modelo de memoria de Java (como se define para Java 5+ en JSR 133), cualquier operación, de lectura o escritura, en una
volatile
variable crea una relación de suceder antes con respecto a cualquier otra operación en la misma variable. Esto significa que el compilador y JIT se ven obligados a evitar ciertas optimizaciones, como reordenar instrucciones dentro del hilo o realizar operaciones solo dentro de la caché local.Dado que algunas optimizaciones no están disponibles, el código resultante es necesariamente más lento de lo que hubiera sido, aunque probablemente no mucho.
Sin embargo, no debe crear una variable a
volatile
menos que sepa que se accederá a ella desde múltiples subprocesos fuera de lossynchronized
bloques. Incluso entonces, debe considerar si volátil es la mejor opción frente asynchronized
,AtomicReference
y sus amigos, lasLock
clases explícitas , etc.fuente
Acceder a una variable volátil es en muchos aspectos similar a envolver el acceso a una variable ordinaria en un bloque sincronizado. Por ejemplo, el acceso a una variable volátil evita que la CPU reordene las instrucciones antes y después del acceso, y esto generalmente ralentiza la ejecución (aunque no puedo decir cuánto).
De manera más general, en un sistema multiprocesador no veo cómo se puede acceder a una variable volátil sin penalización; debe haber alguna forma de garantizar que una escritura en el procesador A se sincronice con una lectura en el procesador B.
fuente