Una variabile membro inutilizzata occupa memoria?


91

L'inizializzazione di una variabile membro e il mancato riferimento / utilizzo occupa ulteriormente RAM durante il runtime o il compilatore ignora semplicemente quella variabile?

struct Foo {
    int var1;
    int var2;

    Foo() { var1 = 5; std::cout << var1; }
};

Nell'esempio precedente, il membro "var1" ottiene un valore che viene quindi visualizzato nella console. "Var2", tuttavia, non viene utilizzato affatto. Quindi scriverlo in memoria durante il runtime sarebbe uno spreco di risorse. Il compilatore prende in considerazione questo tipo di situazioni e ignora semplicemente le variabili inutilizzate, oppure l'oggetto Foo ha sempre le stesse dimensioni, indipendentemente dal fatto che i suoi membri vengano utilizzati?


25
Questo dipende dal compilatore, dall'architettura, dal sistema operativo e dall'ottimizzazione utilizzata.
Gufo

16
C'è una tonnellata metrica di codice driver di basso livello là fuori che aggiunge specificamente membri della struttura non fare nulla per il riempimento per abbinare le dimensioni dei frame dei dati hardware e come trucco per ottenere l'allineamento della memoria desiderato. Se un compilatore iniziasse a ottimizzarli, ci sarebbero molti problemi.
Andy Brown

2
@ Anddy non fanno davvero nulla poiché viene valutato l'indirizzo dei seguenti membri di dati. Ciò significa che l'esistenza di quei membri di riempimento ha un comportamento osservabile nel programma. Qui var2no.
YSC

4
Sarei sorpreso se il compilatore potesse ottimizzarlo dato che qualsiasi unità di compilazione che indirizza tale struttura potrebbe essere collegata a un'altra unità di compilazione utilizzando la stessa struttura e il compilatore non può sapere se l'unità di compilazione separata indirizza il membro o meno.
Galik

2
@geza sizeof(Foo)non può diminuire per definizione - se si stampa sizeof(Foo)deve cedere 8(su piattaforme comuni). I compilatori possono ottimizzare lo spazio utilizzato da var2(non importa se attraverso newo sullo stack o nelle chiamate di funzione ...) in qualsiasi contesto lo trovano ragionevole, anche senza LTO o l'ottimizzazione dell'intero programma. Dove ciò non è possibile, non lo faranno, come con qualsiasi altra ottimizzazione. Credo che la modifica alla risposta accettata renda significativamente meno probabile che venga fuorviato da essa.
Max Langhof

Risposte:


106

La regola d'oro C ++ "come se" 1 afferma che, se il comportamento osservabile di un programma non dipende da un'esistenza inutilizzata di membri dati, il compilatore può ottimizzarlo .

Una variabile membro inutilizzata occupa memoria?

No (se è "veramente" inutilizzato).


Ora vengono due domande in mente:

  1. Quando il comportamento osservabile non dipenderebbe dall'esistenza di un membro?
  2. Questo tipo di situazioni si verifica nei programmi di vita reale?

Cominciamo con un esempio.

Esempio

#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{}; }

Se chiediamo a gcc di compilare questa unità di traduzione , restituisce:

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

f2è uguale a f1, e nessuna memoria è mai usata per contenere un reale Foo2::var2. ( Clang fa qualcosa di simile ).

Discussione

Alcuni potrebbero dire che questo è diverso per due motivi:

  1. questo è un esempio troppo banale,
  2. la struttura è completamente ottimizzata, non conta.

Bene, un buon programma è un assemblaggio intelligente e complesso di cose semplici piuttosto che una semplice giustapposizione di cose complesse. Nella vita reale, scrivi tonnellate di semplici funzioni usando strutture semplici che il compilatore ottimizza. Per esempio:

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

