Nimmt eine nicht verwendete Mitgliedsvariable Speicherplatz ein?

91

Wird durch das Initialisieren einer Mitgliedsvariablen und das Nichtreferenzieren / Verwenden dieser Variable zur Laufzeit mehr RAM belegt, oder ignoriert der Compiler diese Variable einfach?

struct Foo {
    int var1;
    int var2;

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

Im obigen Beispiel erhält das Mitglied 'var1' einen Wert, der dann in der Konsole angezeigt wird. 'Var2' wird jedoch überhaupt nicht verwendet. Daher wäre es eine Verschwendung von Ressourcen, es zur Laufzeit in den Speicher zu schreiben. Berücksichtigt der Compiler solche Situationen und ignoriert er einfach nicht verwendete Variablen oder ist das Foo-Objekt immer gleich groß, unabhängig davon, ob seine Mitglieder verwendet werden?

Chriss555888
quelle
25
Dies hängt vom Compiler, der Architektur, dem Betriebssystem und der verwendeten Optimierung ab.
Eule
16
Es gibt eine Tonne Treibercode auf niedriger Ebene, die speziell Do-Nothing-Strukturelemente zum Auffüllen hinzufügen, um sie an die Größe der Hardwaredatenrahmen anzupassen, und als Hack, um die gewünschte Speicherausrichtung zu erhalten. Wenn ein Compiler anfangen würde, diese zu optimieren, würde es viel Bruch geben.
Andy Brown
2
@Andy sie tun nicht wirklich nichts, da die Adresse der folgenden Datenelemente ausgewertet wird. Dies bedeutet, dass die Existenz dieser Polsterelemente ein beobachtbares Verhalten im Programm aufweist. Hier var2nicht.
YSC
4
Es würde mich wundern, wenn der Compiler es wegoptimieren könnte, da eine Kompilierungseinheit, die eine solche Struktur adressiert, möglicherweise mit derselben Struktur mit einer anderen Kompilierungseinheit verknüpft wird und der Compiler nicht wissen kann, ob die separate Kompilierungseinheit das Mitglied anspricht oder nicht.
Galik
2
@geza sizeof(Foo)kann per Definition nicht abnehmen - wenn Sie drucken sizeof(Foo), muss es ergeben 8(auf gängigen Plattformen). Compiler können den von var2(egal ob durch newoder auf dem Stack oder in Funktionsaufrufen ...) genutzten Speicherplatz in jedem Kontext optimieren, den sie für sinnvoll halten, auch ohne LTO oder Optimierung des gesamten Programms. Wo dies nicht möglich ist, werden sie es nicht tun, wie bei fast jeder anderen Optimierung. Ich glaube, dass die Bearbeitung der akzeptierten Antwort die Wahrscheinlichkeit, dass sie irregeführt wird, erheblich verringert.
Max Langhof

Antworten:

106

Die goldene C ++ "Als-ob" -Regel 1 besagt, dass der Compiler , wenn das beobachtbare Verhalten eines Programms nicht von einer nicht verwendeten Existenz eines Datenelements abhängt, es wegoptimieren darf .

Nimmt eine nicht verwendete Mitgliedsvariable Speicherplatz ein?

Nein (wenn es "wirklich" unbenutzt ist).


Nun kommen zwei Fragen in den Sinn:

  1. Wann würde das beobachtbare Verhalten nicht von der Existenz eines Mitglieds abhängen?
  2. Treten solche Situationen in realen Programmen auf?

Beginnen wir mit einem Beispiel.

Beispiel

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

Wenn wir gcc bitten , diese Übersetzungseinheit zu kompilieren , wird Folgendes ausgegeben:

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

f2ist das gleiche wie f1und es wird nie ein Speicher verwendet, um eine tatsächliche zu halten Foo2::var2. ( Clang macht etwas Ähnliches ).

Diskussion

Einige mögen sagen, dass dies aus zwei Gründen anders ist:

