Mientras escribía una respuesta sobre cómo los compiladores deben tratar volatile , creo que me he topado con un error de gcc y me gustaría que alguien lo verifique antes de informarlo.
Escribí una función simple como esta:
int foo (int a, int b, int c) { b = a + 1; c = b + 1; a = c + 1; return a; } Sin optimizaciones, esto da como resultado una gran cantidad de movimientos de datos sin sentido de un lado a otro. Con las optimizaciones, el compilador simplemente toma el registro donde se almacenó a , luego agrega 3 y devuelve ese resultado. Hablar x86 lea eax, [rdi+3] y ret . Esto es lo esperado, hasta ahora todo bien.
Para demostrar la secuenciación y el acceso volátil, cambié el ejemplo a esto:
int foo (int a, int b, int c) { b = a + 1; c = *(volatile int*)&b + 1; a = c + 1; return a; } Aquí hay un acceso lvalue del contenido de b que está calificado como volátil y, por lo que puedo decir, el compilador no tiene permitido optimizar ese acceso 1) . Desde gcc 4.1.2 (y probablemente antes) hasta gcc 10.3 obtengo un comportamiento conforme (lo mismo en clang). El código de máquina x86 se ve así incluso con -O3 :
foo: add edi, 1 mov DWORD PTR [rsp-4], edi mov eax, DWORD PTR [rsp-4] add eax, 2 retLuego intento lo mismo en gcc 11.1 y más allá, ahora obtengo:
foo: lea eax, [rdi+3] rethttps://godbolt.org/z/e5x74z3Kb
ARM gcc 11.1 hace algo similar.
¿Es esto un error del compilador?
1) Referencias: ISO/IEC 9899:2018 5.1.2.3, particularmente §2, §4 y §6.
Pasar la dirección a una función no en línea hace que GCC respete las conversiones volatile para accesos posteriores (y tal vez anteriores, no verificados) a una función arg o local. https://godbolt.org/z/cssveev7n
Dupliqué la línea c = y el asm contiene dos cargas de b gracias al volatile cast, usando GCC trunk.
void bar(void*); int foo (int a, int b, int c) { bar(&b); // b's address has now "escaped" - potentially globally visible b = a + 1; c = *(volatile int*)&b + 1; c = *(volatile int*)&b + 1; // both accesses present. a = c + 1; return a; } # GCC trunk -O3 -fverbose-asm call bar # mov DWORD PTR [rsp+12], ebx # b, tmp89 mov eax, DWORD PTR [rsp+12] # _2, MEM[(volatile int *)&b] mov eax, DWORD PTR [rsp+12] # _3, MEM[(volatile int *)&b] ... add eax, 2 ret Entonces, esto parece inocente, excepto tal vez en algunos casos de uso de microbenchmark; no va a romper atómicos enrollados a mano usando moldes como estos, como las macros READ_ONCE / WRITE_ONCE del kernel de Linux .
Todavía podría decirse que viola las reglas ISO C , si es legal alias un int simple con un volatile int . Si no, es solo el comportamiento de definición de GCC, por lo que depende de GCC. Publico esto más como un punto de datos que como un argumento en cualquier dirección sobre ese aspecto de la pregunta.
Según C18 5.1.2.3/6, los accesos a objetos volátiles (estrictamente de acuerdo con las reglas de la máquina abstracta) son parte del comportamiento observable del programa, que todas las implementaciones conformes deben reproducir. El término "acceso" en este contexto incluye lecturas y escrituras.
C18 5.1.2.3/2 y /4 refuerzan que los accesos volátiles son efectos secundarios necesarios , excluidos de la regla de que las implementaciones están permitidas para evitar producir efectos secundarios innecesarios.
Lo único que veo para GCC sería un argumento de que aunque (volatile int*)&b es un lvalue con un tipo calificado como volatile , puede probar que el objeto que designa ( b ) no es en realidad un "objeto volátil", que de hecho no lo es si vas por su declaración. Y eso es consistente con el comportamiento observado de GCC 11.2 para esta versión de la función:
int foo (int a, int b, int c) { volatile int bv = a + 1; c = bv + 1; a = c + 1; return a; }, que produce el mismo ensamblado que las versiones anteriores de GCC para el código original ( godbolt ).
No está claro si esto constituye un error en el sentido de no conformidad con el estándar del lenguaje, pero ciertamente GCC está frustrando la aparente intención del programador.
Escuché a un equipo de compiladores argumentar de manera convincente (bueno, casi me quedo dormido, así que obtuve un esquema aproximado) que fuera de un objeto del tamaño de una palabra con alcance externo, volátil era una decoración sin sentido. Además, el compilador proporcionó algún tipo de comportamiento tradicional en torno a objetos atribuidos sin sentido como una conveniencia para las personas que trabajan con código heredado. Esta interpretación se basó en una reducción absurda del estándar C que es mejor que correcto, es técnicamente correcto , el estándar de oro de los alpha-geeks.