¿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?

Antecedentes

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ón

Lo 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.

Entrada

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);
}
Salida

g++ 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álisis

Las 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.

Bueno, quiero decir ...un poco. Me gusta, no realmente, porquevolatile 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.

Este caso por sí solo tiene sentido práctico para mí, por las razones descritas anteriormente en contraf(). 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í?

Preguntas¿Por qué un valor local declarado en el cuerpo produce resultados diferentes de un parámetro de valor, con el primero permitiendo que las lecturas se optimicen? Ambos son declaradosvolatile. Tampoco se ha desmayado una dirección, y no tengostatic dirección, descartando cualquier ASM en líneaPOKEry - 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?

Respuestas a la pregunta(1)

Su respuesta a la pregunta