Questo è un vero esempio di un membro dei dati (qui std::pair<std::set<int>::iterator, bool>::first) non utilizzato. Indovina un po? È ottimizzato ( esempio più semplice con un set fittizio se quell'assemblaggio ti fa piangere).

Ora sarebbe il momento perfetto per leggere l'eccellente risposta di Max Langhof ( vota positivamente per me). Spiega perché, alla fine, il concetto di struttura non ha senso a livello di assembly in uscita dal compilatore.

"Ma, se faccio X, il fatto che il membro inutilizzato sia ottimizzato è un problema!"

Ci sono stati numerosi commenti in cui si sostiene che questa risposta deve essere sbagliata perché qualche operazione (come assert(sizeof(Foo2) == 2*sizeof(int))) potrebbe rompere qualcosa.

Se X fa parte del comportamento osservabile del programma 2 , al compilatore non è consentito ottimizzare le cose. Ci sono molte operazioni su un oggetto contenente un membro dati "inutilizzato" che avrebbe un effetto osservabile sul programma. Se viene eseguita una tale operazione o se il compilatore non può provare che non ne sia stata eseguita nessuna, quel dato membro "inutilizzato" fa parte del comportamento osservabile del programma e non può essere ottimizzato .

Le operazioni che influenzano il comportamento osservabile includono, ma non sono limitate a:

  • prendendo le dimensioni di un tipo di oggetto (sizeof(Foo) ),
  • prendendo l'indirizzo di un membro dei dati dichiarato dopo quello "non utilizzato",
  • copiare l'oggetto con una funzione come memcpy,
  • manipolare la rappresentazione dell'oggetto (come con memcmp),
  • qualificare un oggetto come volatile ,
  • ecc .

1)

[intro.abstract]/1

Le descrizioni semantiche in questo documento definiscono una macchina astratta non deterministica parametrizzata. Questo documento non pone alcun requisito sulla struttura delle implementazioni conformi. In particolare, non hanno bisogno di copiare o emulare la struttura della macchina astratta. Piuttosto, sono necessarie implementazioni conformi per emulare (solo) il comportamento osservabile della macchina astratta come spiegato di seguito.

2) Come un'affermazione che passa o fallisce.


I commenti che suggeriscono miglioramenti alla risposta sono stati archiviati nella chat .
Cody Grey

1
Anche il assert(sizeof(…)…)non vincola effettivamente il compilatore: deve fornire un sizeofcodice che consenta al codice di usare cose come memcpyper funzionare, ma ciò non significa che il compilatore debba in qualche modo usare così tanti byte a meno che non siano esposti a un memcpytale che può non riscrivere comunque per produrre il valore corretto.
Davis Herring

@ Davis Assolutamente.
YSC

63

È importante rendersi conto che il codice prodotto dal compilatore non ha alcuna conoscenza effettiva delle strutture dati (perché una cosa del genere non esiste a livello di assembly) e nemmeno l'ottimizzatore. Il compilatore produce solo codice per ogni funzione , non strutture di dati .

Ok, scrive anche sezioni di dati costanti e simili.

Sulla base di ciò, possiamo già dire che l'ottimizzatore non "rimuoverà" o "eliminerà" i membri, perché non restituisce strutture di dati. Produce codice , che può o non può utilizzare i membri, e tra i suoi obiettivi c'è il risparmio di memoria o cicli eliminando usi inutili (cioè scritture / letture) dei membri.


L'essenza è che "se il compilatore può provare nell'ambito di una funzione (comprese le funzioni che sono state integrate in essa) che il membro inutilizzato non fa differenza per come opera la funzione (e cosa restituisce), allora è probabile che la presenza del membro non causa overhead ".

Man mano che si rendono le interazioni di una funzione con il mondo esterno più complicate / poco chiare per il compilatore (prendere / restituire strutture di dati più complesse, ad es. std::vector<Foo> , nascondere la definizione di una funzione in una diversa unità di compilazione, proibire / disincentivare l'inlining ecc.) , diventa sempre più probabile che il compilatore non possa provare che il membro inutilizzato non ha alcun effetto.

Non ci sono regole rigide qui perché tutto dipende dalle ottimizzazioni effettuate dal compilatore, ma finché fai cose banali (come mostrato nella risposta di YSC) è molto probabile che non sia presente alcun overhead, mentre fare cose complicate (ad es. a std::vector<Foo>da una funzione troppo grande per l'inlining) probabilmente incorrerà in un sovraccarico.


Per illustrare il punto, considera questo esempio :

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];
}

