¿Es volátil caro?

Después de leer The JSR-133 Cookbook for Compiler Writers sobre la implementación de volátiles, especialmente la sección “Interacciones con las instrucciones atómicas”, supongo que al leer una variable volátil sin actualizarla se necesita una barrera LoadLoad o LoadStore. Más abajo, en la página, veo que LoadLoad y LoadStore no funcionan 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 una lectura de variable normal tan rápida (sin tener en cuenta las restricciones de reordenamiento de la volatilidad)?

Creo que no entiendo esto correctamente. ¿Alguien podría importarme iluminarme?

EDITAR: Me pregunto si hay diferencias en los entornos de multiprocesador. En sistemas de CPU individuales, la CPU podría ver sus propios cachés de hilos, como afirma John V., pero en los sistemas de CPU múltiples debe haber alguna opción de configuración para las CPU que esto no sea suficiente y se tenga que golpear la memoria principal, volviendo más volátil en sistemas multi cpu, ¿verdad?

PD: En mi camino para aprender más sobre esto, tropecé con los siguientes excelentes artículos, y dado que esta pregunta puede ser interesante para otros, compartiré mis enlaces aquí:

  • Teoría y práctica de Java: reparación del modelo de memoria de Java, parte 1 y
  • Teoría y práctica de Java: reparación del modelo de memoria de Java, parte 2

En Intel, una lectura volátil sin competencia es bastante barata. Si consideramos el siguiente caso simple:

public static long l; public static void run() { if (l == -1) System.exit(-1); if (l == -2) System.exit(-1); } 

Usando la capacidad de Java 7 para imprimir código de ensamblado, el método de ejecución se ve algo así como:

 # {method} 'run2' '()V' in 'Test2' # [sp+0x10] (sp of caller) 0xb396ce80: mov %eax,-0x3000(%esp) 0xb396ce87: push %ebp 0xb396ce88: sub $0x8,%esp ;*synchronization entry ; - Test2::run2@-1 (line 33) 0xb396ce8e: mov $0xffffffff,%ecx 0xb396ce93: mov $0xffffffff,%ebx 0xb396ce98: mov $0x6fa2b2f0,%esi ; {oop('Test2')} 0xb396ce9d: mov 0x150(%esi),%ebp 0xb396cea3: mov 0x154(%esi),%edi ;*getstatic l ; - Test2::run@0 (line 33) 0xb396cea9: cmp %ecx,%ebp 0xb396ceab: jne 0xb396ceaf 0xb396cead: cmp %ebx,%edi 0xb396ceaf: je 0xb396cece ;*getstatic l ; - Test2::run@14 (line 37) 0xb396ceb1: mov $0xfffffffe,%ecx 0xb396ceb6: mov $0xffffffff,%ebx 0xb396cebb: cmp %ecx,%ebp 0xb396cebd: jne 0xb396cec1 0xb396cebf: cmp %ebx,%edi 0xb396cec1: je 0xb396ceeb ;*return ; - Test2::run@28 (line 40) 0xb396cec3: add $0x8,%esp 0xb396cec6: pop %ebp 0xb396cec7: test %eax,0xb7732000 ; {poll_return} ;... lines removed 

Si mira las 2 referencias a getstatic, la primera implica una carga de la memoria, la segunda se salta la carga a medida que el valor se reutiliza de los registros en los que ya está cargada (long es de 64 bits y en mi portátil de 32 bits) usa 2 registros).

Si hacemos que la variable l sea volátil, el ensamblaje resultante es diferente.

 # {method} 'run2' '()V' in 'Test2' # [sp+0x10] (sp of caller) 0xb3ab9340: mov %eax,-0x3000(%esp) 0xb3ab9347: push %ebp 0xb3ab9348: sub $0x8,%esp ;*synchronization entry ; - Test2::run2@-1 (line 32) 0xb3ab934e: mov $0xffffffff,%ecx 0xb3ab9353: mov $0xffffffff,%ebx 0xb3ab9358: mov $0x150,%ebp 0xb3ab935d: movsd 0x6fb7b2f0(%ebp),%xmm0 ; {oop('Test2')} 0xb3ab9365: movd %xmm0,%eax 0xb3ab9369: psrlq $0x20,%xmm0 0xb3ab936e: movd %xmm0,%edx ;*getstatic l ; - Test2::run@0 (line 32) 0xb3ab9372: cmp %ecx,%eax 0xb3ab9374: jne 0xb3ab9378 0xb3ab9376: cmp %ebx,%edx 0xb3ab9378: je 0xb3ab93ac 0xb3ab937a: mov $0xfffffffe,%ecx 0xb3ab937f: mov $0xffffffff,%ebx 0xb3ab9384: movsd 0x6fb7b2f0(%ebp),%xmm0 ; {oop('Test2')} 0xb3ab938c: movd %xmm0,%ebp 0xb3ab9390: psrlq $0x20,%xmm0 0xb3ab9395: movd %xmm0,%edi ;*getstatic l ; - Test2::run@14 (line 36) 0xb3ab9399: cmp %ecx,%ebp 0xb3ab939b: jne 0xb3ab939f 0xb3ab939d: cmp %ebx,%edi 0xb3ab939f: je 0xb3ab93ba ;*return ;... lines removed 

