web-dev-qa-db-fra.com

Une variable membre inutilisée prend-elle de la mémoire?

L'initialisation d'une variable membre et son non-référencement/utilisation prennent-ils davantage RAM lors de 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 utilisé du tout. Par conséquent, l'écrire dans la mémoire pendant l'exécution serait une perte de ressources. Le compilateur prend-il ces types de situations dans un compte et ignore-t-il simplement les variables inutilisées, ou l'objet Foo est-il toujours de la même taille, que ses membres soient ou non utilisés?

89
Chriss555888

La règle d'or du C++ "comme si"1 indique 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 .

Une variable membre inutilisée prend-elle de la mémoire?

Non (s'il n'est "vraiment" pas utilisé).


Vient maintenant deux questions à l'esprit:

  1. Quand le comportement observable ne dépendrait-il pas de l'existence d'un membre?
  2. Ce genre de situations se produit-il dans des programmes réels?

Commençons par un exemple.

Exemple

#include <iostream>

struct Foo1
{ int var1 = 5;           Foo1() { std::cout << var1; } };

struct Foo2
{ int var1 = 5; int var2; Foo2() { std::cout << var1; } };

void f1() { (void) Foo1{}; }
void f2() { (void) Foo2{}; }

Si nous demandons gcc pour compiler cette unité de traduction , il génère:

f1():
        mov     esi, 5
        mov     edi, OFFSET FLAT:_ZSt4cout
        jmp     std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
f2():
        jmp     f1()

f2 Est identique à f1, Et aucune mémoire n'est jamais utilisée pour contenir un Foo2::var2 Réel. ( Clang fait quelque chose de similaire ).

Discussion

Certains diront que c'est différent pour deux raisons:

  1. c'est un exemple trop trivial,
  2. la structure est entièrement optimisée, cela ne compte pas.

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:

bool insert(std::set<int>& set, int value)
{
    return set.insert(value).second;
}

Ceci est un exemple authentique d'un membre de données (ici, std::pair<std::set<int>::iterator, bool>::first) Inutilisé. Devine quoi? Il est optimisé loin ( 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 (voter 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 des sorties du compilateur.

"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 soutenant que cette réponse doit être fausse car une opération (comme assert(sizeof(Foo2) == 2*sizeof(int))) casserait quelque chose.

Si X fait partie du comportement observable du programme2, 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, sans s'y limiter:

  • prendre la taille d'un type d'objet (sizeof(Foo)),
  • prendre l'adresse d'un membre de données déclaré après celui "inutilisé",
  • copier l'objet avec une fonction comme memcpy,
  • manipuler la représentation de l'objet (comme avec memcmp),
  • qualifier un objet comme volatile,
  • etc.

1)

[intro.abstract]/1

Les descriptions sémantiques de ce document définissent une machine abstraite non déterministe paramétrée. Ce document n'impose aucune exigence sur la structure des implémentations conformes. En particulier, ils n'ont pas besoin de copier ou d'émuler la structure de la machine abstraite. Au contraire, des implémentations conformes sont nécessaires pour émuler (uniquement) le comportement observable de la machine abstraite comme expliqué ci-dessous.

2) Comme une affirmation qui passe ou échoue.

104
YSC

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 ne produit que code pour chaque fonction, pas 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 "n'éliminera" les membres, car il ne génère pas de structures de données. Il génère code, qui peut ou non utiliser les membres, et parmi ses objectifs est d'économiser de la mémoire ou des cycles en éliminant inutile utilise (c'est-à-dire écrit/lit) des membres.


L'essentiel est que "si le compilateur peut prouver dans le cadre d'une fonction (y compris les fonctions qui y étaient insérées) que le membre inutilisé ne fait aucune différence pour le fonctionnement de la fonction (et ce qu'il retourne) alors les chances sont bonnes que la présence du membre n'entraîne pas de frais généraux ".

Lorsque vous rendez les interactions d'une fonction avec le monde extérieur plus compliquées/peu claires pour le compilateur (prenez/renvoyez des structures de données plus complexes, par exemple un std::vector<Foo>, Masquez la définition d'une fonction dans une autre unité de compilation, interdisez/dissuader la mise en ligne, 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 apportées par le compilateur, mais tant que vous faites des choses triviales (comme indiqué dans la réponse de YSC), il est très probable qu'aucune surcharge ne soit présente, tandis que des choses compliquées (par exemple, le retour un std::vector<Foo> d'une fonction trop grande pour être inséré) entraînera probablement la surcharge.


Pour illustrer ce point, considérons cet exemple :

struct Foo {
    int var1 = 3;
    int var2 = 4;
    int var3 = 5;
};