  1. Dies ist ein zu triviales Beispiel.
  2. Die Struktur ist vollständig optimiert, sie zählt nicht.

Nun, ein gutes Programm ist eher eine intelligente und komplexe Zusammenstellung einfacher Dinge als eine einfache Gegenüberstellung komplexer Dinge. Im wirklichen Leben schreiben Sie Tonnen einfacher Funktionen mit einfachen Strukturen, die der Compiler nicht optimiert. Zum Beispiel:

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

Dies ist ein echtes Beispiel dafür, dass ein Datenelement (hier std::pair<std::set<int>::iterator, bool>::first) nicht verwendet wird. Erraten Sie, was? Es wird weg optimiert ( einfacheres Beispiel mit einem Dummy-Set wenn diese Baugruppe Sie zum Weinen bringt).

Jetzt wäre der perfekte Zeitpunkt, um die ausgezeichnete Antwort von Max Langhof zu lesen (bitte für mich). Es erklärt, warum das Konzept der Struktur auf Assembly-Ebene, die der Compiler ausgibt, letztendlich keinen Sinn ergibt.

"Aber wenn ich X mache, ist die Tatsache, dass das nicht verwendete Mitglied wegoptimiert ist, ein Problem!"

Es gab eine Reihe von Kommentaren, in denen argumentiert wurde, dass diese Antwort falsch sein muss, da eine Operation (wie assert(sizeof(Foo2) == 2*sizeof(int))) etwas kaputt machen würde.

Wenn X Teil des beobachtbaren Verhaltens von Programm 2 ist, darf der Compiler keine optimierten Dinge entfernen. Es gibt viele Operationen an einem Objekt, die ein "nicht verwendetes" Datenelement enthalten, was sich beobachtbar auf das Programm auswirken würde. Wenn eine solche Operation ausgeführt wird oder der Compiler nicht nachweisen kann, dass keine ausgeführt wird, ist dieses "nicht verwendete" Datenelement Teil des beobachtbaren Verhaltens des Programms und kann nicht wegoptimiert werden .

Operationen, die das beobachtbare Verhalten beeinflussen, umfassen, sind aber nicht beschränkt auf:

  • die Größe eines Objekttyps annehmen (sizeof(Foo) ),
  • die Adresse eines Datenelements nehmen, das nach dem "nicht verwendeten" deklariert wurde,
  • Kopieren des Objekts mit einer Funktion wie memcpy:
  • Manipulieren der Darstellung des Objekts (wie bei memcmp),
  • ein Objekt als flüchtig qualifizieren ,
  • etc .

1)

[intro.abstract]/1

Die semantischen Beschreibungen in diesem Dokument definieren eine parametrisierte nichtdeterministische abstrakte Maschine. Dieses Dokument stellt keine Anforderungen an die Struktur konformer Implementierungen. Insbesondere müssen sie die Struktur der abstrakten Maschine nicht kopieren oder emulieren. Vielmehr sind konforme Implementierungen erforderlich, um (nur) das beobachtbare Verhalten der abstrakten Maschine zu emulieren, wie nachstehend erläutert.

2) Wie eine Behauptung ist bestanden oder nicht bestanden.

YSC
quelle
Kommentare, die Verbesserungen der Antwort vorschlagen, wurden im Chat archiviert .
Cody Gray
1
Selbst das assert(sizeof(…)…)schränkt den Compiler nicht wirklich ein - es muss ein sizeofCode bereitgestellt werden, der es ermöglicht, dass Code mit Dingen wie memcpyArbeit funktioniert, aber das bedeutet nicht, dass der Compiler irgendwie so viele Bytes verwenden muss, es sei denn, sie sind möglicherweise einem solchen ausgesetzt, wie memcpyes möglich ist Schreiben Sie nicht um, um den richtigen Wert zu erhalten.
Davis Herring
@ Davis Absolut.
YSC
63

Es ist wichtig zu wissen, dass der vom Compiler erzeugte Code keine tatsächlichen Kenntnisse Ihrer Datenstrukturen hat (da so etwas auf Assembly-Ebene nicht vorhanden ist), und der Optimierer auch nicht. Der Compiler erzeugt nur Code für jede Funktion , keine Datenstrukturen .

