¿Está permitido que un compilador optimice una variable volátil local?

79

¿Está permitido el compilador optimizar esto (de acuerdo con el estándar C ++ 17):

int fn() {
    volatile int x = 0;
    return x;
}

¿a esto?

int fn() {
    return 0;
}

¿Si es así por qué? ¿Si no, porque no?


Aquí hay algunas reflexiones sobre este tema: los compiladores actuales compilan fn()como una variable local puesta en la pila y luego la devuelven. Por ejemplo, en x86-64, gcc crea esto:

mov    DWORD PTR [rsp-0x4],0x0 // this is x
mov    eax,DWORD PTR [rsp-0x4] // eax is the return register
ret    

Ahora, hasta donde yo sé, el estándar no dice que una variable volátil local deba colocarse en la pila. Entonces, esta versión sería igualmente buena:

mov    edx,0x0 // this is x
mov    eax,edx // eax is the return
ret    

Aquí, edxtiendas x. Pero ahora, ¿por qué detenerse aquí? Como edxy eaxambos son cero, podríamos decir:

xor    eax,eax // eax is the return, and x as well
ret    

Y nos transformamos fn()a la versión optimizada. ¿Es válida esta transformación? Si no es así, ¿qué paso no es válido?

geza
fuente
1
Los comentarios no son para una discusión extensa; esta conversación se ha movido al chat .
@philipxy: No se trata de "lo que podría producir". Se trata de si la transformación está permitida. Porque, si no está permitido, entonces no debe producir la versión transformada.
geza
El estándar define para un programa una secuencia de accesos a volátiles y otros observables que una implementación debe respetar. Pero el acceso a un medio volátil está definido por la implementación. Por lo tanto, no tiene sentido preguntar qué podría producir una implementación: produce lo que está definido para producir. Dada alguna descripción del comportamiento de implementación, puede buscar otra que prefiera. Pero necesitas uno para empezar. Tal vez esté realmente interesado en las reglas observables del estándar, ya que la generación de código es irrelevante además de tener que satisfacer las reglas del estándar y una implementación.
philipxy
1
@philipxy: Aclararé mi pregunta de que se trata del estándar. Generalmente está implícito en este tipo de preguntas. Me interesa lo que dice la norma.
geza

Respuestas:

63

No. El acceso a los volatileobjetos se considera un comportamiento observable, exactamente como E / S, sin distinción particular entre locales y globales.

Los requisitos mínimos para una implementación conforme son:

  • El acceso a los volatileobjetos se evalúa estrictamente de acuerdo con las reglas de la máquina abstracta.

[...]

Estos colectivamente se conocen como el comportamiento observable del programa.

N3690, [intro.execution], ¶8

La forma exacta en que esto es observable está fuera del alcance del estándar y cae directamente en un territorio específico de implementación, exactamente como E / S y acceso a volatileobjetos globales . volatilesignifica "crees que sabes todo lo que sucede aquí, pero no es así; confía en mí y haz estas cosas sin ser demasiado inteligente, porque estoy en tu programa haciendo mis cosas secretas con tus bytes". En realidad, esto se explica en [dcl.type.cv] ¶7:

[Nota: volatilees una sugerencia para la implementación para evitar una optimización agresiva que involucre al objeto porque el valor del objeto puede ser cambiado por medios indetectables por una implementación. Además, para algunas implementaciones, volatile podría indicar que se requieren instrucciones especiales de hardware para acceder al objeto. Consulte 1.9 para conocer la semántica detallada. En general, se pretende que la semántica de volatile sea la misma en C ++ que en C. - nota final]

Matteo Italia
fuente
2
Dado que esta es la pregunta más votada y la pregunta se amplió mediante la edición, sería bueno editar esta respuesta para discutir los nuevos ejemplos de optimización.
hyde
Lo correcto es "sí". Esta respuesta no distingue claramente los observables abstractos de la máquina del código generado. Este último está definido por la implementación. Por ejemplo, quizás para su uso con un depurador dado, se garantiza que un objeto volátil estará en la memoria y / o en el registro; por ejemplo, típicamente bajo una arquitectura de destino relevante, se garantizan escrituras y / o lecturas de objetos volátiles en ubicaciones de memoria especiales especificadas por pragma. La implementación define cómo se reflejan los accesos en el código; decide cómo y cuándo los objetos "pueden ser cambiados por medios indetectables por una implementación". (Vea mis comentarios sobre la pregunta.)
philipxy
12

