inbae jeong
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 puede almacenar en caché el valor de la variable atómica y desenrollar el ciclo? Si el compilador puede desenrollar el ciclo, entonces creo que debo poner volatile
a la bandera, y quiero estar seguro.
debo poner volatile
?
Lo siento por ser ambiguo. Yo (supongo que yo) entiendo qué es reordenar y qué memory_order_*
s malo, y estoy seguro de que entiendo completamente lo que volatile
es.
Pienso que el while()
bucle se puede transformar como un infinito if
declaraciones como esta.
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 evitan que las operaciones secuenciadas antes se muevan más allá de la carga atómica, creo que se puede reorganizar si no es volátil.
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 volátil, entonces creo que el código puede incluso transformarse 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 loca, pero supongo que sigue siendo una situación posible. Creo que la única manera de prevenir esto es poner volatile
a la variable atómica y estoy preguntando al respecto.
Hay muchas conjeturas que hice, por favor dígame si hay algo mal entre ellas.
Maxim Egorushkin
¿El compilador puede almacenar en caché el valor de la variable atómica y desenrollar el ciclo?
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 es libre de reordenar 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 se puede reordenar con respecto a las cargas y tiendas circundantes y que todas las variables no locales deben volver a cargarse desde la memoria después de la llamada, como si todas estuvieran marcadas como volátiles. (Sin embargo, las variables locales cuya dirección no se haya pasado a otro lugar no se volverán a cargar).
El 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, tienes varias llamadas. El comportamiento observado del código transformado puede ser diferente.
Y una nota de std::memory_order:
Relación con volátiles
Dentro de un subproceso de ejecución, se garantiza que los accesos (lecturas y escrituras) a todos los objetos volátiles no se reordenarán entre sí, pero no se garantiza que este orden sea observado por otro subproceso, ya que el acceso volátil no establece sincronización entre subprocesos. .
Además, los accesos volátiles no son atómicos (la lectura y escritura simultáneas es una carrera de datos) y no ordenan la memoria (los accesos a la memoria no volátil se pueden reordenar libremente en torno al acceso volátil).
este bit los accesos a la memoria no volátil se pueden reordenar libremente en torno al acceso volátil también es cierto para los atómicos relajados, ya que la carga y las tiendas relajadas se pueden reordenar con respecto a otras cargas y tiendas.
En otras palabras, adornar su atómica con volatile
no cambiaría el comportamiento de su código.
Independientemente, las variables atómicas de C++11 no necesitan estar marcadas con volatile
palabra clave.
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
producir 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
-
“que garantiza la visibilidad de las tiendas a las variables atómicas” No hay garantía de que una tienda se vuelva visible para otras cargas dentro de un período de tiempo finito; lo mejor que tenemos es “Las implementaciones deben hacer que los depósitos atómicos sean visibles para las cargas atómicas dentro de un período de tiempo razonable”, lo cual es un estímulo normativo (“debería”), no un requisito.
– CT
08/04/2016 a las 16:14
-
@MaximEgorushkin: el compilador puede eliminar cargas consecutivas a la misma variable atómica (si no hay otras barreras de memoria en el medio), ya que no puede notar la diferencia entre una carga redundante elidida y dos cargas, eso sucede tan rápido después de otro, que ningún otro subproceso tuvo tiempo de cambiar la variable en el medio.
– Mike MB
11/04/2016 a las 10:31
-
@MaximEgorushkin: quise decir “tiene permitido”, no “puede” o “quiere”. En realidad, no creo que ningún compilador lo haga, porque casi nunca es lo que realmente quiere el programador. Además, creo que la mayoría de las implementaciones de operaciones atómicas involucran cargas o escrituras a través de punteros volátiles en alguna parte.
– Mike MB
11 de abril de 2016 a las 15:01
-
@MaximEgorushkin: Como dije: porque no puedes notar la diferencia (o al menos no sé cómo). La regla “como si” es la base de la mayoría de las optimizaciones.
– Mike MB
11 de abril de 2016 a las 15:12
-
@MikeMB tiene razón: según ISO C++ 11, compilador se les permite eliminar cargas consecutivas del mismo no-
volatile
variable atómica. Maxim: Los compiladores actuales no hacen eso, ni fusionan escrituras consecutivas, como un problema de calidad de implementación, no porque el estándar lo prohíba. (por ejemplo, las tiendas de actualización de la barra de progreso podrían salirse de un bucle…) ¿Por qué los compiladores no fusionan las escrituras std::atomic redundantes? El comité de C++ está trabajando en nuevas características para que los programadores puedan controlar cuándo el compilador tiene o no permiso para optimizar atómicos.– Peter Cordes
24 de abril de 2018 a las 3:43
Si do_the_job()
no cambia 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 impide reordenar los accesos. Eso significa que si otro hilo establece stop
a true
, el bucle puede seguir ejecutándose unas cuantas veces, porque los accesos pueden estar reordenados. Entonces es la misma situación que con un bucle desenrollado: do_the_job()
puede ejecutarse varias veces después de que se haya establecido otro subproceso stop
a true
.
Así que no, no uses volatile
usar std::memory_order_acquire
y std::memory_order_release
.
-
Tengo tu punto. Dado que no se garantiza que la operación de carga obtenga el último valor, no tiene sentido limitar el número de llamadas a la función. ¿Qué pasa con el caso que agregué?
– Inbae Jung
08/04/2016 a las 11:38
-
Me resulta difícil razonar sobre esto sin saber qué
do_the_job()
hace, y lo que el hilo que establecestop
hace. Seguramente se necesita más sincronización entre los dos si acceden a datos comunes, creo. ¿Puedes publicar un ejemplo más detallado?– alain
08/04/2016 a las 11:56
-
Cómo
std::memory_order_acquire
evitar el desenrollado?– chico curioso
1 de enero de 2020 a las 6:09
No me parece. visto mucho por
std::atomic
últimamente, pero nadie dijo que debería serlo. Supongo que dentro de la clase hayvolatile
variable en alguna parte.– Nick
8 de abril de 2016 a las 9:59
Posible duplicado de Concurrency: Atomic and volatile en el modelo de memoria C++11
-Johann Gerell
08/04/2016 a las 10:00
No, no debería ser volátil.
–Sven Nilsson
08/04/2016 a las 10:06
¿Está preguntando qué está garantizado o qué sucede en alguna plataforma en particular? Si es lo primero, ¿por qué mencionarías
volatile
, ya que no tiene una semántica multiproceso garantizada? Y si es lo último, ¿por qué no mencionas tu plataforma?–David Schwartz
08/04/2016 a las 10:14
@Mella,
std::atomic
no necesitavolatile
dentro en alguna parte, porquevolatile
no es necesario ni suficiente para una correcta sincronización entre hilos. Usandovolatile
no ayudaría en absoluto.std::atomic
utiliza operaciones atómicas, novolatile
, porque necesita ser atómico, no volátil. Son conceptos ortogonales. isvolatileutilwiththreads.com–Jonathan Wakely
08/04/2016 a las 10:24