int test()
{
    Foo foo;
    std::array<char, sizeof(Foo)> arr;
    std::memcpy(&arr, &foo, sizeof(Foo));
    return arr[0] + arr[4];
}

Nous faisons des choses non triviales ici (prendre des adresses, inspecter et ajouter des octets de la représentation des octets ) et pourtant l'optimiseur peut comprendre que le résultat est toujours le même sur cette plate-forme:

test(): # @test()
  mov eax, 7
  ret

Non seulement les membres de Foo n'occupaient aucune mémoire, un Foo n'était même pas né! S'il y a d'autres utilisations qui ne peuvent pas être optimisées, par exemple sizeof(Foo) peut être important - mais uniquement pour ce segment de code! Si toutes les utilisations pouvaient être optimisées de cette manière, alors l'existence par ex. var3 N'influence 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 à cause d'un membre inutile, d'autres non. Consultez le manuel de votre compilateur pour plus de détails.

61
Max Langhof

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 la taille de Foo étant la même.

Je ne pense pas qu'un compilateur actuel effectue de telles optimisations à moins que la structure ne soit pas vraiment utilisée du tout. Certains compilateurs peuvent au moins avertir des variables privées inutilisées, mais pas généralement des variables publiques.

22
Alan Birtles

En général, vous devez supposer que vous obtenez ce que vous avez demandé, par exemple, les variables membres "inutilisées" sont là.

Étant donné que dans votre exemple, les deux membres sont public, le compilateur ne peut pas savoir si du code (en particulier provenant d'autres unités de traduction = autres fichiers * .cpp, qui sont compilés séparément puis liés) accéderait 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, le compilateur ne sait généralement rien. Les interfaces suivent généralement certains ABI prédéfinis (comme that ) de sorte que différents fichiers objets peuvent être liés entre eux sans aucun problème. En règle générale, les ABI ne font aucune 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 (sauf s'il est éliminé plus tard par l'éditeur de liens).

Et tant que vous êtes dans les limites de la langue, vous ne pouvez pas observer d'élimination. Si vous appelez sizeof(Foo), vous obtiendrez 2*sizeof(int). Si vous créez un tableau de Foos, la distance entre les débuts de deux objets consécutifs de Foo est toujours sizeof(Foo) octets.

Votre type est un type de mise en page standard , ce qui signifie que vous pouvez également accéder aux membres sur la base des décalages calculés au moment de la compilation (cf. la macro offsetof ) . De plus, vous pouvez inspecter la représentation octet par octet de l'objet en copiant sur un tableau de char en utilisant std::memcpy. Dans tous ces cas, le deuxième membre peut être observé comme étant là.

7
Handy999

Les exemples fournis par d'autres réponses à cette question qui éludent var2 reposent sur une seule technique d'optimisation: propagation constante et élision subséquente de l'ensemble de la structure (et non pas l'élision de seulement var2). C'est le cas simple, et les compilateurs d'optimisation l'implémentent.

Pour les codes C/C++ non gérés, la réponse est que le compilateur n'élidera généralement pas var2. Pour 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, alors var2 ne peut pas être élidé. Pour autant que je sache, aucun compilateur C/C++ actuel ne peut spécialiser les fonctions en fonction de l'élision de var2, donc si la structure est passée à ou retournée par une fonction non en ligne, alors var2 ne peut pas être élidé.

Pour les langages gérés tels que C #/Java avec un compilateur JIT, le compilateur peut être en mesure d'élider 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 gérés peut être différente de sa taille signalée au programmeur.

Les compilateurs C/C++ de l'année 2019 ne peuvent pas être supprimés var2 à partir de la structure à moins que toute la variable de structure ne soit élidée. Pour les cas intéressants d'élision de var2 de la structure, la réponse est: Non.

Certains futurs compilateurs C/C++ pourront élider var2 à partir de la structure, et l'écosystème construit autour des compilateurs devra s'adapter pour traiter les informations d'élision générées par les compilateurs.

6
atomsymbol

Cela dépend de votre compilateur et de son niveau d'optimisation.

Dans gcc, si vous spécifiez -O, Il activera les drapeaux d'optimisation suivants :

-fauto-inc-dec 
-fbranch-count-reg 
-fcombine-stack-adjustments 
-fcompare-elim 
-fcprop-registers 
-fdce
-fdefer-pop
...

-fdce Signifie Dead Code Elimination .

Vous pouvez utiliser __attribute__((used)) pour empêcher gcc d'éliminer une variable inutilisée avec un stockage statique:

Cet attribut, attaché à une variable avec stockage statique, signifie que la variable doit être émise même s'il apparaît que la variable n'est pas référencée.

Lorsqu'il est appliqué à un membre de données statiques d'un modèle de classe C++, l'attribut signifie également que le membre est instancié si la classe elle-même est instanciée.

4
wonter