Este bucle se puede optimizar mediante la regla como si porque no tiene un comportamiento observable:

for (unsigned i = 0; i < n; ++i) { bool looped = true; }

Éste no puede:

for (unsigned i = 0; i < n; ++i) { volatile bool looped = true; }

El segundo ciclo hace algo en cada iteración, lo que significa que el ciclo tarda O (n) tiempo. No tengo idea de cuál es la constante, pero puedo medirla y luego tengo una forma de estar ocupado en bucle durante una cantidad de tiempo (más o menos) conocida.

Puedo hacerlo porque el estándar dice que el acceso a los volátiles debe ocurrir, en orden. Si un compilador decidiera que en este caso el estándar no se aplica, creo que tendría derecho a presentar un informe de error.

Si el compilador opta por ponerlo loopeden un registro, supongo que no tengo un buen argumento contra eso. Pero aún debe establecer el valor de ese registro en 1 para cada iteración del ciclo.

rici
fuente
Entonces, ¿está diciendo que la versión final xor ax, ax(donde axse considera que está volatile x) en la pregunta es válida o inválida? OIA, ¿cuál es su respuesta a la pregunta?
hyde
@hyde: La pregunta, tal como la leí, era "¿se puede eliminar la variable" y mi respuesta es "No". Para la implementación específica de x86 que plantea la cuestión de si el volátil se puede colocar en un registro, no estoy del todo seguro. Sin embargo, incluso si se reduce a xor ax, axese código de operación no se puede eliminar, incluso si parece inútil, y tampoco se puede fusionar. En mi ejemplo de bucle, el código compilado tendría que ejecutarse xor ax, axn veces para satisfacer la regla de comportamiento observable. Con suerte, la edición responde a su pregunta.
rici
Sí, la cuestión quedó amplió un poco por la edición, pero dado que su respuesta después de la edición, pensé que esta respuesta debe cubrir la parte nueva ...
Hyde
2
@hyde: De hecho, uso volátiles de esa manera en los puntos de referencia para evitar que el compilador optimice un bucle que de otra manera no hace nada. Así que realmente espero tener razón sobre esto: =)
rici
El Estándar dice que las operaciones sobre volatileobjetos son, en sí mismas, una especie de efecto secundario. Una implementación podría definir su semántica de una manera que no requiera que generen instrucciones de CPU reales, pero un bucle que accede a un objeto calificado para volátiles tiene efectos secundarios y, por lo tanto, no es elegible para elisión.
supercat
10

Ruego disentir con la opinión de la mayoría, a pesar del pleno entendimiento de que eso volatilesignifica E / S observable.

Si tiene este código:

{
    volatile int x;
    x = 0;
}

Creo que el compilador puede optimizarlo bajo la regla como si , asumiendo que:

  1. De volatilelo contrario, la variable no se hace visible externamente a través de, por ejemplo, punteros (lo que obviamente no es un problema aquí ya que no existe tal cosa en el alcance dado)

  2. El compilador no le proporciona un mecanismo para acceder externamente a ese volatile

La razón es simplemente que no se pudo observar la diferencia de todos modos, debido al criterio n. ° 2.

Sin embargo, en su compilador, ¡el criterio # 2 puede no ser satisfecho ! El compilador puede intentar brindarle garantías adicionales sobre la observación de volatilevariables desde "afuera", como por ejemplo, analizando la pila. En tales situaciones, el comportamiento realmente es observable, por lo que no se puede optimizar.

Ahora la pregunta es, ¿el siguiente código es diferente al anterior?

{
    volatile int x = 0;
}

Creo que he observado un comportamiento diferente para esto en Visual C ++ con respecto a la optimización, pero no estoy completamente seguro de por qué. ¿Puede ser que la inicialización no cuente como "acceso"? No estoy seguro. Esto puede valer una pregunta separada si está interesado, pero por lo demás, creo que la respuesta es como expliqué anteriormente.

usuario541686
fuente
6

Teóricamente, un manejador de interrupciones podría

  • compruebe si la dirección de devolución entra dentro de la fn()función. Puede acceder a la tabla de símbolos o los números de línea de origen a través de la instrumentación o la información de depuración adjunta.
  • luego cambie el valor de x, que se almacenaría en un desplazamiento predecible del puntero de la pila.

