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?
var2
nicht.sizeof(Foo)
kann per Definition nicht abnehmen - wenn Sie druckensizeof(Foo)
, muss es ergeben8
(auf gängigen Plattformen). Compiler können den vonvar2
(egal ob durchnew
oder 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.Antworten:
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 .
Nein (wenn es "wirklich" unbenutzt ist).
Nun kommen zwei Fragen in den Sinn:
Beginnen wir mit einem Beispiel.
Beispiel
Wenn wir gcc bitten , diese Übersetzungseinheit zu kompilieren , wird Folgendes ausgegeben:
f2
ist das gleiche wief1
und es wird nie ein Speicher verwendet, um eine tatsächliche zu haltenFoo2::var2
. ( Clang macht etwas Ähnliches ).Diskussion
Einige mögen sagen, dass dies aus zwei Gründen anders ist:
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:
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:
sizeof(Foo)
),memcpy
:memcmp
),1)
2) Wie eine Behauptung ist bestanden oder nicht bestanden.
quelle
assert(sizeof(…)…)
schränkt den Compiler nicht wirklich ein - es muss einsizeof
Code bereitgestellt werden, der es ermöglicht, dass Code mit Dingen wiememcpy
Arbeit funktioniert, aber das bedeutet nicht, dass der Compiler irgendwie so viele Bytes verwenden muss, es sei denn, sie sind möglicherweise einem solchen ausgesetzt, wiememcpy
es möglich ist Schreiben Sie nicht um, um den richtigen Wert zu erhalten.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 :
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:
Die Mitglieder von
Foo
besetzten nicht nur keine Erinnerung,Foo
es entstand nicht einmal eine! Wenn es andere Verwendungen gibt, die nicht optimiert werden können, ist diessizeof(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 zBvar3
keinen Einfluss auf den generierten Code. Aber selbst wenn es woanders verwendet wird,test()
würde es optimiert bleiben!Kurzum: Jede Nutzung von
Foo
wird 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.quelle
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.
quelle
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 Sie2*sizeof(int)
. Wenn Sie ein Array vonFoo
s erstellen, beträgt der Abstand zwischen den Anfängen zweier aufeinanderfolgender ObjekteFoo
immersizeof(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
offsetof
Makro). Darüber hinaus können Sie die byteweise Darstellung des Objekts überprüfen, indem Sie sie in ein Array mitchar
using kopierenstd::memcpy
. In all diesen Fällen kann beobachtet werden, dass das zweite Mitglied dort ist.quelle
gcc -fwhole-program -O3 *.c
kö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 Wertsizeof()
dieses Ziel hat, und weil es eine wirklich komplizierte Optimierung ist, die Programmierer von Hand durchführen sollten, wenn sie es wollen.)Die Beispiele anderer Antworten auf diese Frage, die elidieren,
var2
basieren auf einer einzigen Optimierungstechnik: konstante Ausbreitung und anschließende Elision der gesamten Struktur (nicht die Elision von gerechtvar2
). 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,var2
kann sie nicht entfernt werden. Soweit ich weiß, kann kein aktueller C / C ++ - Compiler Funktionen gemäß der Elision von spezialisierenvar2
. Wenn also die Struktur an eine nicht inline-Funktion übergeben oder von dieser zurückgegeben wird,var2
kann sie nicht entfernt werden.Bei verwalteten Sprachen wie C # / Java mit einem JIT-Compiler kann der Compiler möglicherweise sicher umgehen,
var2
da 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
var2
aus der Struktur entfernt werden, wenn die gesamte Strukturvariable entfernt wird. Für interessante Fälle der Elisionvar2
aus der Struktur lautet die Antwort: Nein.Einige zukünftige C / C ++ - Compiler können sich
var2
von der Struktur lösen , und das um die Compiler herum aufgebaute Ökosystem muss sich an die von Compilern generierten Elisionsinformationen anpassen.quelle
Dies hängt von Ihrem Compiler und seiner Optimierungsstufe ab.
Wenn Sie in gcc angeben
-O
, werden die folgenden Optimierungsflags aktiviert :-fdce
steht für Dead Code Elimination .Sie können verwenden
__attribute__((used))
, um zu verhindern, dass gcc eine nicht verwendete Variable mit statischem Speicher entfernt:quelle