¿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í, edx
tiendas x
. Pero ahora, ¿por qué detenerse aquí? Como edx
y eax
ambos 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?
Respuestas:
No. El acceso a los
volatile
objetos se considera un comportamiento observable, exactamente como E / S, sin distinción particular entre locales y globales.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
volatile
objetos globales .volatile
significa "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:fuente
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
looped
en 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.fuente
xor ax, ax
(dondeax
se considera que estávolatile x
) en la pregunta es válida o inválida? OIA, ¿cuál es su respuesta a la pregunta?xor ax, ax
ese 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 ejecutarsexor ax, ax
n veces para satisfacer la regla de comportamiento observable. Con suerte, la edición responde a su pregunta.volatile
objetos 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.Ruego disentir con la opinión de la mayoría, a pesar del pleno entendimiento de que eso
volatile
significa 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:
De
volatile
lo 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)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
volatile
variables 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.
fuente
Teóricamente, un manejador de interrupciones podría
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.x
, que se almacenaría en un desplazamiento predecible del puntero de la pila.... por lo que
fn()
devuelve un valor distinto de cero.fuente
fn()
. Utilizandovolatile
produce código gen que es comogcc -O0
para esa variable: derrame / recarga entre cada declaración de C. (-O0
aún puede combinar múltiples accesos dentro de una declaración sin romper la consistencia del depurador, perovolatile
no se le permite hacer eso).x
? ¿Qué pasa si en x86-64,xor rax, rax
contiene 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 quex
está almacenadarax
)? ¿Esto viola el estándar?fn()
puede estar insertada. Con MSVC 2017 y el modo de lanzamiento predeterminado, lo es. Entonces no hay "dentro de lafn()
función". Independientemente, dado que la variable es el almacenamiento automático, no hay un "desplazamiento predecible".volatile
y porquevolatile
no 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.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
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 x
el 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.
como si fuera una regla
Si desea leer las especificaciones, creo que estas son las que necesita leer
fuente
_AddressOfReturnAddress
implica 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.return x;
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.
fuente
x
en su código hay una "variable volátil local". No lo es.volatile
y no tiene nada que ver con que sea local. También podría tenerstatic 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.