... por lo que fn()devuelve un valor distinto de cero.

siguió a Monica a Codidact
fuente
1
O podría hacer esto más fácilmente con un depurador estableciendo un punto de interrupción en fn(). Utilizandovolatile produce código gen que es como gcc -O0para esa variable: derrame / recarga entre cada declaración de C. ( -O0aún puede combinar múltiples accesos dentro de una declaración sin romper la consistencia del depurador, pero volatileno se le permite hacer eso).
Peter Cordes
O más fácilmente, usando un depurador :) Pero, ¿qué estándar dice que la variable debe ser observable? Quiero decir, una implementación puede elegir que debe ser observable. Otro puede decir que no es observable. ¿Este último viola el estándar? Tal vez no. La norma no especifica cómo puede ser observable una variable volátil local.
geza
Incluso, ¿qué significa "observable"? ¿Debería colocarse en una pila? ¿Qué pasa si un registro se mantiene x? ¿Qué pasa si en x86-64, xor rax, raxcontiene el cero (quiero decir, el registro de valor de retorno contienex ), que por supuesto puede ser observado / modificado por un depurador fácilmente (es decir, la información de símbolo de depuración contiene que xestá almacenada rax)? ¿Esto viola el estándar?
geza
2
−1 Cualquier llamada a fn()puede estar insertada. Con MSVC 2017 y el modo de lanzamiento predeterminado, lo es. Entonces no hay "dentro de la fn()función". Independientemente, dado que la variable es el almacenamiento automático, no hay un "desplazamiento predecible".
Saludos y hth. - Alf
1
0 @berendi: Sí, tienes razón en eso, y yo estaba equivocado en eso. Lo siento, mala mañana para mí en ese sentido (equivocado dos veces). Aún así, en mi opinión, no vale la pena discutir cómo el compilador puede admitir el acceso a través de otro software, porque puede hacerlo independientemente de volatiley porque volatileno lo obliga a proporcionar ese soporte. Entonces elimino el voto negativo (estaba equivocado), pero no voto a favor, porque creo que esta línea de razonamiento no está aclarando.
Saludos y hth. - Alf
6

Solo voy a agregar una referencia detallada para la regla como si y la palabra clave volátil . (En la parte inferior de estas páginas, siga "ver también" y "Referencias" para rastrear las especificaciones originales, pero creo que cppreference.com es mucho más fácil de leer / comprender).

En particular, quiero que leas esta sección

objeto volátil: un objeto cuyo tipo es volátil calificado, o un subobjeto de un objeto volátil, o un subobjeto mutable de un objeto constante-volátil. Cada acceso (operación de lectura o escritura, llamada a función miembro, etc.) realizado a través de una expresión glvalue de tipo calificado volátil se trata como un efecto secundario visible para fines de optimización (es decir, dentro de un único hilo de ejecución, volátil los accesos no se pueden optimizar o reordenar con otro efecto secundario visible que se secuencia antes o después del acceso volátil. Esto hace que los objetos volátiles sean adecuados para la comunicación con un manejador de señales, pero no con otro hilo de ejecución, ver std :: memory_order ). Cualquier intento de hacer referencia a un objeto volátil a través de un glvalue no volátil (por ejemplo, a través de una referencia o un puntero a un tipo no volátil) da como resultado un comportamiento indefinido.

Entonces, la palabra clave volátil se trata específicamente de deshabilitar la optimización del compilador en glvalues . Lo único que aquí puede afectar la palabra clave volátil es posiblemente que return xel compilador puede hacer lo que quiera con el resto de la función.

Cuánto puede optimizar el compilador el retorno depende de cuánto se le permita al compilador optimizar el acceso de x en este caso (ya que no está reordenando nada, y estrictamente hablando, no está eliminando la expresión de retorno. Existe el acceso , pero está leyendo y escribiendo en la pila, que debería poder simplificarse). Así que mientras lo leo, esta es un área gris en cuanto a cuánto se le permite optimizar al compilador, y se puede argumentar fácilmente en ambos sentidos.