Facciamo cose non banali qui (prendiamo indirizzi, ispezioniamo e aggiungiamo byte dalla rappresentazione dei byte ) e tuttavia l'ottimizzatore può capire che il risultato è sempre lo stesso su questa piattaforma:

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

Non solo i membri di Foonon occupavano alcuna memoria, a Foonon sono nemmeno nati! Se ci sono altri usi che non possono essere ottimizzati, ad esempio sizeof(Foo)potrebbe essere importante, ma solo per quel segmento di codice! Se tutti gli usi potessero essere ottimizzati in questo modo, l'esistenza di eg var3non influenza il codice generato. Ma anche se fosse utilizzato altrove, test()rimarrebbe ottimizzato!

In breve: ogni utilizzo di Fooè ottimizzato in modo indipendente. Alcuni potrebbero utilizzare più memoria a causa di un membro non necessario, altri no. Consultare il manuale del compilatore per maggiori dettagli.


6
Mic drop "Consultare il manuale del compilatore per maggiori dettagli." : D
YSC

22

Il compilatore ottimizzerà solo una variabile membro inutilizzata (specialmente una pubblica) se può dimostrare che la rimozione della variabile non ha effetti collaterali e che nessuna parte del programma dipende dalla dimensione Foodell'essere la stessa.

Non credo che nessun compilatore corrente esegua tali ottimizzazioni a meno che la struttura non sia realmente utilizzata. Alcuni compilatori possono almeno mettere in guardia sulle variabili private inutilizzate ma di solito non per quelle pubbliche.


1
Eppure lo fa: godbolt.org/z/UJKguS + nessun compilatore avviserebbe per un membro dati inutilizzato.
YSC

@YSC clang ++ avverte di variabili e membri dati inutilizzati.
Maxim Egorushkin

3
@YSC Penso che sia una situazione leggermente diversa, ha ottimizzato completamente la struttura e stampa solo 5 direttamente
Alan Birtles

4
@AlanBirtles Non vedo come sia diverso. Il compilatore ha ottimizzato tutto dall'oggetto che non ha alcun effetto sul comportamento osservabile del programma. Quindi la tua prima frase "è molto improbabile che il compilatore ottimizzi una variabile membro inutilizzata" è sbagliata.
YSC

2
@YSC in codice reale in cui la struttura viene effettivamente utilizzata piuttosto che semplicemente costruita per i suoi effetti collaterali, probabilmente è più improbabile che venga ottimizzata
Alan Birtles

7

In generale, devi presumere di ottenere ciò che hai chiesto, ad esempio, le variabili membro "inutilizzate" sono presenti.

Poiché nel tuo esempio sono entrambi i membri public, il compilatore non può sapere se un codice (in particolare da altre unità di traduzione = altri file * .cpp, che sono compilati separatamente e quindi collegati) accederà al membro "inutilizzato".

La risposta di YSC fornisce un esempio molto semplice, in cui il tipo di classe viene utilizzato solo come variabile di durata della memorizzazione automatica e in cui non viene preso alcun puntatore a quella variabile. Lì, il compilatore può incorporare tutto il codice e quindi eliminare tutto il codice inattivo.

Se si dispone di interfacce tra funzioni definite in diverse unità di traduzione, in genere il compilatore non sa nulla. Le interfacce seguono tipicamente alcune ABI predefinite (come quella ) in modo tale che diversi file oggetto possono essere collegati insieme senza problemi. In genere gli ABI non fanno la differenza se un membro viene utilizzato o meno. Quindi, in questi casi, il secondo membro deve essere fisicamente nella memoria (a meno che non venga eliminato in seguito dal linker).

E fintanto che sei all'interno dei confini della lingua, non puoi osservare che avviene alcuna eliminazione. Se chiami sizeof(Foo), riceverai 2*sizeof(int). Se crei un array di Foos, la distanza tra l'inizio di due oggetti consecutivi di Fooè sempre sizeof(Foo)byte.

Il tuo tipo è un tipo di layout standard , il che significa che puoi accedere anche ai membri in base agli offset calcolati in fase di compilazione (vedi la offsetofmacro). Inoltre, puoi controllare la rappresentazione byte per byte dell'oggetto copiandola su un array di charusing std::memcpy. In tutti questi casi, si può osservare la presenza del secondo membro.


