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?
optimization
compiler
Aviv Cohn
quelle
quelle
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.Antworten:
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.
quelle
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.
quelle
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
fact
mit 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
CALL
Maschinenbefehl 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.quelle
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 ...
quelle
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.
quelle
Erstens gibt es einfache Beispiele, bei denen das Inlinen sehr schlecht funktioniert. Betrachten Sie diesen einfachen C-Code:
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.
quelle