Sé que la gente dice que la optimización del código solo debería sacar el error oculto en el programa, pero escúchame. Me quedo en una pantalla, hasta que se cumple alguna entrada a través de una interrupción.
Esto es lo que veo en el depurador. Observe la línea inspeccionada y el valor de expresión interceptado.
Código en la imagen:
//...
ui::Context::addEventListener(ui::EventType::JOYSTICK_DOWN, &constant_dynamic_handler);
while (true) {
if (choice != 0) //debugger pause
break;
}
ui::Context::removeEventListener(ui::EventType::JOYSTICK_DOWN, &constant_dynamic_handler);
if (choice == 1) goto constant;
else if (choice == 2) goto dynamic;
else if (choice == 3) goto reset;
else if (choice == 4) goto exit;
//...
//debugger view:
//expression: choice
//value: 1
Es constant_dynamic_handler
una función lambda declarada anteriormente, que simplemente cambia choice
a algún número entero que no sea 0
. El hecho de que pueda hacer una pausa en el ciclo significa que no se sale del ciclo, pero el valor de hecho se cambia. No puedo pasar un paso en el depurador ya que no podrá leer la memoria en la CPU y requiere un reinicio para depurar nuevamente.
choice
se declara simplemente en el mismo ámbito que el bloque de sentencias if, como int choice = 0;
. Se modifica solo dentro de un oyente de interrupción activado con una entrada de hardware.
El programa funciona con O0
bandera en lugar de O1
o O2
.
Estoy usando NXP K60 y c++11, si es necesario. ¿Es mi problema? ¿Puede haber algo de lo que no sea consciente? Soy un principiante en la programación de MCU., y este código funciona en el escritorio(Acabo de intentarlo, no funciona).
( Duplicado entre sitios en SO sobre el caso del subproceso, en lugar del caso de interrupción/controlador de señal). También relacionado: ¿Cuándo usar volátil con subprocesos múltiples?
Una carrera de datos en un 1atomic
no variable es un comportamiento indefinido en C++11 2 . es decir, lectura+escritura o escritura+escritura potencialmente concurrentes sin ninguna sincronización para proporcionar una relación que suceda antes, por ejemplo, una exclusión mutua o sincronización de liberación/adquisición.
El compilador puede asumir que ningún otro subproceso se ha modificado choice
entre dos lecturas (porque eso sería UB de carrera de datos ( Comportamiento indefinido )), por lo que puede CSE y sacar el control del ciclo.
De hecho, esto es lo que hace gcc (y la mayoría de los otros compiladores también):
while(!choice){}
optimiza en asm que se ve así:
if(!choice) // conditional branch outside the loop to skip it
while(1){} // infinite loop, like ARM .L2: b .L2
Esto sucede en la parte independiente del objetivo de gcc, por lo que se aplica a todas las arquitecturas.
Desea que el compilador pueda realizar este tipo de optimización, porque el código real contiene cosas como for (int i=0 ; i < global_size ; i++ ) { ... }
. Desea que el compilador pueda cargar el global fuera del ciclo, no volver a cargarlo cada iteración del ciclo o para cada acceso posterior en una función. Los datos deben estar en los registros para que la CPU funcione con ellos, no la memoria.
El compilador podría incluso suponer que nunca se llega al código con choice == 0
, porque un bucle infinito sin efectos secundarios es un comportamiento indefinido. (Las lecturas/escrituras de no volatile
variables no cuentan como efectos secundarios). Cosas como printf
son un efecto secundario, pero llamar a una función no en línea también evitaría que el compilador optimice las relecturas de choice
, a menos que lo fuera static int choice
. (Entonces el compilador sabría que printf
no pudo modificarlo, a menos que algo en esta unidad de compilación pasara &choice
a una función no en línea. Es decir , el análisis de escape podría permitirle al compilador demostrar que static int choice
no pudo modificarse mediante una llamada a un "desconocido" función no en línea).
En la práctica, los compiladores reales no optimizan los bucles infinitos simples, asumen (como un problema de calidad de implementación o algo así) que tenía la intención de escribir while(42){}
. Pero un ejemplo en https://en.cppreference.com/w/cpp/language/ub muestra que clang optimizará un bucle infinito si hubiera un código sin efectos secundarios que se optimizó.
Realmente no tienes varios subprocesos, tienes un controlador de interrupciones. En términos de C++11, es exactamente como un controlador de señales: puede ejecutarse de forma asíncrona con su programa principal, pero en el mismo núcleo.
C y C ++ han tenido una solución para eso durante mucho tiempo: volatile sig_atomic_t
se garantiza que está bien escribir en un controlador de señal y leer en su programa principal
Un tipo entero al que se puede acceder como una entidad atómica incluso en presencia de interrupciones asincrónicas realizadas por señales.
void reader() {
volatile sig_atomic_t shared_choice;
auto handler = a lambda that sets shared_choice;
... register lambda as interrupt handler
sig_atomic_t choice; // non-volatile local to read it into
while((choice=shared_choice) == 0){
// if your CPU has any kind of power-saving instruction like x86 pause, do it here.
// or a sleep-until-next-interrupt like x86 hlt
}
... unregister it.
switch(choice) {
case 1: goto constant;
...
case 0: // you could build the loop around this switch instead of a separate spinloop
// but it doesn't matter much
}
}
El estándar no garantiza que otros volatile
tipos sean atómicos (aunque en la práctica tienen al menos un ancho de puntero en arquitecturas normales como x86 y ARM, porque los locales se alinearán naturalmente. uint8_t
es un solo byte, y los ISA modernos pueden almacenar atómicamente un byte sin una lectura/modificación/escritura de la palabra que lo rodea, a pesar de cualquier información errónea que haya escuchado sobre las CPU orientadas a palabras ).
Lo que realmente le gustaría es una forma de hacer que un acceso específico sea volátil, en lugar de necesitar una variable separada. Es posible que pueda hacer eso con *(volatile sig_atomic_t*)&choice
, como la macro del kernel de Linux ACCESS_ONCE
, pero Linux compila con el alias estricto deshabilitado para que ese tipo de cosas sean seguras. Creo que en la práctica eso funcionaría en gcc/clang, pero creo que no es estrictamente C++ legal.
std::atomic<T>
para sin bloqueoT
(con std::memory_order_relaxed
para obtener asm eficiente sin instrucciones de barrera, como puede obtener de volatile
)
C++11 introduce un mecanismo estándar para manejar el caso en el que un subproceso lee una variable mientras otro subproceso (o controlador de señales) la escribe.
Proporciona control sobre el ordenamiento de la memoria, con consistencia secuencial por defecto, lo cual es costoso y no es necesario para su caso. std::memory_order_relaxed
Las cargas/almacenes atómicos se compilarán en el mismo asm (para su CPU K60 ARM Cortex-M4) que volatile uint8_t
, con la ventaja de permitirle usar un uint8_t
ancho en lugar de cualquier ancho sig_atomic_t
, mientras evita incluso una pizca de UB de carrera de datos C ++ 11 .
( Por supuesto, solo es portátil para plataformas donde atomic<T>
su T no tiene bloqueos; de lo contrario, el acceso asíncrono desde el programa principal y un controlador de interrupciones pueden bloquearse . Las implementaciones de C ++ no pueden inventar escrituras en los objetos circundantesuint8_t
, por lo que si tienen algo , debe ser atómico sin bloqueos. O simplemente usar unsigned char
. Pero para tipos demasiado anchos para ser atómicos naturalmente, atomic<T>
usará un bloqueo oculto. Con código regular incapaz de despertar y liberar un bloqueo mientras el único núcleo de CPU está atascado en un manejador de interrupciones, estás jodido si llega una señal/interrupción mientras se mantiene ese bloqueo).
#include <atomic>
#include <stdint.h>
volatile uint8_t v;
std::atomic<uint8_t> a;
void a_reader() {
while (a.load(std::memory_order_relaxed) == 0) {}
// std::atomic_signal_fence(std::memory_order_acquire); // optional
}
void v_reader() {
while (v == 0) {}
}
Ambos compilan en el mismo asm, con gcc7.2 -O3 para ARM, en el explorador del compilador Godbolt
a_reader():
ldr r2, .L7 @ load the address of the global
.L2: @ do {
ldrb r3, [r2] @ zero_extendqisi2
cmp r3, #0
beq .L2 @ }while(choice eq 0)
bx lr
.L7:
.word .LANCHOR0
void v_writer() {
v = 1;
}
void a_writer() {
// a = 1; // seq_cst needs a DMB, or x86 xchg or mfence
a.store(1, std::memory_order_relaxed);
}
ARM asm para ambos:
ldr r3, .L15
movs r2, #1
strb r2, [r3, #1]
bx lr
Entonces, en este caso, para esta implementación, volatile
puede hacer lo mismo que std::atomic
. En algunas plataformas, volatile
podría implicar el uso de instrucciones especiales necesarias para acceder a los registros de E/S asignados a la memoria. (No conozco ninguna plataforma como esa, y no es el caso de ARM. Pero esa es una característica volatile
que definitivamente no quieres).
Con atomic
, incluso puede bloquear el reordenamiento en tiempo de compilación con respecto a las variables no atómicas, sin costo de tiempo de ejecución adicional si tiene cuidado.
No use .load(mo_acquire)
, eso hará que asm sea seguro con respecto a otros subprocesos que se ejecutan en otros núcleos al mismo tiempo. En su lugar, use cargas/tiendas relajadas y use atomic_signal_fence
(no thread_fence) después de una carga relajada, o antes de una tienda relajada , para obtener pedidos de adquisición o liberación.
Un posible caso de uso sería un controlador de interrupciones que escribe un pequeño búfer y luego establece una bandera atómica para indicar que está listo. O un índice atómico para especificar cuál de un conjunto de búferes.
Tenga en cuenta que si el controlador de interrupciones puede ejecutarse nuevamente mientras el código principal aún está leyendo el búfer, tiene un UB de carrera de datos (y un error real en el hardware real) En C ++ puro donde no hay restricciones de tiempo ni garantías, es posible que tenga potencial teórico UB (que el compilador debe suponer que nunca sucede).
Pero solo es UB si realmente sucede en tiempo de ejecución; Si su sistema integrado tiene garantías en tiempo real, entonces puede garantizar que el lector siempre pueda terminar de verificar la bandera y leer los datos no atómicos antes de que la interrupción pueda dispararse nuevamente, incluso en el peor de los casos, donde entra otra interrupción y retrasa las cosas. Es posible que necesite algún tipo de barrera de memoria para asegurarse de que el compilador no se optimice al continuar haciendo referencia al búfer, en lugar de cualquier otro objeto en el que lea el búfer. El compilador no comprende que la evitación de UB requiere leer el búfer una vez de inmediato, a menos que se lo indique de alguna manera. (Algo como GNU C asm("":::"memory")
debería funcionar, o incluso asm(""::"m"(shared_buffer[0]):"memory")
).
Por supuesto, las operaciones de lectura/modificación/escritura como a++
se compilarán de manera diferente av++
, a un RMW atómico seguro para subprocesos , utilizando un bucle de reintento LL/SC o un x86 lock add [mem], 1
. La volatile
versión se compilará en una carga, luego en una tienda separada. Puedes expresar esto con atómicos como:
uint8_t non_atomic_inc() {
auto tmp = a.load(std::memory_order_relaxed);
uint8_t old_val = tmp;
tmp++;
a.store(tmp, std::memory_order_relaxed);
return old_val;
}
Si realmente desea aumentar la choice
memoria alguna vez, podría considerar volatile
evitar el dolor de sintaxis si eso es lo que desea en lugar de los incrementos atómicos reales. Pero recuerde que cada acceso a un volatile
o atomic
es una carga o almacenamiento adicional, por lo que realmente debe elegir cuándo leerlo en un local no atómico / no volátil.
Los compiladores actualmente no optimizan atomics , pero el estándar lo permite en casos que son seguros a menos que use volatile atomic<uint8_t> choice
.
Una vez más , lo que realmente nos gusta es atomic
el acceso mientras el controlador de interrupciones está registrado, luego el acceso normal.
std::atomic_ref<>
Pero ni gcc ni clang soportan esto en su biblioteca estándar todavía (libstdc++ o libc++). no member named 'atomic_ref' in namespace 'std'
, con gcc y clang-std=gnu++2a
. Sin embargo, no debería haber ningún problema para implementarlo; A las funciones integradas de GNU C les gusta __atomic_load
trabajar en objetos regulares, por lo que la atomicidad es por acceso en lugar de por objeto.
void reader(){
uint8_t choice;
{ // limited scope for the atomic reference
std::atomic_ref<uint8_t> atomic_choice(choice);
auto choice_setter = [&atomic_choice] (int x) { atomic_choice = x; };
ui::Context::addEventListener(ui::EventType::JOYSTICK_DOWN, &choice_setter);
while(!atomic_choice) {}
ui::Context::removeEventListener(ui::EventType::JOYSTICK_DOWN, &choice_setter);
}
switch(choice) { // then it's a normal non-atomic / non-volatile variable
}
}
Probablemente termine con una carga extra de la variable vs. while(!(choice = shared_choice)) ;
, pero si está llamando a una función entre el spinloop y cuando lo usa, probablemente sea más fácil no forzar al compilador a registrar el último resultado de lectura en otro local (que puede que tenga que derramarse). O supongo que después de cancelar el registro, podría hacer una última choice = shared_choice;
para que el compilador pueda mantener choice
solo un registro y volver a leer el atómico o volátil.
Nota al pie 1:volatile
Incluso las carreras de datos volatile
son técnicamente UB, pero en ese caso el comportamiento que obtienes en la práctica en implementaciones reales es útil, y normalmente idéntico a atomic
, memory_order_relaxed
si evitas las operaciones atómicas de lectura, modificación y escritura.
¿Cuándo usar volátil con subprocesos múltiples? explica con más detalle para el caso de varios núcleos: básicamente nunca, use en su std::atomic
lugar (con memory_order relajado).
El código generado por el compilador que se carga o almacena uint8_t
es atómico en su CPU ARM. Leer/modificar/escribir como nochoice++
sería un RMW atómico , solo una carga atómica, luego una tienda atómica posterior que podría pisar otras tiendas atómicas.volatile uint8_t choice
Nota al pie 2: C++03 :
Antes de C ++ 11, el estándar ISO C ++ no decía nada sobre los hilos, pero los compiladores más antiguos funcionaban de la misma manera; C ++ 11 básicamente hizo oficial que la forma en que los compiladores ya funcionan es correcta, aplicando la regla como si para preservar el comportamiento de un solo hilo solo a menos que use funciones especiales de lenguaje.
MEMW
instrucción ("espera de memoria") antes de leer y después de escribir en variables volátiles para asegurarse de que los datos se hayan propagado a través de cualquier/ todas las canalizaciones o cachés. IIRC, también había un error de silicio conocido en el que varias escrituras en la misma ubicación de memoria en rápida sucesión (sin MEMW) podían hacer que se omitieran las escrituras anteriores y solo propagaran las escrituras posteriores a hardware/memoria fuera del núcleo.El optimizador de código ha analizado el código y, por lo que puede ver, el valor de choice
nunca cambiará. Y dado que nunca cambiará, no tiene sentido verificarlo en primer lugar.
La solución es declarar la variable volatile
para que el compilador se vea obligado a emitir un código que verifique su valor independientemente del nivel de optimización utilizado.
std::atomic<uint8_t> choice
sería bueno para la comunicación entre un controlador de interrupciones y otro código. Use choice.store(value, std::memory_order_relaxed)
, y en este ciclo uint8_t tmp; while(0 == (tmp=choice.load(std::memory_order_relaxed)) {}
sería bueno. (Y probablemente compilar al mismo asm que volatile
)std::atomic<uint8_t>
es muy probable que el uso produzca un ensamblaje diferente en comparación con volatile
(a menos que esté usando esa extraña extensión MSVC, que IIRC solo funciona para x86, x64 y posiblemente ARM). Atomics necesita actualizar el valor atómicamente, es decir, ningún observador debería poder ver ningún estado intermedio. OTOH volatile
solo dice "este valor podría haber cambiado desde la última vez que lo leyó", lo cual es mucho menos restrictivo. Además, en algunas plataformas hay instrucciones especiales para algunos valores volátiles especiales, por ejemplo, registros mapeados en memoria..fetch_add
(o choice++
). Sí, por supuesto, eso se compila de manera diferente volatile
a x86, lock add
por ejemplo ( ¿Puede num++ ser atómico para 'int num'? ). Pero pure-load y pure-store ya son atómicos para enteros de 8 bits en todos los ISA que conozco (excepto el DEC Alpha temprano que solo tiene cargas/almacenes del tamaño de una palabra). (por ejemplo, x86, ¿por qué la asignación de enteros en una variable naturalmente alineada es atómica en x86? ).atomic
. Si alguna vez desea incrementar, puede escribirlo como tmp = choice; tmp++; choice=tmp;
, lo que permitiría compilar add byte [choice], 1
en x86 (sin lock
prefijo).choice
a la variable dentro de la lambda, lo que claramente causa un efecto secundario. ¿No le pediría eso al compilador que no optimice la variable, ya que la variable podría cambiar?
volatile
primitivas de sincronización) para sugerir que choice
podría cambiarse en otro lugar .atomic
variable no es un comportamiento indefinido en C ++ 11 (lectura + escritura simultánea), por lo que el compilador puede convertirse while(!choice){}
en if(!choice) infloop();
, es decir, sacar la carga del bucle. Una gran cantidad de código hace referencia repetidamente a la misma variable global dentro de una función, y obligar a los compiladores de C++ 11 a desoptimizarla sería muy malo.volatile
acceso a algo más dentro del bucle. (O algo como printf
, si pudiera probar que printf
no pudo cambiar el valor de choice
. ej static int choice
. con ). IMO carrera de datos UB es lo más importante de entender aquí porque explica toda una gama de optimizaciones.while(42)
o while(u++ <= UINT_MAX)
. Algunos compiladores se optimizan en función de UB infloop (¿al menos a veces? Intenté crear un ejemplo pero fallé: godbolt.org/g/KofYh6 ). Pero creo que la última vez que vi esto, el compilador (¿tal vez MSVC?) while(42){}
Preservó bucles infinitos obviamente intencionales, incluso si desaparecieron otros flujos de entrada.x = 2; x = 3
pueden estar optimizados para x = 3;
, por lo que no hay almacenamiento 2
porque ningún observador podría decir si ese almacenamiento sucedió o no) que no se puede hacer para volatile
valores (desde la lectura o escribir un volatile
valor puede causar un efecto secundario para algunos valores especiales, por ejemplo, un puerto IO).volatile atomic
si no los optimizaran, debido a los posibles problemas que podría crear. Consulte ¿Por qué los compiladores no combinan escrituras std::atomic redundantes?volatile const
hasta que me di cuenta de que su uso en realidad tenía sentidovolatile const*
: es un puntero con una dirección constante a los datos que son volátiles: un puerto de E/S.
Arsenal
Julio
frotis
-O2
. También recomendaría (como una cuestión de estilo) simplificar el ciclo: ¿por qué repetirlo para siempre y luego interrumpirlo cuando se cumple una condición, en lugar de simplemente hacerwhile (choice == 0) {}
?omarl
omarl
choice
declara?Daniel Cheung
Daniel Cheung
hoffmale
while(true) { /*...*/ }
, que podrían causar dolores de cabeza adicionales... (Consulte esta parte de una charla para ver un ejemplo relevante).Daniel Cheung
Daniel Cheung
graham
Daniel Cheung
goto
aquí es porque la versión anterior no funcionó. Estaba agotando todas las formas posibles de escribir el código que tiene el mismo efecto.usuario194316
choice
, para que no reciba comentarios como este . Es completamente posible que el error no se encuentre en el código que mostró, aunque el programa parece comenzar a comportarse mal allí, es posible que haya invocado un comportamiento indefinido hace mucho tiempo.Reino UnidoMonkey
Carreras de ligereza en órbita
Adán Haun