En este caso, las dos referencias estéticas a la variable l implican una carga de la memoria, es decir, el valor no puede mantenerse en un registro en múltiples lecturas volátiles. Para garantizar que haya una lectura atómica, el valor se lee de la memoria principal en un registro MMX movsd 0x6fb7b2f0(%ebp),%xmm0 haciendo 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 32 bits lee en un sistema de 32 bits).

Por lo tanto, el costo total de una lectura volátil será aproximadamente equivalente al de una carga de memoria y puede ser tan barato como el acceso a caché L1. Sin embargo, si otro núcleo está escribiendo en la variable volátil, la línea de caché se invalidará requiriendo una memoria principal o quizás un acceso a caché L3. El costo real dependerá en gran medida de la architecture de la CPU. Incluso entre Intel y AMD, los protocolos de coherencia de caché son diferentes.

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 los sistemas que son caché coherente.

Para responder a la pregunta del OP, las escrituras volátiles son caras, mientras que las lecturas generalmente no lo son.

¿Significa esto que las operaciones de lectura volátil se pueden realizar sin una invalidación de caché explícita en x86, y es una lectura de variable rápida y normal (sin tener en cuenta los inconvenientes de reordenamiento de la volatilidad)?

Sí, a veces, al validar un campo, es posible que la CPU no llegue siquiera a la memoria principal, sino que espia a otras cachés de hilos y obtiene el valor de allí (explicación muy general).

Sin embargo, respaldo la sugerencia de Neil de que si tienes un campo al que acceden varios hilos que ves envuélvelo como una AtomicReference. Al ser una AtomicReference, ejecuta aproximadamente el mismo rendimiento para las lecturas / escrituras, pero también es más obvio que se accederá al campo y se modificará mediante varios subprocesos.

Editar para responder a la edición de OP:

La coherencia de la caché es un protocolo algo complicado, pero en resumen: las CPU compartirán una línea de caché común que está conectada a la memoria principal. Si una CPU carga memoria y ninguna otra CPU la tuvo, esa CPU asumirá que es el valor más actualizado. Si otra CPU intenta cargar la misma ubicación de memoria, la CPU ya cargada tendrá conocimiento de esto y realmente compartirá la referencia almacenada en la memoria caché a la CPU solicitante, ahora la CPU de solicitud tiene una copia de esa memoria en su caché de la CPU. (Nunca tuvo que buscar en la memoria principal para la referencia)

Hay un poco más de protocolo involucrado, pero esto da una idea de lo que está sucediendo. También para responder a su otra pregunta, con la ausencia de procesadores múltiples, las lecturas / escrituras volátiles pueden de hecho ser más rápidas que con múltiples procesadores. Hay algunas aplicaciones que, de hecho, se ejecutan más rápido al mismo tiempo con una única CPU que múltiples.

En las palabras del Modelo de memoria de Java (como se define para Java 5+ en JSR 133), cualquier operación – leer o escribir – en una variable volatile crea una relación de pasar antes que con respecto a cualquier otra operación en la misma variable. Esto significa que el comstackdor y el JIT se ven obligados a evitar ciertas optimizaciones, como reordenar las instrucciones dentro del hilo o realizar operaciones solo dentro del 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 hacer que una variable sea volatile menos que sepa que se accederá desde múltiples hilos fuera de synchronized bloques synchronized . Incluso entonces debes considerar si la mejor opción es volátil versus synchronized , AtomicReference y sus amigos, las clases de Lock explícitas, etc.

El acceso a una variable volátil es similar en muchos aspectos al ajuste del acceso a una variable ordinaria en un bloque sincronizado. Por ejemplo, el acceso a una variable volátil evita que la CPU vuelva a ordenar las instrucciones antes y después del acceso, y esto generalmente ralentiza la ejecución (aunque no puedo decir por cuánto).

De manera más general, en un sistema multiprocesador no veo cómo se puede realizar el acceso a una variable volátil sin penalización: debe haber alguna manera de garantizar que una escritura en el procesador A se sincronice con una lectura en el procesador B.