Ich habe versucht, diesen Code sowohl mit Clang als auch mit GCC zu kompilieren:
%Vor% Das Ergebnis ist das gleiche. Obwohl der Aufruf von pF
nicht erlaubt ist, sein einziges Argument zu ändern, wird das Objekt a
für den zweiten Aufruf nach pF1
kopiert. Warum ist das?
Hier ist die Assembly-Ausgabe (aus GCC):
%Vor% Kann der Optimierer das nicht sehen, da die Funktion, auf die pF
zeigt, seinen Parameter nicht ändern kann (wie es const
deklariert wird) und den letzten Kopiervorgang weglässt? Kürzlich habe ich auch gesehen, dass die Variable a
nicht weiter im Code gelesen wird, sondern ihren Speicher für die Funktionsargumente.
Derselbe Code kann wie folgt geschrieben werden:
%Vor% Ich kompiliere mit -O3
flag. Fehle ich etwas?
Es ist das gleiche, auch wenn ich UB nicht aufruft (da die Funktionszeiger standardmäßig NULL
sind) und ich initialisiere sie stattdessen wie folgt:
Ich glaube nicht, dass diese Optimierung legal ist. Was Sie übersehen, ist, dass ein Funktionstyp mit einem const-Argument mit einem Funktionstyp mit einem nichtkonstanten Argument kompatibel ist, sodass eine Funktion, die das Argument mutiert, dem Zeiger pF
zugeordnet werden kann.
Hier ist ein Beispielprogramm:
%Vor%Die untere Zeile besagt, dass eine Const-Annotation für das Argument dem Compiler keine zuverlässige Information darüber liefert, ob der Argumentspeicher vom Angerufenen mutiert wurde. Die Durchführung dieser Optimierung scheint zu erfordern, dass der ABI so ist, dass der Argumentspeicher nicht mutiert werden muss (oder irgendeine Art von Programmanalyse, aber egal).
Ich denke, die Funktion muss immer noch eine Kopie machen (siehe das Ende für das, was ich für die optimale zulässige Version halte). Der Rest sind (mehr oder weniger verständliche) Optimierungsfehler.
Der SysV x86-64 ABI garantiert nicht, dass eine Funktion seinen Stack nicht ändert -args. Es sagt nichts über const
aus. Alles, was es nicht garantiert, kann nicht angenommen werden. Es besagt nur, dass große Objekte, die nach Wert übergeben werden, auf dem Stapel abgelegt werden. nichts über den Zustand, wenn die aufgerufene Funktion zurückkehrt. Der Angerufene "besitzt" seine Argumente, auch wenn sie als const
deklariert sind. Siehe auch die x86 Wiki, aber das ABI Dokument selbst ist der einzige Link in das Wiki, das wirklich relevant ist.
Gleichermaßen können schmale Integer-Typen in Registern mit Abfall in den hohen Bits als Argumente oder Rückgabewerte vorkommen. Der ABI sagt auch nicht explizit etwas, daher gibt es keine Garantie dafür, dass hohe Bits auf Null gesetzt werden. Dies ist in der Tat, was gcc tut: Es wird davon ausgegangen, dass beim Empfang von Werten ein hoher Müll vorhanden ist und beim Übergeben von Werten ein hoher Müll übrig bleibt. Gleiches gilt für float / double in xmm regs. Ich habe dies vor kurzem bei einem der ABI-Betreuer bestätigt, während ich einen unsicheren Code untersuchte, der durch Klänge erzeugt wurde. Ich bin mir also sicher, dass die richtige Interpretation ist, dass Sie nichts annehmen sollten, was nicht ausdrücklich von der ABI garantiert wird .
gcc macht das nicht, aber ich glaube, dass es für eine aufgerufene Funktion wie diese legal wäre, keine Kopie zu machen:
%Vor% Speichern Sie stattdessen einfach in arg und jmp pFconstval
.
Meine Vermutung ist, dass es eine verpasste Optimierung ist, anstatt dass gcc und clang in ihrer Interpretation des Standards konservativ sind.
Es scheint, dass gcc und clang bei der Optimierung von Kopien für Objekte, die zu groß sind, um in ein Register zu passen, keine gute Arbeit leisten. Quellcode, der sie gar nicht erst kopiert hat, wäre sogar besser als der beste Job, den der Compiler damit machen könnte (zB by const *
oder C ++ const-reference), da ich nicht glaube, dass Ihre vorgeschlagene Optimierung ist legal.
Seltsames Ding: Mit -march=haswell
(oder einer anderen Intel-CPU) gibt gcc einen Funktionsaufruf an memcpy statt an rep movsq
Inline-Code aus. Ich verstehe es nicht. Dies geschieht sogar mit -ffreestanding
/ -nostdlib
IDK, wenn irgendjemand anders dachte, dass rdi
ein Zeiger auf den Speicher war, d. h. dass er von einer unsichtbaren Referenz übergeben wurde. Es hat ewig gedauert, bis die Call-by-Value-Funktionen keinerlei Parameter in den Registern übernommen haben. Ich hielt es für seltsam, dass rep movsq
links rdi
auf die hohe Kopie zeigte.
gcc's Ausgabe für modifyarg
ist urkomisch:
Die Kopie wird auch dann ausgeführt, wenn Sie x
nicht ändern. Clang erstellt eine tatsächliche Kopie an einem anderen Ort vor dem Rückruf jmp
.
wie ich die ABI verstehe:
%Vor% Übrigens, gcc's Verwendung von rbx
ist albern. Es speichert vier Code-Bytes:
push
/ pop
: 2 Bytes. mov rbx, rsp
: 3B. 2x mov rsi, rbx
: 2x3B. Gesamt = 12B
Ersetzen Sie all das mit 2x lea rsi, [rsp+208]
: 2x 8B. Gesamt = 16B.
Es vermeidet nicht den zusätzlichen Stack-Engine-Syncup, da auch mov rdi, rsp
verwendet wird. 4B Code ist es nicht wert, 3 Ups auszugeben. In meiner Version, die nur einmal kopiert (und nur eine LEA benötigt), ist es auch ein Verlust in Code-Bytes.
Tags und Links optimization c gcc assembly clang