Nota al margen: En estos casos, asuma siempre que el compilador hará lo contrario de lo que quería / necesitaba. Debe deshabilitar la optimización (al menos para este módulo) o intentar encontrar un comportamiento más definido para lo que desea. (Esta es también la razón por la que las pruebas unitarias son tan importantes) Si cree que es un defecto, debe comentarlo con los desarrolladores de C ++.


Todo esto sigue siendo muy difícil de leer, así que trato de incluir lo que creo que es relevante para que pueda leerlo usted mismo.

glvalue Una expresión glvalue es lvalue o xvalue.

Propiedades:

Un glvalue se puede convertir implícitamente en un prvalue con conversión implícita de lvalue a rvalue, matriz a puntero o función a puntero. Un glvalue puede ser polimórfico: el tipo dinámico del objeto que identifica no es necesariamente el tipo estático de la expresión. Un glvalue puede tener un tipo incompleto, donde lo permita la expresión.


xvalue Las siguientes expresiones son expresiones xvalue:

una llamada a función o una expresión de operador sobrecargada, cuyo tipo de retorno es rvalue referencia a objeto, como std :: move (x); a [n], la expresión de subíndice incorporada, donde un operando es una matriz rvalue; am, el miembro de la expresión de objeto, donde a es un rvalue ym es un miembro de datos no estáticos de tipo no de referencia; a. * mp, el puntero al miembro de la expresión del objeto, donde a es un rvalue y mp es un puntero al miembro de datos; un ? b: c, la expresión condicional ternaria para algunos b y c (ver definición para más detalles); una expresión de conversión a rvalue referencia al tipo de objeto, como static_cast (x); cualquier expresión que designe un objeto temporal, después de la materialización temporal. (desde C ++ 17) Propiedades:

Igual que rvalue (abajo). Igual que glvalue (abajo). En particular, como todos los rvalues, los xvalues ​​se unen a las referencias de rvalue y, como todos los glvalues, los xvalues ​​pueden ser polimórficos y los xvalues ​​que no pertenecen a la clase pueden estar calificados como cv.


lvalue Las siguientes expresiones son expresiones lvalue:

el nombre de una variable, una función o un miembro de datos, independientemente del tipo, como std :: cin o std :: endl. Incluso si el tipo de la variable es una referencia rvalue, la expresión que consta de su nombre es una expresión lvalue; una llamada a función o una expresión de operador sobrecargada, cuyo tipo de retorno es referencia de valor l, como std :: getline (std :: cin, str), std :: cout << 1, str1 = str2 o ++ it; a = b, a + = b, a% = b, y todas las demás expresiones de asignación integradas y de asignación compuesta; ++ ay --a, las expresiones de preincremento y pre-decremento integradas; * p, la expresión de indirección incorporada; a [n] yp [n], las expresiones de subíndice integradas, excepto donde a es un rvalue de matriz (desde C ++ 11); am, el miembro de la expresión de objeto, excepto donde m es un enumerador de miembros o una función miembro no estática, o donde a es un rvalue ym es un miembro de datos no estático de tipo no de referencia; p-> m, el miembro incorporado de la expresión de puntero, excepto donde m es un enumerador de miembros o una función miembro no estática; a. * mp, el puntero al miembro de la expresión del objeto, donde a es un lvalue y mp es un puntero al miembro de datos; p -> * mp, el puntero incorporado al miembro de la expresión del puntero, donde mp es un puntero al miembro de datos; a, b, la expresión de coma incorporada, donde b es un valor l; un ? b: c, la expresión condicional ternaria para algunos byc (p. ej., cuando ambos son valores l del mismo tipo, pero consulte la definición para obtener más detalles); una cadena literal, como "¡Hola, mundo!"; una expresión de conversión al tipo de referencia lvalue, como static_cast (x); una llamada de función o una expresión de operador sobrecargada, cuyo tipo de retorno es rvalue referencia a la función; una expresión de conversión a rvalue referencia al tipo de función, como static_cast (x). (desde C ++ 11) Propiedades:

Igual que glvalue (abajo). Se puede tomar la dirección de un valor l: & ++ i 1 y & std :: endl son expresiones válidas. Se puede utilizar un lvalue modificable como operando de la izquierda de los operadores de asignación incorporados y de asignación compuesta. Se puede usar un lvalue para inicializar una referencia de lvalue; esto asocia un nuevo nombre con el objeto identificado por la expresión.


como si fuera una regla