Ok, es schreibt auch konstante Datenabschnitte und so.

Auf dieser Grundlage können wir bereits sagen, dass der Optimierer keine Mitglieder "entfernt" oder "entfernt", da er keine Datenstrukturen ausgibt. Es gibt Code , der nicht kann oder verwendet die Mitglieder, und unter ihren Zielen durch Eliminieren sinnlos Speicher oder Zyklen sparende Anwendungen (dh schreibt / liest) der Mitglieder.


Das Wesentliche dabei ist: "Wenn der Compiler im Rahmen einer Funktion (einschließlich der darin eingefügten Funktionen) nachweisen kann, dass das nicht verwendete Element keinen Unterschied für die Funktionsweise der Funktion (und deren Rückgabe) macht, stehen die Chancen gut, dass Die Anwesenheit des Mitglieds verursacht keinen Overhead. "

Wenn Sie die Interaktionen einer Funktion mit der Außenwelt für den Compiler komplizierter / unklarer machen (komplexere Datenstrukturen übernehmen / zurückgeben, z. B. a std::vector<Foo>, die Definition einer Funktion in einer anderen Kompilierungseinheit verbergen, Inlining verbieten / deaktivieren usw.) Es wird immer wahrscheinlicher, dass der Compiler nicht beweisen kann, dass das nicht verwendete Mitglied keine Wirkung hat.

Hier gibt es keine strengen Regeln, da alles von den Optimierungen abhängt, die der Compiler vornimmt. Solange Sie jedoch triviale Dinge tun (wie in der Antwort von YSC gezeigt), ist es sehr wahrscheinlich, dass kein Overhead vorhanden ist, während Sie komplizierte Dinge tun (z. B. zurückkehren) a std::vector<Foo>von einer Funktion, die zum Inlining zu groß ist) wird wahrscheinlich den Overhead verursachen.


Betrachten Sie dieses Beispiel, um den Punkt zu veranschaulichen :

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

Wir machen hier nicht triviale Dinge (nehmen Sie Adressen, überprüfen Sie und fügen Sie Bytes aus der Byldarstellung hinzu ), und dennoch kann der Optimierer feststellen, dass das Ergebnis auf dieser Plattform immer das gleiche ist:

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

Die Mitglieder von Foobesetzten nicht nur keine Erinnerung, Fooes entstand nicht einmal eine! Wenn es andere Verwendungen gibt, die nicht optimiert werden können, ist dies sizeof(Foo)möglicherweise von Bedeutung - aber nur für dieses Codesegment! Wenn alle Verwendungen auf diese Weise optimiert werden könnten, hat das Vorhandensein von zB var3keinen Einfluss auf den generierten Code. Aber selbst wenn es woanders verwendet wird, test()würde es optimiert bleiben!

Kurzum: Jede Nutzung von Foowird unabhängig optimiert. Einige verwenden möglicherweise mehr Speicher aufgrund eines nicht benötigten Mitglieds, andere möglicherweise nicht. Weitere Informationen finden Sie in Ihrem Compiler-Handbuch.

Max Langhof
quelle
6
Mic drop "Weitere Informationen finden Sie in Ihrem Compiler-Handbuch." : D
YSC
22

Der Compiler optimiert eine nicht verwendete Mitgliedsvariable (insbesondere eine öffentliche) nur dann, wenn er nachweisen kann, dass das Entfernen der Variablen keine Nebenwirkungen hat und dass kein Teil des Programms von der Größe von abhängt Foo .

Ich glaube nicht, dass ein aktueller Compiler solche Optimierungen durchführt, es sei denn, die Struktur wird überhaupt nicht wirklich verwendet. Einige Compiler warnen möglicherweise zumindest vor nicht verwendeten privaten Variablen, normalerweise jedoch nicht vor öffentlichen.

