Wie JIT unseren C # -Code einfügt (Heuristik)

Inlining ist eine der wichtigsten Optimierungen in Compilern. Es entfernt nicht nur den Overhead aus dem Aufruf, sondern eröffnet auch viele Möglichkeiten für andere Optimierungen, z. B. ständiges Falten, Eliminieren von totem Code usw. Darüber hinaus führt Inlining manchmal zu einer Verringerung der Größe der aufrufenden Funktion! Ich fragte mehrere Leute, ob sie wüssten, nach welchen Regeln die Funktionen in C # inline sind, und die meisten antworteten, dass die JIT die Größe des IL-Codes betrachtet und nur kleine Funktionen mit einer Größe von beispielsweise bis zu 32 Bytes inline. Aus diesem Grund habe ich beschlossen, diesen Beitrag zu schreiben, um anhand eines solchen Beispiels Implementierungsdetails offenzulegen, in denen mehrere Heuristiken gleichzeitig in Aktion gezeigt werden:




Denken Sie, dass der Aufruf des Volume- Konstruktors hier eingefügt wird ? Offensichtlich nicht. Es ist zu groß, vor allem wegen des Schwergewichts werfen neue Operatoren, die zu einem ziemlich kühnen Codegen führen. Lassen Sie uns in Disasmo einchecken:



Inline! Außerdem wurden alle Ausnahmen und deren Zweige erfolgreich gelöscht! Sie können etwas im Stil von "Ah, okay, der Jit ist sehr klug und hat eine vollständige Analyse aller Kandidaten für Inline durchgeführt, hat nachgesehen, was passieren würde, wenn Sie bestimmte Argumente übergeben" oder "Der Jit versucht, alles Mögliche inline zu machen, führt alle Optimierungen durch und entscheidet dann profitabel." it or not “(Kombinatorik aufgreifen und die Komplexität dieser Operation berechnen, z. B. für ein Aufrufdiagramm mit zehn oder zwei Methoden).

Nun ... nein, das ist unrealistisch, besonders in Bezug auf die Zeit. Daher verwenden die meisten Compiler die sogenannten Beobachtungen und Heuristiken, um dieses klassische Rucksackproblem zu lösen und versuchen, ihr eigenes Budget zu bestimmen und so effizient wie möglich hinein zu passen (und nein, PGO ist kein Allheilmittel). RyuJIT hat positive und negative Beobachtungen. Positiv den Leistungskoeffizienten erhöhen (Leistungsmultiplikator). Je höher der Koeffizient, desto mehr Code können wir inline. Negative Beobachtungen im Gegenteil - senken Sie es oder verbieten Sie sogar das Inlining. Mal sehen, welche Beobachtungen RyuJIT für unser Beispiel gemacht hat:



Diese Beobachtungen sind in den Protokollen von COMPlus_JitDump (zum Beispiel in Disasmo) zu sehen:



Alle diese einfachen Beobachtungen haben den Koeffizienten von 1,0 erhöhtbis 11.5 und hat dazu beigetragen, das Budget des Inlineers erfolgreich zu überwinden. Die Tatsache, dass wir ein konstantes Argument übergeben und es mit einer anderen Konstante verglichen wird, zeigt, dass mit hoher Wahrscheinlichkeit nach dem Kollabieren der Konstanten einer der Bedingungszweige gelöscht wird und der Code kleiner wird. Oder zum Beispiel ist die Tatsache, dass dies ein Konstruktor ist und innerhalb der Schleife aufgerufen wird, auch ein Hinweis darauf, dass die Anforderungen für das Inlining gemildert werden sollten.

Zusätzlich zum Vorteilsmultiplikator verwendet RyuJIT Beobachtungen, um die Größe des nativen Funktionscodes und seine Auswirkungen auf die Leistung mithilfe magischer Konstanten in EstimateCodeSize () und EstimatePerformanceImpact () vorherzusagen, die mit ML erhalten wurden.

Übrigens, haben Sie diesen Trick bemerkt?

if ((value - 'A') > ('Z' - 'A'))

