c++ cv qualifier
Volátil en C++ 11 (1)
En el estándar C ++ 11, el modelo de la máquina cambió de una máquina de un solo hilo a una máquina de múltiples hilos.
¿Esto significa que la típica static int x; void func() { x = 0; while (x == 0) {} }
static int x; void func() { x = 0; while (x == 0) {} }
static int x; void func() { x = 0; while (x == 0) {} }
ejemplo de lectura optimizada no aparecerá más en C ++ 11?
EDITAR: para aquellos que no conocen este ejemplo (estoy realmente sorprendido), por favor lea esto: https://en.wikipedia.org/wiki/Volatile_variable
EDIT2: OK, realmente estaba esperando que todos los que sabían qué volatile
hayan visto este ejemplo.
Si usa el código en el ejemplo, la variable leída en el ciclo se optimizará, haciendo que el ciclo sea interminable.
La solución, por supuesto, es usar volatile
que forzarán al compilador a leer la variable en cada acceso.
Mi pregunta es si este es un problema obsoleto en C ++ 11, ya que el modelo de la máquina tiene varios subprocesos, por lo tanto, el compilador debe considerar el acceso simultáneo a la variable para estar presente en el sistema.
Si está optimizado depende completamente de los compiladores y de lo que elijan optimizar. El modelo de memoria C ++ 98/03 no reconoce la posibilidad de que x
pueda cambiar entre la configuración y la recuperación del valor.
El modelo de memoria C ++ 11 reconoce que x
podría ser cambiado. Sin embargo, no le importa . El acceso no atómico a las variables (es decir, que no usan std::atomic
s o mutexes propios) produce un comportamiento indefinido. Por lo tanto, está perfectamente bien para un compilador de C ++ 11 suponer que x
nunca cambia entre la escritura y la lectura, ya que el comportamiento no definido puede significar que "la función nunca ve x
cambiar alguna vez".
Ahora, veamos lo que dice C ++ 11 sobre volatile int x;
. Si pones eso ahí, y tienes algún otro problema con x
, todavía tienes un comportamiento indefinido . Volátil no afecta el comportamiento de enhebrado . El modelo de memoria de C ++ 11 no define las lecturas o escrituras de / a x
como atómicas, ni requiere las barreras de memoria necesarias para que las lecturas / escrituras no atómicas se ordenen adecuadamente. volatile
no tiene nada que ver con eso de una forma u otra.
Oh, tu código podría funcionar. Pero C ++ 11 no lo garantiza .
Lo volatile
le dice al compilador que no puede optimizar las lecturas de memoria de esa variable. Sin embargo, los núcleos de CPU tienen cachés diferentes, y la mayoría de las escrituras de memoria no salen de inmediato a la memoria principal. Se almacenan en el caché local de ese núcleo y pueden escribirse ... eventualmente .
Las CPU tienen formas de forzar las líneas de caché en la memoria y sincronizar el acceso a la memoria entre los diferentes núcleos. Estas barreras de memoria permiten que dos hilos se comuniquen efectivamente. Simplemente leer de la memoria en un núcleo que fue escrito en otro núcleo no es suficiente; el núcleo que escribió la memoria debe emitir una barrera, y el núcleo que está leyendo debe tener esa barrera completa antes de leerla para obtener realmente los datos.
volatile
garantiza nada de esto . Volatile funciona con "hardware, memoria mapeada y demás" porque el hardware que escribe esa memoria se asegura de que se resuelva el problema de caché. Si los núcleos de CPU emitieron una barrera de memoria después de cada escritura, básicamente puedes besar cualquier esperanza de despedida de rendimiento. Entonces C ++ 11 tiene un lenguaje específico que indica cuándo se requieren construcciones para emitir una barrera.
volatile
se trata de acceso a la memoria (cuándo leer); el enhebrado se trata de la integridad de la memoria (lo que en realidad se almacena allí).
El modelo de memoria C ++ 11 es específico sobre qué operaciones harán que las escrituras en un hilo sean visibles en otro. Se trata de la integridad de la memoria , que no es algo maneja la volatile
. Y la integridad de la memoria generalmente requiere que ambos hilos hagan algo.
Por ejemplo, si el hilo A bloquea un mutex, realiza una escritura y luego lo desbloquea, el modelo de memoria C ++ 11 solo requiere que la escritura sea visible para el hilo B si el hilo B lo bloquea más adelante. Hasta que realmente adquiera ese bloqueo particular , no está definido qué valor hay. Este material se presenta con gran detalle en la sección 1.10 de la norma.
Miremos el código que cita , con respecto al estándar. La Sección 1.10, p8 habla de la capacidad de ciertas llamadas de biblioteca para hacer que un hilo "se sincronice" con otro hilo. La mayoría de los otros párrafos explican cómo la sincronización (y otras cosas) crean un orden de operaciones entre hilos. Por supuesto, su código no invoca nada de esto . No hay punto de sincronización, no hay pedidos de dependencia, nada.
Sin dicha protección, sin alguna forma de sincronización u orden, 1.10 p21 viene en:
La ejecución de un programa contiene una carrera de datos si contiene dos acciones en conflicto en diferentes hilos, al menos uno de los cuales no es atómico, y ninguno sucede antes que el otro. Cualquier raza de datos de este tipo da como resultado un comportamiento indefinido.
Su programa contiene dos acciones conflictivas (leer de x
escribir a x
). Ninguno de los dos es atómico, y ninguno de los dos está ordenado por sincronización antes que el otro.
Por lo tanto, has logrado un comportamiento indefinido.
Por lo tanto, el único caso en el que el modelo de memoria C ++ 11 garantiza el comportamiento de subprocesos múltiples es si utiliza un mutex o std::atomic<int> x
adecuado con las llamadas atómicas de carga / almacenamiento apropiadas.
Ah, y no necesitas hacer x
volátil también. Cada vez que llamas a una función (no en línea), esa función o algo a lo que llama podría modificar una variable global. Por lo tanto, no puede optimizar la lectura de x
en el ciclo while. Y cada mecanismo de C ++ 11 para sincronizar requiere llamar a una función. Eso sucede para invocar una barrera de memoria.