I commenti non sono per discussioni estese; questa conversazione è stata spostata nella chat .
Cody Grey

2
+1: solo un'ottimizzazione aggressiva dell'intero programma potrebbe eventualmente regolare il layout dei dati (comprese le dimensioni e gli offset in fase di compilazione) per i casi in cui un oggetto struct locale non è completamente ottimizzato,. gcc -fwhole-program -O3 *.cpotrebbe in teoria farlo, ma in pratica probabilmente non lo farà. (ad esempio, nel caso in cui il programma faccia alcune ipotesi su quale valore esatto sizeof()ha su questo obiettivo, e perché è un'ottimizzazione davvero complicata che i programmatori dovrebbero fare a mano se lo desiderano.)
Peter Cordes

6

Gli esempi forniti da altre risposte a questa domanda che elide var2si basano su un'unica tecnica di ottimizzazione: propagazione costante, e successiva elisione dell'intera struttura (non l'elisione del giusto var2). Questo è il caso semplice e l'ottimizzazione dei compilatori lo implementa.

Per i codici C / C ++ non gestiti la risposta è che il compilatore in generale non elide var2. Per quanto ne so, non c'è supporto per una tale trasformazione della struttura C / C ++ nelle informazioni di debug e se la struttura è accessibile come variabile in un debugger, var2non può essere elisa. Per quanto ne so, nessun compilatore C / C ++ corrente può specializzare le funzioni in base all'elisione di var2, quindi se la struttura viene passata o restituita da una funzione non inline, var2non può essere elisa.

Per i linguaggi gestiti come C # / Java con un compilatore JIT, il compilatore potrebbe essere in grado di elide in modo sicuro var2perché può tracciare con precisione se viene utilizzato e se sfugge a codice non gestito. La dimensione fisica della struttura nei linguaggi gestiti può essere diversa dalla sua dimensione riportata al programmatore.

I compilatori C / C ++ per l'anno 2019 non possono elidere var2dalla struttura a meno che l'intera variabile della struttura non sia elisa. Per casi interessanti di elisione di var2dalla struttura, la risposta è: No.

Alcuni futuri compilatori C / C ++ saranno in grado di elidere var2dalla struttura e l'ecosistema costruito attorno ai compilatori dovrà adattarsi per elaborare le informazioni di elisione generate dai compilatori.


1
Il tuo paragrafo sulle informazioni di debug si riduce a "non possiamo ottimizzarlo se questo rendesse il debug più difficile", il che è semplicemente sbagliato. O sto interpretando male. Potresti chiarire?
Max Langhof

Se il compilatore emette informazioni di debug sulla struttura, non può elide var2. Le opzioni sono: (1) Non emettere le informazioni di debug se non corrispondono alla rappresentazione fisica della struttura, (2) Supporta l'elisione del membro della struttura nelle informazioni di debug ed emette le informazioni di debug
atomsymbol

Forse più generale è fare riferimento alla sostituzione scalare degli aggregati (e quindi all'elisione dei negozi morti, ecc .).
Davis Herring

4

Dipende dal tuo compilatore e dal suo livello di ottimizzazione.

In gcc, se specifichi -O, attiverà i seguenti flag di ottimizzazione :

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

-fdcesta per Dead Code Elimination .

Puoi utilizzare __attribute__((used))per impedire a gcc di eliminare una variabile inutilizzata con l'archiviazione statica:

Questo attributo, collegato a una variabile con memoria statica, significa che la variabile deve essere emessa anche se sembra che la variabile non sia referenziata.

Quando viene applicato a un membro dati statico di un modello di classe C ++, l'attributo significa anche che viene creata un'istanza del membro se viene creata un'istanza della classe stessa.


Questo è per i membri di dati statici , non per i membri inutilizzati per istanza (che non vengono ottimizzati a meno che non lo faccia l'intero oggetto). Ma sì, immagino che conti. A proposito, l'eliminazione delle variabili statiche inutilizzate non è l' eliminazione del codice morto , a meno che GCC non pieghi il termine.
Peter Cordes
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.