Alan Birtles
quelle
1
Und doch: godbolt.org/z/UJKguS + Kein Compiler würde vor einem nicht verwendeten Datenelement warnen.
YSC
@YSC clang ++ warnt vor nicht verwendeten Datenelementen und Variablen.
Maxim Egorushkin
3
@YSC Ich denke, das ist eine etwas andere Situation, es hat die Struktur komplett optimiert und druckt nur 5 direkt
Alan Birtles
4
@ AlanBirtles Ich sehe nicht, wie es anders ist. Der Compiler optimierte alles aus dem Objekt heraus, was keinen Einfluss auf das beobachtbare Verhalten des Programms hat. Ihr erster Satz "Es ist sehr unwahrscheinlich, dass der Compiler eine nicht verwendete Mitgliedsvariable optimiert" ist also falsch.
YSC
2
@YSC in echtem Code, wo die Struktur tatsächlich verwendet wird und nicht nur für ihre Nebenwirkungen konstruiert wird, ist es wahrscheinlich unwahrscheinlicher, dass sie weg optimiert wird
Alan Birtles
7

Im Allgemeinen müssen Sie davon ausgehen, dass Sie das bekommen, wonach Sie gefragt haben, zum Beispiel, dass die "nicht verwendeten" Mitgliedsvariablen vorhanden sind.

Da in Ihrem Beispiel beide Mitglieder vorhanden sind public, kann der Compiler nicht wissen, ob ein Code (insbesondere von anderen Übersetzungseinheiten = anderen * .cpp-Dateien, die separat kompiliert und dann verknüpft werden) auf das "nicht verwendete" Mitglied zugreifen würde.

Die Antwort von YSC gibt ein sehr einfaches Beispiel, bei dem der Klassentyp nur als Variable für die automatische Speicherdauer verwendet wird und kein Zeiger auf diese Variable verwendet wird. Dort kann der Compiler den gesamten Code einbinden und dann den gesamten toten Code entfernen.

Wenn Sie Schnittstellen zwischen Funktionen haben, die in verschiedenen Übersetzungseinheiten definiert sind, weiß der Compiler normalerweise nichts. Die Schnittstellen folgen in der Regel einige ABI vorgegeben (wie die ), sodass verschiedene Objektdateien problemlos miteinander verknüpft werden können. In der Regel machen ABIs keinen Unterschied, ob ein Mitglied verwendet wird oder nicht. In solchen Fällen muss sich das zweite Mitglied also physisch im Speicher befinden (sofern es nicht später vom Linker entfernt wird).

Und solange Sie sich innerhalb der Grenzen der Sprache befinden, können Sie nicht beobachten, dass eine Eliminierung stattfindet. Wenn Sie anrufen sizeof(Foo), erhalten Sie 2*sizeof(int). Wenn Sie ein Array von Foos erstellen, beträgt der Abstand zwischen den Anfängen zweier aufeinanderfolgender Objekte Fooimmer sizeof(Foo)Bytes.

Ihr Typ ist ein Standardlayouttyp. Dies bedeutet, dass Sie auch auf Mitglieder zugreifen können, die auf berechneten Offsets zur Kompilierungszeit basieren (siehe offsetofMakro). Darüber hinaus können Sie die byteweise Darstellung des Objekts überprüfen, indem Sie sie in ein Array mit charusing kopieren std::memcpy. In all diesen Fällen kann beobachtet werden, dass das zweite Mitglied dort ist.

Handy999
quelle
Kommentare sind nicht für eine ausführliche Diskussion gedacht. Dieses Gespräch wurde in den Chat verschoben .
Cody Gray
2
+1: Nur eine aggressive Optimierung des gesamten Programms kann möglicherweise das Datenlayout (einschließlich Größen und Offsets zur Kompilierungszeit) für Fälle anpassen, in denen ein lokales Strukturobjekt nicht vollständig optimiert wird. gcc -fwhole-program -O3 *.ckönnte es theoretisch tun, aber in der Praxis wahrscheinlich nicht. (zB für den Fall, dass das Programm einige Annahmen darüber macht, welchen genauen Wert sizeof()dieses Ziel hat, und weil es eine wirklich komplizierte Optimierung ist, die Programmierer von Hand durchführen sollten, wenn sie es wollen.)
Peter Cordes
6