Dies ist eine optimierte Version für:

if (value < 'A' || value > 'Z')

Beide Ausdrücke sind ein und dasselbe, aber im ersten Fall haben wir eine Basiseinheit und im zweiten drei. Es stellt sich heraus, dass der Inliner die Anzahl der Basisblöcke in der Funktion streng begrenzt. Wenn er 5 überschreitet, spielt es keine Rolle, wie groß unser Vorteilsmultiplikator ist - Inlining wird abgebrochen. Also habe ich diesen Trick angewendet, um in diese strenge Anforderung zu passen. Es wäre großartig, wenn Roslyn es für mich tun würde. Problem

in Roslyn : github.com/dotnet/runtime/issues/13347
von PR in RyuJIT (mein umständlicher Versuch): github.com/dotnet/coreclr/pull/27480

Dort habe ich ein Beispiel beschrieben, warum es sinnvoll ist, nicht nur in Jit, sondern auch zu tun und im C # -Compiler.

Inlining und virtuelle Methoden


Hier ist alles klar, Sie können nicht inline setzen, worüber es in der Kompilierungsphase keine Informationen gibt. Wenn jedoch der Typ oder die Methode versiegelt ist, warum nicht ?

Ausnahmen einfügen und werfen


Wenn eine Methode niemals einen Wert zurückgibt (zum Beispiel nur throw new...), werden solche Methoden automatisch als Wurfhelfer markiert und nicht inline. Auf diese Weise wird das komplexe Codegen throw newunter dem Teppich hervorgefegt und der Inliner besänftigt.

Inlining- und [AggressiveInlining] -Attribut


In diesem Fall empfehlen Sie dem Inlineer, die Methode zu integrieren, aber Sie müssen aus zwei Gründen äußerst vorsichtig sein:

  • Vielleicht optimieren Sie einen Fall und verschlechtern alle anderen (z. B. verbessern Sie den Fall konstanter Argumente) um die Größe des Codegens.
  • Inlining generiert häufig eine große Anzahl temporärer Variablen, die einen bestimmten Grenzwert überschreiten können - die Anzahl der Variablen, deren Lebenszyklus RyuJIT verfolgen kann (512), und danach wird der Code zu schrecklichen Verschmutzungen auf dem Stapel und verlangsamt sich erheblich. Zwei gute Beispiele: Tyts und Tyts .

Inlining und dynamische Methoden


Derzeit sind solche Methoden nicht inline und inline selbst nicht: github.com/dotnet/runtime/issues/34500

Mein Versuch, meine Heuristik zu schreiben


Kürzlich habe ich versucht, Ihre eigenen Heuristiken zu schreiben, um hier bei einer solchen Gelegenheit zu helfen:



In einem letzten Beitrag habe ich erwähnt, dass ich kürzlich die RyuJIT-Berechnung der Länge der konstanten Zeichenfolgen optimiert habe ( "Hello".Length -> 5wir sehen, dass wenn zainlaynit), und so erhalten wir im obigen Beispiel ^ Validatein Was ist in dem optimiert, was in der Entfernung des gesamten Zustands / Zweigs optimiert ist? Der Inliner weigerte sich jedoch zu inline : Und das Hauptproblem hierbei ist, dass es noch keine Heuristik gibt , die dem JIT mitteilt, dass wir eine konstante Zeichenfolge übergeben , was bedeutet, dass der Callvirt-Aufruf höchstwahrscheinlich zu einer Konstanten zusammenbricht und der gesamte Zweig gelöscht wird. Eigentlich meine HeuristikTestif ("hello".Length > 10)if (5 > 10)Validate



System.String::get_Lengthund fügt diese Beobachtung hinzu (das einzige Minus ist, dass Sie alle Callvirts auflösen müssen, was nicht sehr schnell ist).

Es gibt andere Einschränkungen, von denen eine Liste hier allgemein zu finden ist . Und hier können Sie die Gedanken eines der wichtigsten JIT-Entwickler über das Design des Inliners und seinen Artikel über die Verwendung von maschinellem Lernen für diesen Fall lesen .

All Articles