¿Inicializar una variable miembro y no referenciarla / usarla consume más RAM durante el tiempo de ejecución, o el compilador simplemente ignora esa variable?
struct Foo {
int var1;
int var2;
Foo() { var1 = 5; std::cout << var1; }
};
En el ejemplo anterior, el miembro 'var1' obtiene un valor que luego se muestra en la consola. Sin embargo, 'Var2' no se utiliza en absoluto. Por lo tanto, escribirlo en la memoria durante el tiempo de ejecución sería una pérdida de recursos. ¿El compilador toma en cuenta este tipo de situaciones y simplemente ignora las variables no utilizadas, o el objeto Foo siempre tiene el mismo tamaño, independientemente de si se utilizan sus miembros?
var2
no lo hace.sizeof(Foo)
no puede disminuir por definición; si imprimesizeof(Foo)
, debe ceder8
(en plataformas comunes). Los compiladores pueden optimizar el espacio utilizado porvar2
(sin importar si es a travésnew
o en la pila o en llamadas a funciones ...) en cualquier contexto que les parezca razonable, incluso sin LTO o optimización del programa completo. Donde eso no sea posible, no lo harán, como con casi cualquier otra optimización. Creo que la edición de la respuesta aceptada hace que sea mucho menos probable que se engañe.Respuestas:
La regla dorada de C ++ "como si" 1 establece que, si el comportamiento observable de un programa no depende de la existencia de un miembro de datos no utilizado, el compilador puede optimizarlo .
No (si no se utiliza "realmente").
Ahora vienen dos preguntas en mente:
Comencemos con un ejemplo.
Ejemplo
Si le pedimos a gcc que compile esta unidad de traducción , da como resultado:
f2
es lo mismo quef1
, y nunca se utiliza ninguna memoria para mantener un archivo realFoo2::var2
. ( Clang hace algo similar ).Discusión
Algunos pueden decir que esto es diferente por dos razones:
Bueno, un buen programa es un ensamblaje inteligente y complejo de cosas simples en lugar de una simple yuxtaposición de cosas complejas. En la vida real, escribe toneladas de funciones simples usando estructuras simples que el compilador optimiza. Por ejemplo:
Este es un ejemplo genuino de un miembro de datos (aquí
std::pair<std::set<int>::iterator, bool>::first
) que no se usa. ¿Adivina qué? Está optimizado ( ejemplo más simple con un conjunto ficticio si ese ensamblaje te hace llorar).Ahora sería el momento perfecto para leer la excelente respuesta de Max Langhof (por favor, por mí). Explica por qué, al final, el concepto de estructura no tiene sentido en el nivel de ensamblaje que genera el compilador.
"Pero, si hago X, ¡el hecho de que el miembro no utilizado esté optimizado es un problema!"
Ha habido una serie de comentarios que argumentan que esta respuesta debe ser incorrecta porque alguna operación (como
assert(sizeof(Foo2) == 2*sizeof(int))
) rompería algo.Si X es parte del comportamiento observable del programa 2 , el compilador no puede optimizar las cosas. Hay muchas operaciones en un objeto que contiene un miembro de datos "no utilizado" que tendría un efecto observable en el programa. Si se realiza una operación de este tipo o si el compilador no puede probar que no se realiza ninguna, ese miembro de datos "no utilizado" es parte del comportamiento observable del programa y no se puede optimizar .
Las operaciones que afectan el comportamiento observable incluyen, pero no se limitan a:
sizeof(Foo)
),memcpy
,memcmp
),1)
2) Como una afirmación que pasa o falla.
fuente
assert(sizeof(…)…)
, en realidad, no limita al compilador; tiene que proporcionar unsizeof
código que permita el uso de cosas comomemcpy
para funcionar, pero eso no significa que el compilador de alguna manera deba usar tantos bytes a menos que puedan estar expuestos a un código talmemcpy
que pueda No reescriba para producir el valor correcto de todos modos.Es importante darse cuenta de que el código que produce el compilador no tiene conocimiento real de sus estructuras de datos (porque tal cosa no existe en el nivel de ensamblaje), y tampoco el optimizador. El compilador solo produce código para cada función , no estructuras de datos .
Ok, también escribe secciones de datos constantes y demás.
En base a eso, ya podemos decir que el optimizador no "eliminará" ni "eliminará" miembros, porque no genera estructuras de datos. Genera código , que puede o no usar a los miembros, y entre sus objetivos está el ahorro de memoria o ciclos al eliminar usos sin sentido (es decir, escrituras / lecturas) de los miembros.
La esencia de esto es que "si el compilador puede probar dentro del alcance de una función (incluidas las funciones que estaban integradas en ella) que el miembro no utilizado no hace ninguna diferencia en cómo opera la función (y lo que devuelve), entonces es muy probable que la presencia del miembro no provoca gastos generales ".
A medida que hace que las interacciones de una función con el mundo exterior sean más complicadas / poco claras para el compilador (tome / devuelva estructuras de datos más complejas, por ejemplo, un
std::vector<Foo>
, , ocultar la definición de una función en una unidad de compilación diferente, prohibir / desincentivar la inserción, etc.) , es cada vez más probable que el compilador no pueda probar que el miembro no utilizado no tiene ningún efecto.No hay reglas estrictas aquí porque todo depende de las optimizaciones que realice el compilador, pero siempre que haga cosas triviales (como se muestra en la respuesta de YSC) es muy probable que no haya sobrecarga, mientras que hace cosas complicadas (por ejemplo, regresar a
std::vector<Foo>
de una función demasiado grande para insertar) probablemente incurrirá en gastos generales.Para ilustrar el punto, considere este ejemplo :
Aquí hacemos cosas no triviales (tomar direcciones, inspeccionar y agregar bytes de la representación de bytes ) y, sin embargo, el optimizador puede darse cuenta de que el resultado es siempre el mismo en esta plataforma:
No solo los miembros de
Foo
no ocuparon ningún recuerdo, ¡Foo
ni siquiera llegaron a existir! Si hay otros usos que no se pueden optimizar, por ejemplo,sizeof(Foo)
podría ser importante, ¡pero solo para ese segmento de código! Si todos los usos pudieran optimizarse así, entonces la existencia de, por ejemplovar3
, no influye en el código generado. Pero incluso si se usa en otro lugar, ¡test()
permanecerá optimizado!En resumen: cada uso de
Foo
se optimiza de forma independiente. Algunos pueden usar más memoria debido a un miembro innecesario, otros pueden no. Consulte el manual del compilador para obtener más detalles.fuente
El compilador solo optimizará una variable miembro no utilizada (especialmente una pública) si puede probar que eliminar la variable no tiene efectos secundarios y que ninguna parte del programa depende del tamaño de
Foo
la misma.No creo que ningún compilador actual realice tales optimizaciones a menos que la estructura no se esté utilizando en absoluto. Algunos compiladores pueden al menos advertir sobre variables privadas no utilizadas, pero generalmente no para las públicas.
fuente
En general, debe asumir que obtiene lo que ha pedido, por ejemplo, las variables miembro "no utilizadas" están ahí.
Dado que en su ejemplo ambos miembros lo son
public
, el compilador no puede saber si algún código (particularmente de otras unidades de traducción = otros archivos * .cpp, que se compilan por separado y luego se vinculan) accederían al miembro "no utilizado".La respuesta de YSC da un ejemplo muy simple, donde el tipo de clase solo se usa como una variable de duración de almacenamiento automático y donde no se toma ningún puntero a esa variable. Allí, el compilador puede incorporar todo el código y luego eliminar todo el código muerto.
Si tiene interfaces entre funciones definidas en diferentes unidades de traducción, normalmente el compilador no sabe nada. Las interfaces siguen típicamente una ABI predefinida (como esa ) de modo que diferentes archivos de objetos se pueden vincular entre sí sin ningún problema. Por lo general, las ABI no marcan la diferencia si un miembro se usa o no. Entonces, en tales casos, el segundo miembro tiene que estar físicamente en la memoria (a menos que el enlazador lo elimine más tarde).
Y mientras esté dentro de los límites del idioma, no puede observar que ocurre ninguna eliminación. Si llama
sizeof(Foo)
, obtendrá2*sizeof(int)
. Si crea una matriz deFoo
s, la distancia entre los comienzos de dos objetos consecutivos deFoo
es siempresizeof(Foo)
bytes.Su tipo es un tipo de diseño estándar , lo que significa que también puede acceder a miembros en función de las compensaciones calculadas en tiempo de compilación (consulte la
offsetof
macro). Además, puede inspeccionar la representación byte a byte del objeto copiando en una matriz dechar
usingstd::memcpy
. En todos estos casos, se puede observar que el segundo miembro está allí.fuente
gcc -fwhole-program -O3 *.c
en teoría podría hacerlo, pero en la práctica probablemente no lo hará. (por ejemplo, en caso de que el programa haga algunas suposiciones sobre qué valor exactosizeof()
tiene en este objetivo, y porque es una optimización realmente complicada que los programadores deben hacer a mano si lo desean)Los ejemplos proporcionados por otras respuestas a esta pregunta que elide
var2
se basan en una única técnica de optimización: la propagación constante y la subsiguiente elisión de toda la estructura (no la elisión de solovar2
). Este es el caso simple, y los compiladores de optimización lo implementan.Para los códigos C / C ++ no administrados, la respuesta es que el compilador, en general, no elide
var2
. Hasta donde yo sé, no hay soporte para tal transformación de estructura C / C ++ en la información de depuración, y si la estructura es accesible como una variable en un depurador, entoncesvar2
no se puede elidir. Hasta donde yo sé, ningún compilador actual de C / C ++ puede especializar funciones de acuerdo con la elisión devar2
, por lo que si la estructura se pasa o se devuelve desde una función no incorporada,var2
no se puede elidir.Para lenguajes administrados como C # / Java con un compilador JIT, el compilador podría eludirlo de manera segura
var2
porque puede rastrear con precisión si se está utilizando y si se escapa al código no administrado. El tamaño físico de la estructura en lenguajes administrados puede ser diferente de su tamaño informado al programador.Los compiladores de C / C ++ del año 2019 no pueden elidir
var2
de la estructura a menos que se elimine toda la variable de estructura. Para casos interesantes de elisión devar2
de la estructura, la respuesta es: No.Algunos compiladores futuros de C / C ++ podrán eludir
var2
la estructura, y el ecosistema construido alrededor de los compiladores deberá adaptarse para procesar la información de elisión generada por los compiladores.fuente
Depende de su compilador y su nivel de optimización.
En gcc, si lo especifica
-O
, activará las siguientes marcas de optimización :-fdce
significa Eliminación de código muerto .Puede usar
__attribute__((used))
para evitar que gcc elimine una variable no utilizada con almacenamiento estático:fuente