Warum sind diese Funktionsaufrufe nicht optimiert?

8

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:

%Vor%     
AnArrayOfFunctions 08.03.2016, 12:52
quelle

2 Antworten

3

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).

    
gsg 08.03.2016 13:51
quelle
2

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 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.

gcc und clang sind jedoch deutlich schlechter als die beste legale Optimierung: siehe die Ausgabe auf godbolt .

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.

Sie benötigen keine Funktionszeiger, um dies zu reproduzieren; normale Funktionen mit Prototypen (und mehr beschreibenden Namen) demonstriert es immer noch.

%Vor%

gcc's Ausgabe für modifyarg ist urkomisch:

%Vor%

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 .

Die beste legale Version Ihrer Funktion

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.

    
Peter Cordes 08.03.2016 15:18
quelle

Tags und Links