L'initialisation d'une variable membre et ne pas la référencer / l'utiliser occupe-t-elle davantage de RAM pendant l'exécution, ou le compilateur ignore-t-il simplement cette variable?
struct Foo {
int var1;
int var2;
Foo() { var1 = 5; std::cout << var1; }
};
Dans l'exemple ci-dessus, le membre 'var1' obtient une valeur qui est ensuite affichée dans la console. 'Var2', cependant, n'est pas du tout utilisé. Par conséquent, l'écrire dans la mémoire pendant l'exécution serait un gaspillage de ressources. Le compilateur prend-il en compte ces types de situations et ignore simplement les variables inutilisées, ou l'objet Foo a-t-il toujours la même taille, que ses membres soient ou non utilisés?
var2
non.sizeof(Foo)
ne peut pas diminuer par définition - si vous imprimez,sizeof(Foo)
il doit céder8
(sur les plates-formes courantes). Les compilateurs peuvent optimiser l'espace utilisé parvar2
(que ce soit à traversnew
ou sur la pile ou dans les appels de fonction ...) dans n'importe quel contexte qu'ils trouvent raisonnable, même sans LTO ou l'optimisation de l'ensemble du programme. Là où ce n'est pas possible, ils ne le feront pas, comme pour n'importe quelle autre optimisation. Je crois que la modification de la réponse acceptée la rend beaucoup moins susceptible d'être induite en erreur.Réponses:
La règle d'or "as-if" 1 du C ++ déclare que, si le comportement observable d'un programme ne dépend pas d'une existence de membre de données inutilisé, le compilateur est autorisé à l'optimiser .
Non (s'il est "vraiment" inutilisé).
Maintenant vient deux questions à l'esprit:
Commençons par un exemple.
Exemple
Si nous demandons à gcc de compiler cette unité de traduction , il renvoie:
f2
est le même quef1
, et aucune mémoire n'est jamais utilisée pour contenir un réelFoo2::var2
. ( Clang fait quelque chose de similaire ).Discussion
Certains peuvent dire que c'est différent pour deux raisons:
Eh bien, un bon programme est un assemblage intelligent et complexe de choses simples plutôt qu'une simple juxtaposition de choses complexes. Dans la vraie vie, vous écrivez des tonnes de fonctions simples en utilisant des structures simples que le compilateur optimise. Par exemple:
Ceci est un exemple authentique d'un membre de données (ici
std::pair<std::set<int>::iterator, bool>::first
) non utilisé. Devine quoi? Il est optimisé à distance ( exemple plus simple avec un ensemble factice si cet assemblage vous fait pleurer).Ce serait le moment idéal pour lire l'excellente réponse de Max Langhof (votez-la pour moi s'il vous plaît). Cela explique pourquoi, en fin de compte, le concept de structure n'a pas de sens au niveau de l'assemblage que le compilateur produit.
"Mais, si je fais X, le fait que le membre inutilisé soit optimisé est un problème!"
Il y a eu un certain nombre de commentaires faisant valoir que cette réponse doit être fausse parce qu'une opération (comme
assert(sizeof(Foo2) == 2*sizeof(int))
) casserait quelque chose.Si X fait partie du comportement observable du programme 2 , le compilateur n'est pas autorisé à optimiser les choses. Il y a beaucoup d'opérations sur un objet contenant un membre de données "inutilisé" qui aurait un effet observable sur le programme. Si une telle opération est effectuée ou si le compilateur ne peut pas prouver qu'aucune n'est effectuée, ce membre de données "inutilisé" fait partie du comportement observable du programme et ne peut pas être optimisé .
Les opérations qui affectent le comportement observable incluent, mais ne sont pas limitées à:
sizeof(Foo)
),memcpy
,memcmp
),1)
2) Comme une affirmation réussie ou échouée.
la source
assert(sizeof(…)…)
ne contraint pas réellement le compilateur - il doit fournir unsizeof
qui permet au code d'utiliser des choses commememcpy
le travail, mais cela ne signifie pas que le compilateur est en quelque sorte obligé d'utiliser autant d'octets à moins qu'ils ne soient exposés à unmemcpy
tel qu'il puisse ne réécrivez pas pour produire la valeur correcte de toute façon.Il est important de réaliser que le code produit par le compilateur n'a aucune connaissance réelle de vos structures de données (car une telle chose n'existe pas au niveau de l'assembly), et l'optimiseur non plus. Le compilateur produit uniquement du code pour chaque fonction , pas des structures de données .
Ok, il écrit également des sections de données constantes et autres.
Sur cette base, nous pouvons déjà dire que l'optimiseur ne «supprimera» ni «éliminera» les membres, car il ne génère pas de structures de données. Il produit du code , qui peut ou non utiliser les membres, et parmi ses objectifs est d'économiser de la mémoire ou des cycles en éliminant les utilisations inutiles (c'est-à-dire les écritures / lectures) des membres.
L'essentiel est que «si le compilateur peut prouver dans le cadre d'une fonction (y compris les fonctions qui y étaient incorporées) que le membre inutilisé ne fait aucune différence quant au fonctionnement de la fonction (et à ce qu'elle renvoie), alors il y a de bonnes chances que la présence du membre n'entraîne aucune surcharge ".
Lorsque vous rendez les interactions d'une fonction avec le monde extérieur plus compliquées / peu claires pour le compilateur (prenez / retournez des structures de données plus complexes, par exemple
std::vector<Foo>
, cachez la définition d'une fonction dans une unité de compilation différente, interdisez / désinciterez l'inlining, etc.) , il devient de plus en plus probable que le compilateur ne puisse pas prouver que le membre inutilisé n'a aucun effet.Il n'y a pas de règles strictes ici car tout dépend des optimisations effectuées par le compilateur, mais tant que vous faites des choses triviales (comme le montre la réponse de YSC), il est très probable qu'aucune surcharge ne sera présente, tout en faisant des choses compliquées (par exemple, retourner a
std::vector<Foo>
d'une fonction trop grande pour l'inlining) entraînera probablement la surcharge.Pour illustrer ce point, considérons cet exemple :
Nous faisons des choses non triviales ici (prendre des adresses, inspecter et ajouter des octets à partir de la représentation d'octets ) et pourtant l'optimiseur peut comprendre que le résultat est toujours le même sur cette plate-forme:
Non seulement les membres de
Foo
n'occupaient aucune mémoire, maisFoo
ils n'ont même pas vu le jour! S'il y a d'autres utilisations qui ne peuvent pas être optimisées, par exemple, celasizeof(Foo)
peut avoir de l'importance - mais uniquement pour ce segment de code! Si toutes les utilisations pouvaient être optimisées de cette manière, l'existence de par exemplevar3
n'influencait pas le code généré. Mais même s'il est utilisé ailleurs,test()
resterait optimisé!En bref: chaque utilisation de
Foo
est optimisée indépendamment. Certains peuvent utiliser plus de mémoire en raison d'un membre inutile, d'autres non. Consultez le manuel de votre compilateur pour plus de détails.la source
Le compilateur n'optimisera une variable membre inutilisée (en particulier une variable publique) que s'il peut prouver que la suppression de la variable n'a pas d'effets secondaires et qu'aucune partie du programme ne dépend de sa taille
Foo
.Je ne pense pas qu'un compilateur actuel effectue de telles optimisations à moins que la structure ne soit vraiment utilisée du tout. Certains compilateurs peuvent au moins avertir des variables privées inutilisées, mais généralement pas des variables publiques.
la source
En général, vous devez supposer que vous obtenez ce que vous avez demandé, par exemple, les variables membres "inutilisées" sont là.
Puisque dans votre exemple les deux membres le sont
public
, le compilateur ne peut pas savoir si du code (en particulier d'autres unités de traduction = d'autres fichiers * .cpp, qui sont compilés séparément puis liés) accède au membre "inutilisé".La réponse de YSC donne un exemple très simple, où le type de classe n'est utilisé que comme variable de durée de stockage automatique et où aucun pointeur vers cette variable n'est pris. Là, le compilateur peut incorporer tout le code et peut ensuite éliminer tout le code mort.
Si vous avez des interfaces entre des fonctions définies dans différentes unités de traduction, généralement le compilateur ne sait rien. Les interfaces suivent généralement des ABI prédéfinis (comme ça ) de sorte que différents fichiers objets peuvent être liés ensemble sans aucun problème. En général, les ABI ne font pas de différence si un membre est utilisé ou non. Ainsi, dans de tels cas, le deuxième membre doit être physiquement dans la mémoire (à moins d'être éliminé plus tard par l'éditeur de liens).
Et tant que vous êtes dans les limites de la langue, vous ne pouvez pas observer qu'une élimination se produit. Si vous appelez
sizeof(Foo)
, vous obtiendrez2*sizeof(int)
. Si vous créez un tableau deFoo
s, la distance entre les débuts de deux objets consécutifs deFoo
est toujourssizeof(Foo)
octets.Votre type est un type de mise en page standard , ce qui signifie que vous pouvez également accéder aux membres en fonction des décalages calculés lors de la compilation (cf. la
offsetof
macro). De plus, vous pouvez inspecter la représentation octet par octet de l'objet en copiant sur un tableau d'char
utilisationstd::memcpy
. Dans tous ces cas, le deuxième membre peut être observé.la source
gcc -fwhole-program -O3 *.c
pourrait en théorie le faire, mais en pratique ne le fera probablement pas. (par exemple, au cas où le programme émettrait des hypothèses sur la valeur exactesizeof()
de cette cible, et parce que c'est une optimisation vraiment compliquée que les programmeurs devraient faire à la main s'ils le veulent.)Les exemples fournis par d'autres réponses à cette question qui élisent
var2
sont basés sur une seule technique d'optimisation: propagation constante, puis élision de l'ensemble de la structure (pas l'élision de justevar2
). C'est le cas simple, et l'optimisation des compilateurs l'implémente.Pour les codes C / C ++ non gérés, la réponse est que le compilateur ne sera généralement pas élidé
var2
. Autant que je sache, il n'y a pas de support pour une telle transformation de structure C / C ++ dans les informations de débogage, et si la structure est accessible en tant que variable dans un débogueur, ellevar2
ne peut pas être éludée. Autant que je sache, aucun compilateur C / C ++ actuel ne peut spécialiser les fonctions en fonction de l'élision devar2
, donc si la structure est passée ou retournée à une fonction non intégrée, ellevar2
ne peut pas être élidée.Pour les langages gérés tels que C # / Java avec un compilateur JIT, le compilateur peut être en mesure d'éliminer en toute sécurité
var2
car il peut suivre avec précision s'il est utilisé et s'il s'échappe vers du code non managé. La taille physique de la structure dans les langages managés peut être différente de sa taille signalée au programmeur.Les compilateurs C / C ++ de l'année 2019 ne peuvent pas éluder
var2
la structure à moins que la variable struct entière ne soit élidée. Pour les cas intéressants d'élision devar2
la structure, la réponse est: Non.Certains futurs compilateurs C / C ++ pourront échapper
var2
à la structure, et l'écosystème construit autour des compilateurs devra s'adapter aux informations d'élision de processus générées par les compilateurs.la source
Cela dépend de votre compilateur et de son niveau d'optimisation.
Dans gcc, si vous spécifiez
-O
, il activera les indicateurs d'optimisation suivants :-fdce
signifie Dead Code Elimination .Vous pouvez utiliser
__attribute__((used))
pour empêcher gcc d'éliminer une variable inutilisée avec un stockage statique:la source