¿Por qué una variable local volátil se optimiza de manera diferente a un argumento volátil, y por qué el optimizador genera un ciclo no operativo a partir de este último?
Esto se inspiró en esta pregunta / respuesta y la discusión subsiguiente en los comentarios:¿La definición de "volátil" es volátil, o GCC tiene algunos problemas de cumplimiento estándar?. Basado en lo que otros y mi interpretación de lo que debería suceder, como se discutió en los comentarios, lo envié a GCC Bugzilla:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=71793 Otras respuestas relevantes son bienvenidas.
Además, ese hilo ha dado lugar a esta pregunta:¿El acceso a un objeto no volátil declarado a través de una referencia / puntero volátil confiere reglas volátiles a dichos accesos?
IntroducciónLo sévolatile
no es lo que la mayoría de la gente piensa que es yes Un nido de víboras definido por la implementación. Y ciertamente no quiero usar las siguientes construcciones en ningún código real. Dicho esto, estoy totalmente desconcertado por lo que está sucediendo en estos ejemplos, por lo que realmente agradecería cualquier aclaración.
Mi conjetura es que esto se debe a la interpretación altamente matizada del Estándar o (¿más probable?) Solo a los casos de esquina para el optimizador utilizado. De cualquier manera, si bien es más académico que práctico, espero que esto se considere valioso para analizar, especialmente teniendo en cuenta lo típicamente mal entendidovolatile
es. Algunos puntos de datos más, o quizás más probables, puntos en su contra, deben ser buenos.
Dado este código:
#include <cstddef>
void f(void *const p, std::size_t n)
{
unsigned char *y = static_cast<unsigned char *>(p);
volatile unsigned char const x = 42;
// N.B. Yeah, const is weird, but it doesn't change anything
while (n--) {
*y++ = x;
}
}
void g(void *const p, std::size_t n, volatile unsigned char const x)
{
unsigned char *y = static_cast<unsigned char *>(p);
while (n--) {
*y++ = x;
}
}
void h(void *const p, std::size_t n, volatile unsigned char const &x)
{
unsigned char *y = static_cast<unsigned char *>(p);
while (n--) {
*y++ = x;
}
}
int main(int, char **)
{
int y[1000];
f(&y, sizeof y);
volatile unsigned char const x{99};
g(&y, sizeof y, x);
h(&y, sizeof y, x);
}
Salidag++
degcc (Debian 4.9.2-10) 4.9.2
(Debianstable
a.k.a. Jessie) con la línea de comandog++ -std=c++14 -O3 -S test.cpp
produce el siguiente ASM paramain()
. VersiónDebian 5.4.0-6
(actualunstable
) produce un código equivalente, pero acabo de ejecutar el anterior primero, así que aquí está:
main:
.LFB3:
.cfi_startproc
# f()
movb $42, -1(%rsp)
movl $4000, %eax
.p2align 4,,10
.p2align 3
.L21:
subq $1, %rax
movzbl -1(%rsp), %edx
jne .L21
# x = 99
movb $99, -2(%rsp)
movzbl -2(%rsp), %eax
# g()
movl $4000, %eax
.p2align 4,,10
.p2align 3
.L22:
subq $1, %rax
jne .L22
# h()
movl $4000, %eax
.p2align 4,,10
.p2align 3
.L23:
subq $1, %rax
movzbl -2(%rsp), %edx
jne .L23
# return 0;
xorl %eax, %eax
ret
.cfi_endproc
AnálisisLas 3 funciones están en línea, y ambas que asignanvolatile
Las variables locales lo hacen en la pila por razones bastante obvias. Pero eso es lo único que comparten ...
f()
asegura leer dex
en cada iteración, presumiblemente debido a suvolatile
- pero solo vuelca el resultado aedx
, presumiblemente porque el destinoy
no se declaravolatile
y nunca se lee, lo que significa que los cambios pueden ser rechazados bajo elcomo si regla. OK, tiene sentido.
volatile
es realmente para registros de hardware, y claramente un valor local no puede ser uno de esos, y de lo contrario no puede modificarse en unvolatile
camino a menos que su dirección se desmaye ... que no lo es. Mira, simplemente no tiene mucho sentidovolatile
valores locales Pero C ++ nos permite declararlos e intenta hacerloalguna cosa con ellos. Y así, confundidos como siempre, tropezamos hacia adelante.g()
: Qué. Moviendo elvolatile
fuente en un parámetro de paso por valor, que todavía es solo otra variable local, GCC de alguna manera decide que no es oMenos volatile
, por lo que no necesita leerlo cada iteración ... pero aún lleva a cabo el ciclo,a pesar de que su cuerpo ahora no hace nada.
h()
: Al tomar el pasadovolatile
como paso por referencia, el mismo comportamiento efectivo quef()
se restaura, por lo que el bucle hacevolatile
lee.
f()
. Para elaborar: Imaginax
se refiere a un registro de hardware, del cual cada lectura tiene efectos secundarios. No querrás saltarte ninguno de esos.Agregando#define volatile /**/
lleva amain()
ser un no-op, como era de esperar. Entonces, cuando está presente, incluso en una variable localvolatile
hace algo ... simplemente no tengo ideaqué En el caso deg()
. ¿Qué demonios está pasando allí?
volatile
. Tampoco se ha desmayado una dirección, y no tengostatic
dirección, descartando cualquier ASM en líneaPOKE
ry - para que nunca puedan modificarse fuera de la función. El compilador puede ver que cada uno es constante, nunca necesita volver a leerse yvolatile
simplemente no es ciertoentonces (A) espermitido ser eludido bajo tales restricciones? (interinocomo si no fueron declaradosvolatile
) -y (B) ¿por qué solo uno se deshace? Son algunosvolatile
variables locales másvolatile
¿que otros?Dejando a un lado esa inconsistencia por un momento: después de que la lectura se optimizó, ¿por qué el compilador todavía genera el bucle?No hace nada! ¿Por qué el optimizador no lo evita?como si no se codificó ningún bucle?¿Es este un caso de esquina extraño debido al orden de optimización de análisis o tal? Como el código es un tonto experimento mental, no castigaría a GCC por esto, pero sería bueno saberlo con certeza. (O esg()
¿el ciclo de sincronización manual que la gente ha soñado todos estos años?
Y, por supuesto, la pregunta más importante desde una perspectiva práctica, aunque no quiero que eso eclipse el potencial de la compilación geekery ... ¿Cuáles, si alguno de estos, están bien definidos / correctos según el Estándar?