Die Beispiele anderer Antworten auf diese Frage, die elidieren, var2basieren auf einer einzigen Optimierungstechnik: konstante Ausbreitung und anschließende Elision der gesamten Struktur (nicht die Elision von gerecht var2). Dies ist der einfache Fall, und optimierende Compiler implementieren ihn.

Für nicht verwaltete C / C ++ - Codes lautet die Antwort, dass der Compiler im Allgemeinen nicht elidiert var2. Soweit ich weiß, gibt es keine Unterstützung für eine solche C / C ++ - Strukturtransformation beim Debuggen von Informationen, und wenn die Struktur als Variable in einem Debugger zugänglich ist, var2kann sie nicht entfernt werden. Soweit ich weiß, kann kein aktueller C / C ++ - Compiler Funktionen gemäß der Elision von spezialisieren var2. Wenn also die Struktur an eine nicht inline-Funktion übergeben oder von dieser zurückgegeben wird, var2kann sie nicht entfernt werden.

Bei verwalteten Sprachen wie C # / Java mit einem JIT-Compiler kann der Compiler möglicherweise sicher umgehen, var2da er genau verfolgen kann, ob er verwendet wird und ob er in nicht verwalteten Code übergeht . Die physische Größe der Struktur in verwalteten Sprachen kann sich von der Größe unterscheiden, die dem Programmierer gemeldet wurde.

C / C ++ - Compiler des Jahres 2019 können nur dann var2aus der Struktur entfernt werden, wenn die gesamte Strukturvariable entfernt wird. Für interessante Fälle der Elision var2aus der Struktur lautet die Antwort: Nein.

Einige zukünftige C / C ++ - Compiler können sich var2von der Struktur lösen , und das um die Compiler herum aufgebaute Ökosystem muss sich an die von Compilern generierten Elisionsinformationen anpassen.

Atomsymbol
quelle
1
Ihr Absatz über Debug-Informationen läuft darauf hinaus, "wir können ihn nicht wegoptimieren, wenn dies das Debuggen erschweren würde", was einfach falsch ist. Oder ich verstehe falsch. Könnten Sie das klarstellen?
Max Langhof
Wenn der Compiler Debug-Informationen über die Struktur ausgibt, kann er var2 nicht entfernen. Folgende Optionen stehen zur Verfügung: (1) Geben Sie die Debug-Informationen nicht aus, wenn sie nicht der physischen Darstellung der Struktur entsprechen. (2) Unterstützen Sie die Elision der Strukturmitglieder in den Debug-Informationen und geben Sie die Debug-Informationen aus
Atomsymbol
Allgemeiner ist vielleicht der skalare Ersatz von Aggregaten (und dann die Beseitigung toter Vorräte usw. ).
Davis Herring
4

Dies hängt von Ihrem Compiler und seiner Optimierungsstufe ab.

Wenn Sie in gcc angeben -O, werden die folgenden Optimierungsflags aktiviert :

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

-fdcesteht für Dead Code Elimination .

Sie können verwenden __attribute__((used)), um zu verhindern, dass gcc eine nicht verwendete Variable mit statischem Speicher entfernt:

Dieses Attribut, das an eine Variable mit statischem Speicher angehängt ist, bedeutet, dass die Variable ausgegeben werden muss, auch wenn es den Anschein hat, dass auf die Variable nicht verwiesen wird.

Bei Anwendung auf ein statisches Datenelement einer C ++ - Klassenvorlage bedeutet das Attribut auch, dass das Element instanziiert wird, wenn die Klasse selbst instanziiert wird.

Winter
quelle
Dies gilt für statische Datenelemente, nicht für nicht verwendete Mitglieder pro Instanz (die nur dann optimiert werden, wenn das gesamte Objekt dies tut). Aber ja, ich denke das zählt. Übrigens ist das Eliminieren nicht verwendeter statischer Variablen keine Eliminierung von totem Code , es sei denn, GCC verbiegt den Begriff.
Peter Cordes