Comment JIT intègre notre code C # (heuristique)

L'intégration est l'une des optimisations les plus importantes des compilateurs. Il supprime non seulement les frais généraux de l'appel, mais ouvre également de nombreuses possibilités pour d'autres optimisations, par exemple, le pliage constant, l'élimination du code mort, etc. De plus, parfois l'inlining entraîne une diminution de la taille de la fonction appelante! J'ai demandé à plusieurs personnes si elles savaient par quelles règles les fonctions en C # sont intégrées, et la plupart ont répondu que le JIT examine la taille du code IL et n'inclut que les petites fonctions de la taille, par exemple, jusqu'à 32 octets. Par conséquent, j'ai décidé d'écrire ce message pour divulguer les détails de l'implémentation à l'aide d'un tel exemple, qui montrera plusieurs heuristiques en action à la fois:




Pensez-vous que l'appel au constructeur Volume sera en ligne ici? Évidemment pas. Il est trop volumineux, en particulier en raison du lancement de nouveaux opérateurs lourds , ce qui conduit à un codegen plutôt audacieux. Vérifions dans Disasmo:



Inline! De plus, toutes les exceptions et leurs branches ont été supprimées avec succès! Vous pouvez dire quelque chose dans le style de "Ah, d'accord, le jit est très intelligent et a fait une analyse complète de tous les candidats pour inline, regardé ce qui se passerait si vous passez des arguments spécifiques" ou "Le jit essaie d'inclure tout ce qui est possible, effectue toutes les optimisations, puis décide de manière rentable ou pas »(prendre la combinatoire et calculer la complexité de cette opération, par exemple, pour un graphe d'appel de dix ou deux méthodes).

Eh bien ... non, ce n'est pas réaliste, surtout en termes de juste à temps. Par conséquent, la plupart des compilateurs utilisent les soi-disant observations et heuristiques pour résoudre ce problème de sac à dos classique et tentent de déterminer leur propre budget et de s'y adapter aussi efficacement que possible (et non, PGO n'est pas une panacée). RyuJIT a des observations positives et négatives. Augmentation positive du coefficient de bénéfice (multiplicateur de bénéfice). Plus le coefficient est élevé, plus nous pouvons aligner de code. Observations négatives au contraire - abaisser ou même interdire l'incrustation. Voyons quelles observations RyuJIT a faites pour notre exemple:



Ces observations peuvent être vues dans les journaux de COMPlus_JitDump (par exemple, dans Disasmo):



Toutes ces observations simples ont augmenté le coefficient de 1,0à 11,5 et a aidé à surmonter avec succès le budget de l'inlineer, par exemple, le fait que nous passons un argument constant et qu'il est comparé à une autre constante nous dit qu'avec un degré élevé de probabilité après avoir réduit les constantes, l'une des branches de condition sera supprimée et le code deviendra plus petit. Ou, par exemple, le fait qu'il s'agit d'un constructeur et qu'il soit appelé à l'intérieur de la boucle est également un indice pour le jit qu'il devrait adoucir les exigences pour l'inline.

En plus du multiplicateur d'avantages, RyuJIT utilise également des observations pour prédire la taille du code de fonction native et son impact sur les performances à l'aide de constantes magiques dans EstimateCodeSize () et EstimatePerformanceImpact () obtenues à l'aide de ML.

Au fait, avez-vous remarqué cette astuce?:

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

Il s'agit d'une version optimisée pour:

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

Les deux expressions sont une seule et même chose, mais dans le premier cas, nous avons une unité de base et dans le second, il y en a trois. Il s'avère que l'inliner a une limite stricte sur le nombre de blocs de base dans la fonction et s'il dépasse 5, alors peu importe la taille de notre multiplicateur de bénéfices - l'inline est annulé. J'ai donc appliqué cette astuce pour répondre à cette exigence stricte. Ce serait génial si Roslyn le faisait pour moi.

Problème à Roslyn : github.com/dotnet/runtime/issues/13347
de PR dans RyuJIT (ma tentative maladroite): github.com/dotnet/coreclr/pull/27480

Là, j'ai décrit un exemple de la raison pour laquelle il est logique de faire non seulement dans Jit mais et dans le compilateur C #.

Méthodes intégrées et virtuelles


Tout est clair ici, vous ne pouvez pas aligner sur quoi il n'y a aucune information au stade de la compilation, bien que si le type ou la méthode est scellé, alors pourquoi pas .

Inclusion et levée d'exceptions


Si une méthode ne retourne jamais de valeur (par exemple, elle le fait juste throw new...), ces méthodes sont automatiquement marquées comme assistants de lancement et ne sont pas alignées. C'est une telle façon de balayer le codegen complexe throw newsous le tapis et d'apaiser la doublure.

Attribut Inlining et [AggressiveInlining]


Dans ce cas, vous recommandez que l' inline insère la méthode, mais vous devez être extrêmement prudent pour deux raisons:

  • Vous optimisez peut-être un cas et aggravez tous les autres (par exemple, améliorez le cas des arguments constants) par la taille du codegen.
  • L'inline génère souvent un grand nombre de variables temporaires qui peuvent dépasser une certaine limite - le nombre de variables dont RyuJIT peut suivre le cycle de vie (512) et après cela, le code commencera à se transformer en terribles déversements sur la pile et à ralentir considérablement. Deux bons exemples: tyts et tyts .

Méthodes intégrées et dynamiques


Actuellement, ces méthodes ne s'alignent pas et ne s'alignent pas elles-mêmes: github.com/dotnet/runtime/issues/34500

Ma tentative d'écrire mon heuristique


Récemment, j'ai essayé d'écrire votre propre heuristique pour aider ici une telle occasion:



Dans un dernier post, j'ai mentionné que j'avais récemment optimisé le calcul RyuJIT de la longueur des chaînes constantes ( "Hello".Length -> 5nous voyons que si zainlaynit), et ainsi, dans l'exemple ci-dessus ^ Validatein Test, nous obtenons if ("hello".Length > 10)ce qui est optimisé dans if (5 > 10)ce qui est optimisé dans la suppression de la condition / branche entière. Cependant, l'inline a refusé de s'aligner Validate:



Et le principal problème ici est qu'il n'y a pas encore d'heuristique qui indique au jit que nous transmettons une chaîne constante à System.String::get_Length, ce qui signifie que l'appel callvirt se réduira très probablement en une constante et la branche entière sera supprimée. En fait, mon heuristiqueet ajoute cette observation (le seul inconvénient est que vous devez résoudre tous les appels, ce qui n'est pas très rapide).

Il existe d'autres restrictions, dont une liste peut être trouvée en général ici . Et ici, vous pouvez lire les réflexions d'un des principaux développeurs JIT sur la conception de l'inliner et son article sur l'utilisation du Machine Learning pour ce cas.

All Articles