El compilador de C ++ puede realizar cambios en el programa siempre que se cumpla lo siguiente:

1) En cada punto de secuencia, los valores de todos los objetos volátiles son estables (las evaluaciones anteriores están completas, las nuevas evaluaciones no se inician) (hasta C ++ 11) 1) Los accesos (lee y escribe) a los objetos volátiles ocurren estrictamente de acuerdo con la semántica de las expresiones en las que ocurren. En particular, no se reordenan con respecto a otros accesos volátiles en el mismo hilo. (desde C ++ 11) 2) Al finalizar el programa, los datos escritos en archivos son exactamente como si el programa se hubiera ejecutado como se escribió. 3) El texto de aviso que se envía a los dispositivos interactivos se mostrará antes de que el programa espere la entrada. 4) Si se admite ISO C pragma #pragma STDC FENV_ACCESS y se establece en ON,


Si desea leer las especificaciones, creo que estas son las que necesita leer

Referencias

Estándar C11 (ISO / IEC 9899: 2011): 6.7.3 Calificadores de tipo (p: 121-123)

Estándar C99 (ISO / IEC 9899: 1999): 6.7.3 Calificadores de tipo (p: 108-110)

Estándar C89 / C90 (ISO / IEC 9899: 1990): 3.5.3 Calificadores de tipo

Tezra
fuente
Puede que no sea correcto según el estándar, pero cualquier persona que confíe en que la pila será tocada por otra cosa durante la ejecución debe dejar de codificar. Yo diría que es un defecto estándar.
meneldal
1
@meneldal: Esa es una afirmación demasiado amplia. Usar _AddressOfReturnAddressimplica analizar la pila, por ejemplo. La gente analiza la pila por razones válidas, y no es necesariamente porque la función en sí depende de ella para su corrección.
user541686
1
glvalue está aquí:return x;
geza
@geza Lo siento, todo esto es difícil de leer. ¿Es un glvalue porque x es una variable? Además, para "no se puede optimizar", ¿eso significa que el compilador no puede optimizar en absoluto, o que no puede optimizar cambiando la expresión? (Parece que el compilador todavía puede optimizar aquí porque no hay orden de acceso que mantener, y la expresión aún se está resolviendo, solo de una manera más optimizada) Puedo ver que se argumenta en ambos sentidos sin una mayor comprensión de la especificaciones.
Tezra
Aquí hay una cita de su propia respuesta :) "Las siguientes expresiones son expresiones lvalue: el nombre de una variable ..."
geza
-1

Creo que nunca he visto una variable local usando un volátil que no sea un puntero a un volátil. Como en:

int fn() {
    volatile int *x = (volatile int *)0xDEADBEEF;
    *x = 23;   // request data, 23 = temperature 
    return *x; // return temperature
}

Los únicos otros casos de volátiles que conozco usan un global que está escrito en un manejador de señales. No hay punteros involucrados allí. O acceso a símbolos definidos en un script de enlazador para estar en direcciones específicas relevantes para el hardware.

Es mucho más fácil razonar allí por qué la optimización alteraría los efectos observables. Pero la misma regla se aplica a su variable volátil local. El compilador tiene que comportarse como si el acceso ax fuera observable y no pudiera optimizarlo.

Goswin von Brederlow
fuente
3
Pero esa no es una variable volátil local, es un puntero no volátil local a un int volátil en una dirección conocida.
Inútil
Lo que facilita razonar sobre el comportamiento correcto. Como se dijo, las reglas para acceder a un volátil son las mismas para las variables locales y los indicadores de que las variables volátiles se desreferencian.
Goswin von Brederlow
Solo estoy hablando de la primera oración de su respuesta, que parece sugerir que xen su código hay una "variable volátil local". No lo es.
Inútil
Me enojé cuando int fn (const volatile int argumento) no se compiló.
Joshua
4
La edición hace que su respuesta no sea incorrecta, pero simplemente no responde a la pregunta. Este es el caso de uso de los libros de texto volatiley no tiene nada que ver con que sea local. También podría tener static volatile int *const x = ...un alcance global y todo lo que diga seguirá siendo exactamente lo mismo. Esto es como un conocimiento previo adicional que es necesario para comprender la pregunta, que supongo que tal vez no todos tengan, pero no es una respuesta real.
Peter Cordes