Warum integrieren Compiler nicht alles? [geschlossen]

12

Manchmal rufen Compiler Inline-Funktionen auf. Das heißt, sie verschieben den Code der aufgerufenen Funktion in die aufrufende Funktion. Dies macht die Dinge etwas schneller, da es nicht notwendig ist, Dinge auf den Call-Stack zu schieben und von ihm zu entfernen.

Meine Frage ist also, warum Compiler nicht alles inline? Ich nehme an, es würde die ausführbare Datei deutlich schneller machen.

Der einzige Grund, den ich mir vorstellen kann, ist eine erheblich größere ausführbare Datei. Aber spielt es heutzutage wirklich eine Rolle, wenn Hunderte von GB Speicher zur Verfügung stehen? Lohnt sich die verbesserte Leistung nicht?

Gibt es einen anderen Grund, warum Compiler nicht alle Funktionsaufrufe einbinden?

Aviv Cohn
quelle
17
IDK über Sie, aber ich habe nicht Hunderte von GB Speicher nur herumliegen.
Ampt
2
Isn't the improved performance worth it?Bei einer Methode, die eine Schleife 100-mal ausführt und einige wichtige Zahlen zusammenfasst, ist der Aufwand für das Verschieben von 2 oder 3 Argumenten in CPU-Register gleich Null.
Doval
5
Sie sind zu generisch, bedeutet "Compiler" "alle Compiler" und bedeutet "alles" wirklich "alles"? Dann ist die Antwort einfach: Es gibt Situationen, in denen man einfach nicht inline kann. Rekursion kommt mir in den Sinn.
Otávio Décio
17
Die Cachelokalität ist viel wichtiger als der Aufwand für winzige Funktionsaufrufe.
SK-logic
3
Ist es heutzutage wirklich wichtig, die Leistung mit Hunderten von GFLOPS an Verarbeitungsleistung zu verbessern?
Mouviciel

Antworten:

22

Beachten Sie zunächst, dass ein wichtiger Effekt von Inline darin besteht, dass weitere Optimierungen am Anrufstandort vorgenommen werden können.

Für Ihre Frage: Es gibt Dinge, die sich nur schwer oder gar nicht inline stellen lassen:

  • dynamisch verknüpfte Bibliotheken

  • dynamisch ermittelte Funktionen (dynamischer Versand, durch Funktionszeiger aufgerufen)

  • rekursive Funktionen (Schwanzrekursion kann)

  • Funktionen, für die Sie nicht den Code haben (aber die Optimierung der Verknüpfungszeit ermöglicht dies für einige von ihnen)

