c++ c++11 atomic volatile

c++ - ¿Debe std:: atomic ser volátil?



c++11 volatile (2)

Estoy ejecutando un hilo que se ejecuta hasta que se establece una bandera.

std::atomic<bool> stop(false); void f() { while(!stop.load(std::memory_order_{relaxed,acquire})) { do_the_job(); } }

Me pregunto si el compilador puede desenrollar un bucle como este (no quiero que suceda).

void f() { while(!stop.load(std::memory_order_{relaxed,acquire})) { do_the_job(); do_the_job(); do_the_job(); do_the_job(); ... // unroll as many as the compiler wants } }

Se dice que la volatilidad y la atomicidad son ortogonales, pero estoy un poco confundido. ¿El compilador es libre de almacenar en caché el valor de la variable atómica y desenrollar el bucle? Si el compilador puede desenrollar el bucle, entonces creo que tengo que ponerle volatile a la bandera, y quiero estar seguro.

¿Debo poner volatile ?

Lo siento por ser ambiguo. Supongo que entiendo qué es reordenar y qué significa memory_order_* , y estoy seguro de que entiendo qué es volatile .

Creo que el ciclo while() se puede transformar como un infinito if enunciados como este.

void f() { if(stop.load(std::memory_order_{relaxed,acquire})) return; do_the_job(); if(stop.load(std::memory_order_{relaxed,acquire})) return; do_the_job(); if(stop.load(std::memory_order_{relaxed,acquire})) return; do_the_job(); ... }

Dado que las órdenes de memoria dadas no impiden que las operaciones secuenciadas antes de que se muevan más allá de la carga atómica, creo que se puede reorganizar si es sin volatilidad.

void f() { if(stop.load(std::memory_order_{relaxed,acquire})) return; if(stop.load(std::memory_order_{relaxed,acquire})) return; if(stop.load(std::memory_order_{relaxed,acquire})) return; ... do_the_job(); do_the_job(); do_the_job(); ... }

Si el atómico no implica que sea volátil, entonces creo que el código se puede transformar así en el peor de los casos.

void f() { if(stop.load(std::memory_order_{relaxed,acquire})) return; while(true) { do_the_job(); } }

Nunca habrá una implementación tan insensata, pero supongo que todavía es una situación posible. Creo que la única forma de evitar esto es ponerle volatile a la variable atómica y preguntar por ello.

Hay muchas suposiciones que hice, por favor dígame si hay algo malo entre ellas.


¿El compilador es libre de almacenar en caché el valor de la variable atómica y desenrollar el bucle?

El compilador no puede almacenar en caché el valor de una variable atómica.

Sin embargo, dado que está utilizando std::memory_order_relaxed , eso significa que el compilador puede reordenar las cargas y almacenes desde / hacia esta variable atómica con respecto a otras cargas y almacenes.

También tenga en cuenta que una llamada a una función cuya definición no está disponible en esta unidad de traducción es una barrera de memoria del compilador. Eso significa que la llamada no puede ser reordenada con respecto a las cargas y almacenes circundantes y que todas las variables no locales se deben volver a cargar desde la memoria después de la llamada, como si todas estuvieran marcadas como volátiles. (Las variables locales cuya dirección no se pasó a otra parte no se volverán a cargar).

La transformación del código que le gustaría evitar no sería una transformación válida porque violaría el modelo de memoria de C ++: en el primer caso, tiene una carga de una variable atómica seguida de una llamada a do_the_job , en el segundo, tiene varias llamadas . El comportamiento observado del código transformado puede ser diferente.

Y una nota de std::memory_order :

Relación con volátil

Dentro de un subproceso de ejecución, se garantiza que los accesos (lecturas y escrituras) a todos los objetos volátiles no se reordenan entre sí, pero no se garantiza que este orden sea observado por otro subproceso, ya que el acceso volátil no establece la sincronización entre subprocesos. .

Además, los accesos volátiles no son atómicos (la lectura y escritura concurrentes son una carrera de datos) y no ordenan la memoria ( los accesos de memoria no volátiles pueden reordenarse libremente alrededor del acceso volátil ).

Este bit de acceso no volátil a la memoria se puede reordenar libremente alrededor del acceso volátil, lo que también es cierto para los atómicos relajados, ya que la carga relajada y las tiendas se pueden reordenar con respecto a otras cargas y tiendas.

En otras palabras, adornar tu atómico con volatile no cambiaría el comportamiento de tu código.

En cualquier caso, las variables atómicas de C ++ 11 no necesitan estar marcadas con una palabra clave volatile .

Aquí hay un ejemplo de cómo g ++ - 5.2 respeta las variables atómicas. Las siguientes funciones:

__attribute__((noinline)) int f(std::atomic<int>& a) { return a.load(std::memory_order_relaxed); } __attribute__((noinline)) int g(std::atomic<int>& a) { static_cast<void>(a.load(std::memory_order_relaxed)); static_cast<void>(a.load(std::memory_order_relaxed)); static_cast<void>(a.load(std::memory_order_relaxed)); return a.load(std::memory_order_relaxed); } __attribute__((noinline)) int h(std::atomic<int>& a) { while(a.load(std::memory_order_relaxed)) ; return 0; }

Compilado con g++ -o- -Wall -Wextra -S -march=native -O3 -pthread -std=gnu++11 test.cc | c++filt > test.S g++ -o- -Wall -Wextra -S -march=native -O3 -pthread -std=gnu++11 test.cc | c++filt > test.S produce el siguiente ensamblaje:

f(std::atomic<int>&): movl (%rdi), %eax ret g(std::atomic<int>&): movl (%rdi), %eax movl (%rdi), %eax movl (%rdi), %eax movl (%rdi), %eax ret h(std::atomic<int>&): .L4: movl (%rdi), %eax testl %eax, %eax jne .L4 ret


Si do_the_job() no cambia la stop , no importa si el compilador puede desenrollar el bucle o no.

std::memory_order_relaxed solo se asegura de que cada operación sea atómica, pero no evita que se reordenen los accesos. Eso significa que si otros conjuntos de subprocesos se stop en true , el bucle puede continuar ejecutándose varias veces, porque los accesos pueden ser reordenados. Así que es la misma situación que con un bucle desenrollado: do_the_job() puede ejecutarse unas cuantas veces después de que otro hilo haya establecido stop en true .

Entonces no, no uses volatile , usa std::memory_order_acquire y std::memory_order_release .