Programación de MCU: la optimización de C ++ O2 se rompe mientras se repite el ciclo

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.ingrese la descripción de la imagen aquí

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_handleruna función lambda declarada anteriormente, que simplemente cambia choicea 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.

choicese 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 O0bandera en lugar de O1o 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).

¿Lo has compilado con optimizaciones en tu escritorio?
Tampoco esperaría que esto funcione en un sistema de escritorio. Los compiladores de C pueden leer una variable una vez y luego suponer que no cambia, a menos que se declare volátil. Cada compilador que he usado en los últimos 20 años realiza esta optimización.
Otros ya han señalado la razón por la que esto no funciona con -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 hacer while (choice == 0) {}?
Código postal, no imágenes de código.
¿Cómo se choicedeclara?
@psmears Tenía eso antes de cambiarme a esto porque no funcionó.
@Wilson La imagen que proporcioné solo tenía como objetivo probar mi punto con lo que vi durante la depuración.
¿Qué versión de C++ estás usando? C ++ 11 introdujo algunas suposiciones nuevas sobre bucles como while(true) { /*...*/ }, que podrían causar dolores de cabeza adicionales... (Consulte esta parte de una charla para ver un ejemplo relevante).
@hoffmale C ++ 11 de hecho.
@Jules Tienes razón. Nunca probé eso. Y ahora sé que tampoco funciona en el escritorio.
No relacionado con la pregunta específica, pero "ir a considerado dañino" (googlearlo). Si espera que alguien más mire este código, será mejor que tenga una buena razón para esos gotos, de lo contrario, debería volver a escribir ese código. Para múltiples condiciones en la misma variable, una declaración de cambio es más elegante (y más rápida si el compilador no se da cuenta de que puede convertir la declaración if en una declaración de cambio en la optimización). Además, su bucle while podría escribirse simplemente "while (elección! = 0);".
@Graham La razón por la que usé gotoaquí es porque la versión anterior no funcionó. Estaba agotando todas las formas posibles de escribir el código que tiene el mismo efecto.
(1) Si bien el código en sí puede ser útil, es más útil publicar el código en sí para que las personas puedan probarlo. Consulte meta.stackoverflow.com/q/303812 . (2) Publique un ejemplo verificable mínimo completo , que incluya la declaración de 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.
Habría sido mucho mejor preguntar esto en el desbordamiento de la pila, donde hay muchas más personas dedicadas a C++.
Su pregunta discrimina a los ciegos y los duros de la vista. Publique el código real en formato de texto .
Tenga en cuenta que los depuradores y optimizadores no siempre se mezclan. Si quiere saber qué está pasando en el código, realmente debería mirar el desensamblado. Un buen IDE le permitirá recorrer el ensamblaje y ver, por ejemplo, qué registros se comparan y qué direcciones se cargan. Si el optimizador produjo un ciclo infinito, encontrará una instrucción de bifurcación que apunta a su propia dirección.

Respuestas (2)

( 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 choiceentre 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 volatilevariables no cuentan como efectos secundarios). Cosas como printfson 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 printfno pudo modificarlo, a menos que algo en esta unidad de compilación pasara &choicea una función no en línea. Es decir , el análisis de escape podría permitirle al compilador demostrar que static int choiceno 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ó.


Compatible oficialmente con formas 100 % portátiles/legales de C++11 para hacer esto:

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_tse 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 volatiletipos 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_tes 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.


Con std::atomic<T>para sin bloqueoT