Dann hat Inlining nicht nur positive Auswirkungen:

  • Größere ausführbare Dateien bedeuten mehr Speicherplatz und längere Ladezeit

  • Größere ausführbare Dateien bedeuten eine Erhöhung des Cache-Drucks (Beachten Sie, dass das Inlinen von Funktionen, die klein genug sind, wie z.

Und schließlich ist der Gewinn für Funktionen, deren Ausführung eine nicht triviale Zeit in Anspruch nimmt, den Schmerz einfach nicht wert.

Ein Programmierer
quelle
3
einige rekursive Anrufe können inlined (Endrekursion), aber alle können in Iteration umgewandelt werden , wenn Sie optional einen expliziten Stapel hinzufügen
Ratsche Freak
@ratchetfreak, Sie können auch einen rekursiven Nicht-Tail-Aufruf in Tail-One umwandeln. Aber das ist für mich im Bereich der "schwierigen" (vor allem, wenn Sie co-rekursive Funktionen haben oder dynamisch bestimmen müssen, wohin Sie springen müssen, um die Rückkehr zu simulieren), aber das ist nicht unmöglich (Sie haben nur ein Fortsetzungs-Framework eingerichtet und wenn man bedenkt, dass das Geschenk leichter wird).
Programmierer
11

Eine wesentliche Einschränkung ist der Laufzeitpolymorphismus. Wenn beim Schreiben ein dynamischer Versand stattfindet, kann foo.bar()der Methodenaufruf nicht eingebunden werden. Dies erklärt, warum Compiler nicht alles einbinden.

Rekursive Aufrufe können ebenfalls nicht einfach eingebunden werden.

Modulübergreifendes Inlining ist auch aus technischen Gründen schwierig durchzuführen (eine inkrementelle Neukompilierung wäre beispielsweise nicht möglich).

Compiler machen jedoch eine Menge Dinge inline.

Simon Bergot
quelle
3
Das Inlinen durch einen virtuellen Versand ist sehr schwierig, aber nicht unmöglich. Einige C ++ - Compiler sind unter bestimmten Umständen dazu in der Lage.
bstamour
2
... sowie einige JIT-Compiler (Devirtualisierung).
Frank
@bstamour Jeder halbwegs vernünftige Compiler einer beliebigen Sprache mit entsprechenden Optimierungen wird statisch einen Aufruf einer deklarierten virtuellen Methode für ein Objekt auslösen, dh devirtualisieren, dessen dynamischer Typ zur Kompilierungszeit bekannt ist. Dies kann das Inlining erleichtern, wenn die Devirtualisierungsphase vor der (oder einer anderen) Inliningphase stattfindet. Aber das ist trivial. Gab es noch etwas, was du meintest? Ich sehe nicht, wie ein tatsächliches "Inlining durch einen virtuellen Versand" erreicht werden kann. Um inline zu sein, muss man den statischen Typ kennen - dh devirtualisieren - so dass das Vorhandensein von Inlining bedeutet, dass es keinen virtuellen Versand gibt
underscore_d
9

Erstens können Sie nicht immer inline schreiben, z. B. können rekursive Funktionen nicht immer inline geschrieben werden (aber ein Programm, das eine rekursive Definition von factmit nur einem Ausdruck von enthält, fact(8)kann inline geschrieben werden).

Inlining ist dann nicht immer vorteilhaft. Wenn der Compiler so viel einfügt, dass der Ergebniscode groß genug ist, um seine heißen Teile nicht in z. B. den L1-Anweisungscache zu passen, ist er möglicherweise viel langsamer als die nicht einfügbare Version (die leicht in den L1-Cache passt) ... Außerdem führen neuere Prozessoren einen CALLMaschinenbefehl sehr schnell aus (zumindest zu einem bekannten Ort, dh einem direkten Aufruf, nicht einem Aufruf durch einen Zeiger).

Zum vollständigen Inlining gehört schließlich eine vollständige Programmanalyse. Dies ist möglicherweise nicht möglich (oder zu teuer). Mit C oder C ++, das von GCC kompiliert wurde (und auch mit Clang / LLVM ), müssen Sie die Optimierung der Verknüpfungszeit aktivieren (durch Kompilieren und Verknüpfen mit z. B. g++ -flto -O2), was ziemlich viel Kompilierungszeit in Anspruch nimmt.

Basile Starynkevitch
quelle
1
Für den Datensatz unterstützt LLVM / Clang (und mehrere andere Compiler) auch die Optimierung der Verbindungszeit .
Sie
Ich weiß das; LTO gab es im vorigen Jahrhundert (IIRC, zumindest in einigen proprietären MIPS-Compilern).
Basile Starynkevitch
7

So überraschend es auch sein mag, das Inlinen von allem verkürzt nicht unbedingt die Ausführungszeit. Die größere Größe Ihres Codes kann es der CPU erschweren, Ihren gesamten Code auf einmal im Cache zu behalten. Ein Cache-Fehler in Ihrem Code wird wahrscheinlicher und ein Cache-Fehler ist teuer. Dies ist weitaus schlimmer, wenn Ihre potenziell inline Funktionen groß sind.

Ich hatte von Zeit zu Zeit bemerkenswerte Leistungsverbesserungen, indem ich große Codestücke, die als "Inline" markiert waren, aus Header-Dateien genommen und in den Quellcode eingefügt habe, sodass sich der Code nur an einer Stelle und nicht an jeder Aufrufstelle befindet. Dann wird der CPU-Cache besser genutzt und Sie erhalten auch eine bessere Kompilierzeit ...

Tom Tanner
quelle
dies scheint nur wiederholen Sie die Punkte gemacht und erklärt in einer vor Antwort , die vor einer Stunde gepostet
gnat
1
Was für Caches? L1? L2? L3? Welches ist wichtiger?
Peter Mortensen
1

Alles zu inlinieren würde nicht nur einen erhöhten Speicherbedarf bedeuten, sondern auch einen erhöhten internen Speicherbedarf, der nicht so zahlreich ist. Denken Sie daran, dass der Code auch im Code-Segment gespeichert ist. Wenn eine Funktion an 10000 Stellen aufgerufen wird (z. B. aus Standardbibliotheken in einem relativ großen Projekt), belegt der Code für diese Funktion 10000-mal mehr internen Speicher.

Ein weiterer Grund könnten die JIT-Compiler sein. Wenn alles inline ist, müssen keine Hotspots dynamisch kompiliert werden.

m3th0dman
quelle
1

Erstens gibt es einfache Beispiele, bei denen das Inlinen sehr schlecht funktioniert. Betrachten Sie diesen einfachen C-Code:

void f1 (void) { printf ("Hello, world\n"); }
void f2 (void) { f1 (); f1 (); f1 (); f1 (); }
void f3 (void) { f2 (); f2 (); f2 (); f2 (); }
...
void f99 (void) { f98 (); f98 (); f98 (); f98 (); }

Ratet mal, was Inlining alles für euch bedeutet.

Als Nächstes gehen Sie davon aus, dass Inlining die Dinge schneller macht. Das ist manchmal der Fall, aber nicht immer. Ein Grund dafür ist, dass Code, der in den Anweisungscache passt, viel schneller ausgeführt wird. Wenn ich eine Funktion von 10 Stellen aus aufrufe, führe ich immer Code aus, der sich im Anweisungscache befindet. Wenn es inline ist, sind die Kopien überall und laufen viel langsamer.

Es gibt noch andere Probleme: Inlining erzeugt riesige Funktionen. Riesige Funktionen sind viel schwerer zu optimieren. Ich habe beträchtliche Fortschritte bei leistungskritischem Code erzielt, indem ich Funktionen in einer separaten Datei verstecke, um zu verhindern, dass der Compiler sie einfügt. Infolgedessen war der generierte Code für diese Funktionen viel besser, wenn sie ausgeblendet waren.

Übrigens. Ich habe keine "Hunderte von GB Speicher". Mein Arbeitscomputer verfügt nicht einmal über "Hunderte von GB Festplattenspeicher". Und wenn meine Anwendung "Hunderte von GB Speicher" enthält, dauert es 20 Minuten, um die Anwendung in den Speicher zu laden.

gnasher729
quelle