(con std::memory_order_relaxedpara 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_relaxedLas 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_tancho 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, volatilepuede hacer lo mismo que std::atomic. En algunas plataformas, volatilepodrí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 volatileque 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 volatileversió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 choicememoria alguna vez, podría considerar volatileevitar 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 volatileo atomices 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 atomicel acceso mientras el controlador de interrupciones está registrado, luego el acceso normal.

C ++ 20 proporciona esto constd::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_loadtrabajar 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 choicesolo un registro y volver a leer el atómico o volátil.


Nota al pie 1:volatile

Incluso las carreras de datos volatileson 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_relaxedsi 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::atomiclugar (con memory_order relajado).

El código generado por el compilador que se carga o almacena uint8_tes 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.

Re: "volátil podría implicar el uso de instrucciones especiales necesarias para acceder a los registros de E/S asignados a la memoria". - Lo hace en el Xtensa ISA de, por ejemplo, esos chips ESP-8266: los documentos dicen que el compilador debe insertar la MEMWinstrucció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 choicenunca cambiará. Y dado que nunca cambiará, no tiene sentido verificarlo en primer lugar.

La solución es declarar la variable volatilepara que el compilador se vea obligado a emitir un código que verifique su valor independientemente del nivel de optimización utilizado.

Una vez me encontré con un compilador incrustado que ignoraba la volatilidad cuando encendías el optimizador... Oh, cómo nos reíamos, tomó años encontrarlo y nos arrastramos a través de la salida del ensamblado. Todas las variables que se modifican fuera del flujo de control normal deben declararse volátiles, y sí, ¡"const volátil" es una cosa!
En C++ moderno, std::atomic<uint8_t> choiceserí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)
@PeterCordes: 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 volatilesolo 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.
@hoffmale: Estás hablando de operaciones atómicas de RMW como .fetch_add(o choice++). Sí, por supuesto, eso se compila de manera diferente volatilea x86, lock addpor 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? ).
@hoffmale: solo por diversión, observé la salida de gcc para x86 y MSP430 en Godbolt, y el asm es, de hecho, el mismo para volátil o atómico para el escritor y el spin-wait: godbolt.org/g/4MbgNN . Buen punto de que volatile puede implicar instrucciones especiales, eso es exactamente lo que no desea para la comunicación entre un controlador de interrupciones y el código normal, y una buena razón para elegir atomic. Si alguna vez desea incrementar, puede escribirlo como tmp = choice; tmp++; choice=tmp;, lo que permitiría compilar add byte [choice], 1en x86 (sin lockprefijo).
Tengo curiosidad por saber cómo el compilador ve el código. Se hizo referenciachoice 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?
@DanielCheung: el "problema" con esa referencia es que no modifica el valor dentro del ciclo (ocurre simultáneamente en un controlador de interrupción), y no hay construcciones (como volatileprimitivas de sincronización) para sugerir que choicepodría cambiarse en otro lugar .
@DanielCheung: una carrera de datos en una atomicvariable 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.
@PeterCordes Un bucle infinito sin efectos secundarios es un comportamiento indefinido. en.cppreference.com/w/cpp/language/ub Como tal, el compilador puede optimizar aún más si lo desea, y simplemente eliminarlo por completo.
@PeterCordes No, eso no es correcto. Ser no atómico no es la razón por la que el compilador puede sacar la carga del bucle. Es mucho más simple, como ha explicado UKMonkey.
@iheanyi: UKMonkey tiene razón, los compiladores pueden hacer eso, pero eso no es lo que gcc hace en la práctica. GCC hace la transformación que describí: convertirlo en cheque una vez y luego en bucle infinito. La carrera de datos UB es lo que permite esa transformación, y aún la habilitaría incluso si hubiera volatileacceso a algo más dentro del bucle. (O algo como printf, si pudiera probar que printfno 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.
@iheanyi: gcc elige no optimizar el bucle infinito como 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.
@hoffmale: Agregué una respuesta para aclarar todas las cosas sobre las que la gente me ha estado respondiendo en los comentarios.
Pensándolo un poco más... Hay algunos pases de optimización para atómicos ( x = 2; x = 3pueden estar optimizados para x = 3;, por lo que no hay almacenamiento 2porque ningún observador podría decir si ese almacenamiento sucedió o no) que no se puede hacer para volatilevalores (desde la lectura o escribir un volatilevalor puede causar un efecto secundario para algunos valores especiales, por ejemplo, un puerto IO).
@UKMonkey "Leer un objeto designado por un glvalue volátil ([basic.lval]), modificar un objeto, llamar a una función de E/S de biblioteca o llamar a una función que realiza cualquiera de esas operaciones son todos efectos secundarios, que son cambios en el estado del entorno de ejecución". Entonces, no, si tiene un bucle infinito esperando en un volátil no atómico, no se ajusta a tirarlo.
@Alice pero no es volátil.
@hoffmale: solo para su información, sí, ISO C ++ en papel permite ese tipo de optimización para atómica. En la práctica, los compiladores básicamente tratan todos los elementos atómicos prácticamente como volatile atomicsi no los optimizaran, debido a los posibles problemas que podría crear. Consulte ¿Por qué los compiladores no combinan escrituras std::atomic redundantes?
@DanMills: me reí cuando me encontré por primera vez volatile consthasta 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.
@Den-Jason Puede empeorar, el puntero de destino del búfer de ping pong DMA actualizado en un ISR, por ejemplo, tanto el puntero como los datos son volátiles, o un puntero a un registro de estado de solo lectura, volatile const * const uint32_t .. ..
@PeterCordes: si los autores del Estándar hubieran tenido la intención de que un bucle infinito invitara a un comportamiento completamente arbitrario, podrían haber especificado como restricción que todos los bucles deben tener efectos secundarios o terminar. La noción de que "puede asumir" invita a UB si se viola una suposición es contraria al uso del término "asumir" en otros campos del quehacer humano. Alguien a quien se le dice que puede suponer que un puente estará reparado para el momento en que haga cierto viaje debe ser excusado por llegar tarde si llega al puente y luego tiene que desviarse, pero no debe ser excusado...
... por ignorar los letreros de "PUENTE SALIDA" sobre la base de que no podrían ser correctos a menos que se violara la suposición. La regla "como si" realmente no puede acomodar situaciones en las que algunos, pero no todos los comportamientos que son inconsistentes con la ejecución secuencial, deben verse como aceptables, pero en mi humilde opinión, el estándar estaba destinado a permitir que los compiladores retrasen (posiblemente indefinidamente) la ejecución de un bucle a través del código al que se puede acceder de forma estática desde su interior, y cuya ejecución, una vez alcanzada, no podría verse afectada por nada dentro del bucle. Eso permitiría muchas optimizaciones...
... eso se negaría si los bucles infinitos se trataran como UB (ya que este último tratamiento haría necesario que los programadores incluyeran efectos secundarios ficticios dentro de los bucles para evitar que los compiladores generen código que se comporte de manera inaceptable).