Runtime: Introduire un JIT à plusieurs niveaux

Créé le 14 avr. 2016  ·  63Commentaires  ·  Source: dotnet/runtime

Pourquoi le .NET JIT n'est-il pas hiérarchisé ?

Le JIT a deux objectifs de conception principaux : temps de démarrage rapide et débit élevé en régime permanent.

Au début, ces objectifs semblent contradictoires. Mais avec une conception JIT à deux niveaux, ils sont tous deux réalisables :

  1. Tout le code commence par être interprété. Cela se traduit par un temps de démarrage extrêmement rapide (plus rapide que RyuJIT). Exemple : La méthode Main est presque toujours froide et la jitter est une perte de temps.
  2. Le code qui s'exécute souvent est jitté à l'aide d'un générateur de code de très haute qualité. Très peu de méthodes seront chaudes (1% ?). Par conséquent, le débit du JIT de haute qualité n'a pas beaucoup d'importance. Il peut passer autant de temps qu'un compilateur C à générer un très bon code. De plus, il peut _supposer_ que le code est chaud. Il peut s'aligner comme un fou et dérouler des boucles. La taille du code n'est pas un problème.

Atteindre cette architecture ne semble pas trop coûteux :

  1. Ecrire un interprète semble bon marché par rapport à un JIT.
  2. Un générateur de code de haute qualité doit être créé. Il peut s'agir de VC ou du projet LLILC.
  3. Il doit être possible de faire la transition entre le code en cours d'exécution interprété et le code compilé. C'est possible; la JVM le fait. C'est ce qu'on appelle le remplacement de pile (OSR).

Cette idée est-elle poursuivie par l'équipe JIT ?

.NET fonctionne sur des centaines de millions de serveurs. J'ai l'impression qu'il reste beaucoup de performances sur la table et que des millions de serveurs sont gaspillés pour les clients à cause d'une génération de code sous-optimale.

catégorie:débit
thème : gros paris
niveau de compétence : expert
coût: extra-large

area-CodeGen-coreclr enhancement optimization tenet-performance

Commentaire le plus utile

Avec les PR récents (https://github.com/dotnet/coreclr/pull/17840, https://github.com/dotnet/sdk/pull/2201), vous avez également la possibilité de spécifier une compilation à plusieurs niveaux en tant que runtimeconfig. propriété json ou une propriété de projet msbuild. L'utilisation de cette fonctionnalité nécessitera que vous soyez sur des versions très récentes alors que la variable d'environnement existe depuis un certain temps.

Tous les 63 commentaires

@GSPP La hiérarchisation est un sujet constant dans les conversations de planification. J'ai l'impression que c'est une question de _quand_, pas de _si_, si cela apporte du réconfort. Quant à _pourquoi_ ce n'est pas déjà là, je pense que c'est parce que, historiquement, les gains potentiels perçus ne justifiaient pas les ressources de développement supplémentaires nécessaires pour gérer la complexité et le risque accrus de plusieurs modes codegen. Je devrais vraiment laisser les experts en parler, alors je vais les ajouter.

/cc @dotnet/jit-contrib @russellhadley

D'une certaine manière, je doute que cela soit toujours pertinent dans un monde de crossgen/ngen, Ready to Run et corert.

Aucun de ceux-ci n'offre un débit élevé en régime permanent pour le moment, ce qui est important pour la plupart des applications Web. Si jamais ils le font, j'en suis satisfait car personnellement, je ne me soucie pas du temps de démarrage.

Mais jusqu'à présent, tous les générateurs de code pour .NET ont essayé de faire un équilibre impossible entre les deux objectifs, ne remplissant ni l'un ni l'autre très bien. Débarrassons-nous de cet exercice d'équilibre afin que nous puissions passer les optimisations à 11.

Mais jusqu'à présent, tous les générateurs de code pour .NET ont essayé de faire un équilibre impossible entre les deux objectifs, ne remplissant ni l'un ni l'autre très bien. Débarrassons-nous de cet exercice d'équilibre afin que nous puissions passer les optimisations à 11.

Je suis d'accord, mais réparer cela ne nécessite pas des choses comme un interprète. Juste un bon compilateur crossgen, que ce soit un meilleur RyuJIT ou LLILC.

Je pense que le plus grand avantage concerne les applications qui doivent générer du code au moment de l'exécution. Ceux-ci incluent les langages dynamiques et les conteneurs de serveur.

Il est vrai que le code généré dynamiquement est une motivation - mais il est également vrai qu'un compilateur statique n'aura jamais accès à toutes les informations disponibles au moment de l'exécution. De plus, même lorsqu'il spécule (par exemple sur la base d'informations de profil), il est beaucoup plus difficile pour un compilateur statique de le faire en présence d'un comportement modal ou externe dépendant du contexte.

Les applications Web ne doivent pas nécessiter de traitement de type ngen. Il ne s'intègre pas bien dans le pipeline de déploiement. Cela prend beaucoup de temps pour ngen big binary (même si presque tout le code est dynamiquement mort ou froid).

De plus, lors du débogage et du test d'une application Web, vous ne pouvez pas compter sur ngen pour vous offrir des performances réalistes.

De plus, je rejoins le point de Carol d'utiliser des informations dynamiques. Le niveau d'interprétation peut profiler le code (branches, nombre de boucles, cibles de répartition dynamique). C'est un match parfait ! Collectez d'abord le profil, puis optimisez.

La hiérarchisation résout tout dans chaque scénario pour toujours. En gros :) Cela peut en fait nous amener à la promesse des JIT : atteindre des performances _au-delà de ce qu'un compilateur C peut faire.

L'implémentation actuelle de RyuJIT telle qu'elle est actuellement est suffisante pour un niveau 1... La question est la suivante : serait-il judicieux d'avoir un JIT d'optimisation extrême de niveau 2 pour les chemins chauds qui peuvent s'exécuter après coup ? Essentiellement lorsque nous détectons ou avons suffisamment d'informations d'exécution pour savoir que quelque chose est chaud ou lorsqu'on nous demande de l'utiliser à la place dès le départ.

RyuJIT est de loin assez bon pour être le niveau 1. Le problème avec cela est qu'un interprète aurait un temps de démarrage _loin_ plus rapide (à mon avis). Le deuxième problème est que pour passer au niveau 2, l'état local d'exécution du code de niveau 1 doit être transférable au nouveau code de niveau 2 (OSR). Cela nécessite des modifications RyuJIT. L'ajout d'un interprète serait, je pense, un chemin moins cher avec une meilleure latence de démarrage en même temps.

Une variante encore moins chère consisterait à ne pas remplacer le code en cours d'exécution par un code de niveau 2. Au lieu de cela, attendez que le code de niveau 1 revienne naturellement. Cela peut être un problème si le code entre dans une longue boucle à chaud. Il n'atteindra jamais les performances de niveau 2 de cette façon.

Je pense que ce ne serait pas trop mal et pourrait être utilisé comme stratégie v1. Des idées d'atténuation sont disponibles, comme un attribut marquant une méthode comme chaude (cela devrait exister de toute façon même avec la stratégie JIT actuelle).

@GSPP C'est vrai, mais cela ne signifie pas que vous ne le sauriez pas lors de la prochaine exécution. Si le code et l'instrumentation Jitted deviennent persistants, alors la deuxième exécution vous obtiendrez toujours du code de niveau 2 (au détriment d'un certain temps de démarrage) --- ce qui, pour une fois, je m'en fiche personnellement car j'écris principalement du code serveur.

Ecrire un interprète semble bon marché par rapport à un JIT.

Au lieu d'écrire un tout nouvel interpréteur, serait-il judicieux d'exécuter RyuJIT avec les optimisations désactivées ? Cela améliorerait-il suffisamment le temps de démarrage ?

Un générateur de code de haute qualité doit être créé. Cela pourrait être VC

Parlez-vous de C2, le backend Visual C++ ? Ce n'est pas multiplateforme ni open source. Je doute que la fixation des deux se produise de si tôt.

Bonne idée avec la désactivation des optimisations. Le problème de l'OSR demeure cependant. Vous ne savez pas à quel point il est difficile de générer du code permettant au runtime de dériver l'état architectural IL (locaux et pile) au moment de l'exécution à un point sûr, de le copier dans le code jitted de niveau 2 et de reprendre l'exécution de niveau 2 à mi-fonction. La JVM le fait mais qui sait combien de temps il a fallu pour l'implémenter.

Oui, je parlais de C2. Je pense me souvenir qu'au moins un des Desktop JIT est basé sur le code C2. Ne fonctionne probablement pas pour CoreCLR mais peut-être pour Desktop. Je suis sûr que Microsoft est intéressé à avoir des bases de code alignées, donc c'est probablement sorti. LLVM semble être un excellent choix. Je pense que plusieurs langages sont actuellement intéressés à faire fonctionner LLVM avec les GC et avec les runtimes gérés en général.

LLVM semble être un excellent choix. Je pense que plusieurs langages sont actuellement intéressés à faire fonctionner LLVM avec les GC et avec les runtimes gérés en général.

Un article intéressant sur ce sujet : Apple a récemment déplacé le dernier niveau de son JavaScript JIT loin de LLVM : https://webkit.org/blog/5852/introducing-the-b3-jit-compiler/ . Nous rencontrerions probablement des problèmes similaires à ceux qu'ils ont rencontrés : des temps de compilation lents et le manque de connaissance de LLVM du langage source.

10 fois plus lent que RyuJIT serait tout à fait acceptable pour un 2e niveau.

Je ne pense pas que le manque de connaissance du langage source (qui est un vrai souci) soit inhérent à l'architecture de LLVM. Je pense que plusieurs équipes sont occupées à faire passer LLVM dans un état où la connaissance de la langue source peut être utilisée plus facilement. _Tous_ les langages de haut niveau non-C ont ce problème lors de la compilation sur LLVM.

Le projet WebKIT FTL/B3 est dans une position plus difficile à réussir que .NET car il doit exceller lors de l'exécution de code qui _au total_ consomme quelques centaines de millisecondes de temps, puis se termine. C'est la nature des charges de travail JavaScript qui pilotent les pages Web. .NET n'est pas à cet endroit.

@GSPP Je suis sûr que vous connaissez probablement LLILC . Sinon, jetez un oeil.

Nous travaillons depuis un certain temps sur le support LLVM pour les concepts CLR et avons investi dans les améliorations EH et GC. Encore un peu plus à faire sur les deux. Au-delà de cela, il y a une quantité inconnue de travail pour que les optimisations fonctionnent correctement en présence de GC.

LLILC semble au point mort. Est-ce?
Le 18 avril 2016 à 19h32, "Andy Ayers" [email protected] a écrit :

@GSPP https://github.com/GSPP Je suis sûr que vous connaissez probablement LLILC
https://github.com/dotnet/llilc. Sinon, jetez un oeil.

Nous travaillons depuis un certain temps sur le support LLVM pour les concepts CLR et avons
investi dans les améliorations EH et GC. Encore un peu plus à faire sur
les deux. Au-delà de cela, il y a une quantité inconnue de travail pour obtenir des optimisations
fonctionne correctement en présence de GC.


Vous recevez ceci parce que vous avez commenté.
Répondez directement à cet e-mail ou consultez-le sur GitHub
https://github.com/dotnet/coreclr/issues/4331#issuecomment -211630483

@drbo - LLILC est en veilleuse pour le moment - l'équipe MS s'est concentrée sur l'obtention de plus de cibles dans RyuJIT ainsi que sur la résolution des problèmes qui surviennent lorsque CoreCLR conduit à la publication et cela prend à peu près tout notre temps. C'est sur ma liste TODO (pendant mon temps libre abondant) de rédiger un article sur les leçons apprises basé sur le chemin parcouru (actuellement) avec LLILC, mais je n'y suis pas encore arrivé.
En ce qui concerne la hiérarchisation, ce sujet a suscité de nombreuses discussions au fil des ans. Je pense qu'étant donné certaines des nouvelles charges de travail, ainsi que le nouvel ajout d'images versionnables prêtes à l'emploi, nous allons jeter un nouveau regard sur comment et où hiérarchiser.

@russellhadley avez-vous eu du temps libre pour écrire le message ?

Je suppose qu'il devrait y avoir quelque chose à propos des emplacements de pile non promus et groots cassant les optizations et ralentissant le temps de jitting... Je devrais mieux jeter un coup d'œil au code du projet.

Je me demande également s'il est possible et rentable de sauter directement dans SelectionDAG et d'effectuer une partie du backend LLVM. Au moins un judas et une propagation de copie ... si par exemple la promotion gcroot vers les registres est prise en charge dans LLILC

Je suis curieux de connaître le statut de LLILC, y compris les goulots d'étranglement actuels et comment il se comporte par rapport à RyuJIT. LLVM étant un compilateur "de puissance industrielle" à part entière, il devrait disposer d'une grande richesse d'optimisations disponibles pour OSS. Il y a eu des discussions sur une sérialisation/désérialisation plus efficace et plus rapide du format bitcode sur la liste de diffusion ; Je me demande si c'est une chose utile pour LLILC.

Y a-t-il eu d'autres réflexions à ce sujet? @russellhadley CoreCLR a été publié et RyuJIT a été porté sur (au moins) x86 - quelle est la prochaine étape de la feuille de route ?

Voir dotnet/coreclr#10478 pour les débuts de travail à ce sujet.

Aussi dotnet/coreclr#12193

@noahfalk , pourriez-vous s'il vous plaît fournir un moyen de dire au runtime de forcer une compilation de niveau 2 immédiatement à partir du code géré lui-même? La compilation à plusieurs niveaux est une très bonne idée pour la plupart des cas d'utilisation, mais je travaille sur un projet où le temps de démarrage n'est pas pertinent mais où le débit et une latence stable sont essentiels.

Du haut de ma tête, cela pourrait être soit:

  • un nouveau paramètre dans le fichier de configuration, un commutateur comme <gcServer enabled="true" /> pour forcer le JIT à toujours sauter le niveau 1
  • ou quelque chose comme RuntimeHelpers.PrepareMethod , qui serait appelé par le code sur toutes les méthodes qui font partie du hot path (nous l'utilisons pour pré-JIT notre code au démarrage). Cela a l'avantage de donner un plus grand degré de liberté au développeur qui doit savoir quel est le chemin chaud. Une surcharge supplémentaire de cette méthode serait très bien.

Certes, peu de projets en bénéficieraient, mais je suis un peu inquiet par les optimisations de saut JIT par défaut, et je ne suis pas en mesure de lui dire que je préférerais qu'il optimise fortement mon code à la place.

Je sais que vous avez écrit ce qui suit dans le document de conception :

Ajoutez une nouvelle étape de pipeline de construction accessible à partir des API de code géré pour faire du code auto-modifiable.

Ce qui semble très intéressant 😁 mais je ne suis pas sûr que cela couvre ce que je demande ici.


Aussi une question connexe : quand la deuxième passe JIT entrerait-elle en vigueur ? Quand une méthode va-t-elle être appelée pour la n ième fois ? Le JIT se produira-t-il sur le thread sur lequel la méthode était censée s'exécuter ? Si tel est le cas, cela introduirait un délai avant l'appel de la méthode. Si vous implémentez des optimisations plus agressives, ce délai sera plus long que le temps JIT actuel, ce qui peut devenir un problème.

Cela devrait se produire lorsque la méthode est appelée suffisamment de fois, ou si une boucle
exécute suffisamment d'itérations (remplacement sur scène). Cela devrait arriver
de manière asynchrone sur un thread d'arrière-plan.

Le 29 juin 2017 à 19h01, "Lucas Trzesniewski" [email protected]
a écrit:

@noahfalk https://github.com/noahfalk , pourriez-vous s'il vous plaît fournir un moyen
pour dire au runtime de forcer une compilation de niveau 2 immédiatement à partir du
code managé lui-même ? La compilation à plusieurs niveaux est une très bonne idée pour la plupart
cas d'utilisation, mais je travaille sur un projet où le temps de démarrage n'est pas pertinent
mais un débit et une latence stable sont essentiels.

Du haut de ma tête, cela pourrait être soit:

  • un nouveau paramètre dans le fichier de configuration, un commutateur comme enabled="true" /> pour forcer le JIT à toujours sauter le niveau 1
  • ou quelque chose comme RuntimeHelpers.PrepareMethod, qui serait
    appelé par le code sur toutes les méthodes qui font partie du hot path (nous sommes
    en utilisant ceci pour pré-JIT notre code au démarrage). Ceci a l'avantage de
    donner une plus grande liberté au développeur qui doit savoir ce que
    le chemin chaud est. Une surcharge supplémentaire de cette méthode serait très bien.

Certes, peu de projets en bénéficieraient, mais je suis un peu inquiet
les optimisations de saut JIT par défaut, et je ne peux pas le dire
Je préférerais qu'il optimise fortement mon code à la place.

Je sais que vous avez écrit ce qui suit dans le document de conception :

Ajouter une nouvelle étape de pipeline de construction accessible à partir des API de code géré à faire
code auto-modifiable.

Ce qui semble très intéressant 😁 mais je ne suis pas sûr que cela couvre ce que

Je demande ici.

Aussi une question connexe : quand la deuxième passe JIT entrerait-elle en vigueur ? Lorsqu'un
méthode va être appelée pour la n ième fois ? Le JIT aura-t-il lieu le
le thread sur lequel la méthode était censée s'exécuter ? Si tel est le cas, cela introduirait un
délai avant l'appel de la méthode. Si vous implémentez plus agressif
optimisations, ce délai serait plus long que le temps JIT actuel, ce qui
peut devenir un problème.


Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/dotnet/coreclr/issues/4331#issuecomment-312130920 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AGGWB2WbZ2qVBjRIQWS86MStTSa1ODfoks5sJCzOgaJpZM4IHWs8
.

@ltrzesniewski - Merci pour les commentaires ! J'espère certainement que la compilation à plusieurs niveaux est utile pour la grande majorité des projets, mais les compromis peuvent ne pas être idéaux pour tous les projets. J'ai spéculé que nous laisserions une variable d'environnement en place pour désactiver le jitting à plusieurs niveaux, auquel cas vous conserverez le comportement d'exécution que vous avez maintenant avec une qualité supérieure (mais plus lente à générer) jitting à l'avant. La définition d'une variable d'environnement est-elle raisonnable pour votre application ? D'autres options sont également possibles, je gravite juste autour de la variable d'environnement car c'est l'une des options de configuration les plus simples que nous puissions utiliser.

Aussi une question connexe : quand la deuxième passe JIT entrerait-elle en vigueur ?

C'est une politique qui est très susceptible d'évoluer dans le temps. L'implémentation actuelle du prototype utilise une politique simpliste : "La méthode a-t-elle été appelée >= 30 fois"
https://github.com/dotnet/coreclr/blob/master/src/vm/tieredcompilation.cpp#L89
https://github.com/dotnet/coreclr/blob/master/src/vm/tieredcompilation.cpp#L122

Idéalement, cette politique très simple suggère une belle amélioration des performances de ma machine, même si ce n'est qu'une supposition. Afin de créer de meilleures politiques, nous devons obtenir des commentaires sur l'utilisation dans le monde réel, et obtenir ces commentaires nécessitera que les mécanismes de base soient raisonnablement robustes dans une variété de scénarios. Donc, mon plan est d'améliorer d'abord la robustesse/la compatibilité, puis de faire plus d'exploration pour la politique de réglage.

@DemiMarie - Nous n'avons rien qui suive les itérations de boucle dans le cadre de la politique actuelle, mais c'est une perspective intéressante pour l'avenir.

Y a-t-il eu des réflexions sur le profilage, l'optimisation spéculative et
désoptimisation ? La JVM fait tout cela.

Le 29 juin 2017 à 20h58, "Noah Falk" [email protected] a écrit :

@ltrzesniewski https://github.com/ltrzesniewski - Merci pour le
Rétroaction! J'espère certainement que la compilation à plusieurs niveaux est utile pour le vaste
majorité des projets, mais les compromis peuvent ne pas être idéaux pour tous les projets.
J'ai spéculé que nous laisserions une variable d'environnement en place pour
désactivez le jitting à plusieurs niveaux, auquel cas vous conservez le comportement d'exécution que vous
ont maintenant avec une qualité supérieure (mais plus lente à générer) jitting à l'avant. Est
définir une variable d'environnement quelque chose de raisonnable pour votre application ?
D'autres options sont également possibles, je gravite juste autour de l'environnement
variable car c'est l'une des options de configuration les plus simples que nous puissions utiliser.

Aussi une question connexe : quand la deuxième passe JIT entrerait-elle en vigueur ?

C'est une politique qui est très susceptible d'évoluer dans le temps. Le courant
l'implémentation du prototype utilise une politique simpliste : "La méthode a-t-elle été
appelé >= 30 fois"
https://github.com/dotnet/coreclr/blob/master/src/vm/
tieredcompilation.cpp#L89
https://github.com/dotnet/coreclr/blob/master/src/vm/
tieredcompilation.cpp#L122

Idéalement, cette politique très simple suggère une belle amélioration des performances sur
ma machine, même si ce n'est qu'une supposition. Afin de créer de meilleures politiques
nous devons obtenir des commentaires sur l'utilisation dans le monde réel et obtenir ces commentaires
exigera que la mécanique de base soit raisonnablement robuste dans une variété de
scénarios. Mon plan est donc d'améliorer d'abord la robustesse/la compatibilité, puis de faire
plus d'exploration pour la politique de réglage.

@DemiMarie https://github.com/demimarie - Nous n'avons rien qui
suit les itérations de boucle dans le cadre de la politique maintenant, mais c'est intéressant
perspective d'avenir.


Vous recevez ceci parce que vous avez été mentionné.
Répondez directement à cet e-mail, consultez-le sur GitHub
https://github.com/dotnet/coreclr/issues/4331#issuecomment-312146470 ,
ou couper le fil
https://github.com/notifications/unsubscribe-auth/AGGWB5m2qCnOKJsaXFCFigI3J6Ql8PMQks5sJEgZgaJpZM4IHWs8
.

@noahfalk Une variable d'environnement certainement pas une solution qui permettrait de piloter cette application par application. Pour les applications serveur/service, vous ne vous souciez généralement pas du temps nécessaire au démarrage de l'application (je sais que nous ne le faisons pas au détriment des performances). En développant un moteur de base de données, je peux vous le dire de première main, nous en avons besoin pour fonctionner aussi vite que possible dès le départ et même sur des chemins ou des benchmarks non exceptionnels effectués par de nouveaux clients potentiels.

D'un autre côté, étant donné que dans des environnements typiques, la disponibilité peut être mesurée en semaines à la fois, peu nous importe si cela prend même 30 secondes ; ce qui nous importe, c'est que forcer l'utilisateur à émettre un commutateur général (tout ou rien) ou même que l'utilisateur s'en soucie (comme défini par défaut à partir des fichiers de configuration) revient à 10 pas en arrière.

Ne vous méprenez pas, je recherche plus qu'avant un JIT à plusieurs niveaux, car il ouvre la voie à une haute performance qui prend autant de temps que vous avez besoin d'un chemin de code pour l'optimisation au niveau du JIT. J'ai même suggéré cela il y a longtemps lors de discussions informelles avec certains ingénieurs du JIT, et vous l'aviez déjà sur le radar. Mais un moyen de personnaliser le comportement à l'échelle de l'application (et non à l'échelle du système) est (du moins pour nous) un indicateur de qualité critique pour cette fonctionnalité particulière.

EDIT : Quelques problèmes de style.

@redknightlois - Merci pour le suivi

Une variable d'environnement certainement pas une solution qui permettrait de contrôler cette application par application.

Un peu confus sur cette partie ... les variables d'environnement ont une granularité par processus plutôt que par système, du moins sur les plates-formes que je connaissais. Par exemple, aujourd'hui, pour activer la compilation à plusieurs niveaux pour les tests dans une seule application, j'exécute :

set COMPLUS_EXPERIMENTAL_TieredCompilation=1
MyApp.exe
set COMPLUS_EXPERIMENTAL_TieredCompilation=0

ce qui nous importe, c'est que [nous ne] forçons pas l'utilisateur ... à s'en soucier

Je suppose que vous souhaitez un paramètre de configuration pouvant être spécifié par le développeur de l'application, et non par la personne qui exécute l'application ? Une possibilité avec la variable env est de faire en sorte que l'application que l'utilisateur lance un wrapper trivial (comme un script batch) qui lance l'application coreclr bien que j'admette que cela semble un peu inélégant. Je suis ouvert aux alternatives et non défini sur la variable env. Juste pour définir les attentes, ce n'est pas un domaine dans lequel je consacrerai des efforts de conception actifs dans un avenir très proche, mais je conviens qu'il est important d'avoir une configuration appropriée.

Aussi un avertissement - en supposant que nous continuons sur le chemin de la compilation à plusieurs niveaux de manière décente, je pourrais facilement imaginer que nous atteignons un point où l'activation de la compilation à plusieurs niveaux est non seulement le démarrage le plus rapide, mais elle bat également les performances actuelles en régime permanent. En ce moment, les performances de démarrage sont ma cible, mais ce n'est pas la limite de ce que nous pouvons en faire :)

Y a-t-il eu des réflexions sur le profilage, l'optimisation spéculative et
désoptimisation ?

@DemiMarie - Ils sont certainement apparus dans les conversations et je pense que beaucoup de gens sont ravis que la compilation à plusieurs niveaux ouvre ces possibilités. Parlant juste pour moi, j'essaie de rester concentré sur la fourniture des capacités fondamentales de compilation à plusieurs niveaux avant de viser plus haut. D'autres personnes de notre communauté sont probablement déjà devant moi sur d'autres applications.

@noahfalk Oui, être inélégant signifie également que le processus habituel pour l'exécuter peut (et très probablement) devenir sujet aux erreurs et c'est essentiellement le problème (la seule façon d'être complètement sûr que personne ne gâchera est de le faire à l'échelle du système). Une alternative que nous savons que cela fonctionne est que de la même manière, vous pouvez configurer si vous allez utiliser le serveur GC avec une entrée dans le app.config vous pouvez faire la même chose avec la compilation à plusieurs niveaux (au moins jusqu'à ce que le à plusieurs niveaux peut constamment battre les performances en régime permanent). Étant le JIT, vous pouvez également le faire par assemblage en utilisant le assembly.config et donnerait un degré de capacités qui n'existe pas actuellement si d'autres boutons peuvent également être sélectionnés de cette manière.

Les variables d'environnement sont souvent définies par utilisateur ou par système, ce qui a l'effet négatif potentiel d'affecter tous ces processus, sur plusieurs versions du runtime. Un fichier de configuration par application semble être une bien meilleure solution (même si par utilisateur/par système est également disponible) - quelque chose comme les valeurs de configuration du bureau qui pourraient être définies dans app.config, mais aussi utiliser env vars ou le registre .

Je pense que nous allons implémenter le chemin le plus courant qui est par application. Les paramètres à l'échelle du système peuvent également être utiles, mais je ne pense pas que nous devions y penser avant que la fonctionnalité ne soit implémentée.

Veuillez noter que nous n'avons pas travaillé en détail sur ce que le jit de deuxième niveau devrait faire pour l'optimisation, bien que nous ayons quelques idées. Il pourrait simplement faire ce que le jit fait aujourd'hui, mais il est fort probable qu'il en fera plus.

Alors permettez-moi de souligner quelques complications potentielles...

Il est possible que le jit de deuxième niveau s'amorce sur les observations faites sur le comportement du code créé par le jit de premier niveau. Donc, contourner le jit de premier niveau et demander directement le jit de deuxième niveau peut ne pas fonctionner du tout, ou peut ne pas fonctionner aussi bien, que de simplement laisser la hiérarchisation suivre son cours. Il est possible qu'une option de "contournement de hiérarchisation", quelle que soit sa mise en œuvre, finirait par donner du code comme le code que le jit produit par défaut aujourd'hui, et non le code qu'un jit de deuxième niveau pourrait produire.

Le jit de deuxième niveau peut être réglé de telle sorte que son exécution sur un grand nombre de méthodes entraîne des temps de jit relativement lents (puisque nous nous attendons à ce que relativement peu de méthodes finissent par être jittées avec le jit de deuxième niveau, et nous nous attendons à le jit de deuxième niveau fera une optimisation plus approfondie). Nous ne connaissons pas encore les bons compromis ici.

Cela étant dit...

Je pense qu'un attribut de méthode "d'optimisation agressive" a du sens - celui qui demande au jit de se comporter un peu comme le jit de second niveau pourrait se comporter pour des méthodes spécifiques, et peut-être ignorer ces méthodes pendant le préjitting (puisque le code préjitté s'exécute plus lentement que le code jitté, surtout pour R2R). Mais appliquer cette notion à un assembly entier ou à tous les assemblys d'une application ne semble pas aussi attrayant.

Si vous prenez ce qui se passe dans les compilateurs natifs comme une analogie appropriée, les compromis entre les performances et le temps de compilation/la taille du code peuvent devenir assez mauvais à des niveaux d'optimisation plus élevés, par exemple des compilations 10 fois plus longues pour une amélioration globale des performances de 1 à 2 %. La clé du puzzle est de savoir quelles méthodes sont importantes, et la seule façon de le faire est que les programmeurs le sachent ou que le système le découvre par lui-même.

@AndyAyersMS Je pense que vous avez tapé dans le mille. L'attribut JIT traitant "l'optimisation agressive" résoudrait probablement la plupart des problèmes de ne pas pouvoir disposer de suffisamment d'informations pour que le JIT produise un meilleur code isolé sans que le jit de premier niveau n'ait le temps de fournir ces commentaires.

L' attribut

Ce serait formidable de pouvoir utiliser quelque chose de similaire à MPGO pour commencer à fonctionner avec du code jitted de second niveau. Avance rapide du premier niveau au lieu de le contourner complètement.

@AndyAyersMS , le fait qu'Azul ait implémenté un JIT géré pour la JVM à l'aide de LLVM a-t-il facilité l'intégration de LLVM dans le CLR ? Apparemment, les modifications ont été poussées en amont vers LLVM dans le processus.

Juste pour info, j'ai créé un certain nombre d'éléments de travail pour un travail particulier que nous devons faire pour décoller à plusieurs niveaux (#12609, dotnet/coreclr#12610, dotnet/coreclr#12611, dotnet/coreclr#12612, dotnet/coreclr #12617). Si votre intérêt se rapporte directement à l'un d'entre eux, n'hésitez pas à y ajouter vos commentaires. Pour tous les autres sujets, je suppose que la discussion restera ici, ou n'importe qui peut créer un problème pour un sous-sujet spécifique s'il y a suffisamment d'intérêt pour mériter de le diviser par lui-même.

@MendelMonteiro Rendre les données de retour de style MPGO disponibles lors du jitting est certainement une option (actuellement, nous ne pouvons relire ces données que lors du préjitting). Il existe diverses limites à ce qui peut être instrumenté, donc toutes les méthodes ne peuvent pas être gérées de cette façon, il y a d'autres limites que nous devons examiner (par exemple, aucune donnée de rétroaction n'est disponible pour les inlinees), l'instrumentation et les cycles de formation nécessaires pour créer les données MPGO sont un obstacle pour de nombreux utilisateurs, et les données MPGO peuvent ou non correspondre à ce que nous aurions lors du démarrage du premier niveau, mais l'idée a certainement du mérite.

En ce qui concerne un niveau supérieur basé sur LLVM - nous avons évidemment examiné cela dans une certaine mesure avec LLILC, et à l'époque nous étions en contact fréquent avec les gens d'Azul, nous connaissons donc beaucoup de choses qu'ils faisaient dans LLVM pour le rendre plus propice à la compilation de langages avec un GC précis.

Il y avait (et il y a probablement encore) des différences significatives dans la prise en charge de LLVM nécessaire pour le CLR par rapport à ce qui est nécessaire pour Java, à la fois dans GC et dans EH, et dans les restrictions à imposer à l'optimiseur. Pour ne citer qu'un exemple : les CLR GC ne peuvent actuellement pas tolérer les pointeurs gérés qui pointent vers la fin des objets. Java gère cela via un mécanisme de rapport couplé base/dérivé. Nous aurions soit besoin de prendre en charge ce type de rapports jumelés dans le CLR, soit de restreindre les passes d'optimisation de LLVM pour ne jamais créer ces types de pointeurs. En plus de cela, le jit LLILC était lent et nous n'étions pas sûrs finalement du type de qualité de code qu'il pourrait produire.

Ainsi, déterminer comment LLILC pourrait s'intégrer dans une approche potentielle à plusieurs niveaux qui n'existait pas encore semblait (et semble toujours) prématuré. L'idée pour l'instant est d'intégrer la hiérarchisation dans le cadre et d'utiliser RyuJit pour le jit de deuxième niveau. Au fur et à mesure que nous en apprendrons davantage, nous découvrirons peut-être qu'il y a effectivement de la place pour des jits de niveau supérieur, ou, du moins, mieux comprendre ce que nous devons faire d'autre avant que de telles choses aient un sens.

@AndyAyersMS Peut-être pouvez-vous également introduire les modifications nécessaires dans LLVM plutôt que de contourner ses limites.

Le JIT multicœur et son optimisation de profil fonctionnent-ils avec coreclr ?

@benaadams - Oui, le JIT multicœur fonctionne. Je ne me souviens pas des scénarios (le cas échéant) où il est activé par défaut, mais vous pouvez l'activer via la configuration : https://github.com/dotnet/coreclr/blob/master/src/inc/clrconfigvalues.h# L548

J'ai écrit un compilateur à moitié jouet et j'ai remarqué que la plupart du temps, les optimisations percutantes peuvent être effectuées assez correctement sur la même infrastructure et que très peu de choses peuvent être effectuées dans l'optimiseur de niveau supérieur.

Ce que je veux dire, c'est ceci : si une fonction est utilisée plusieurs fois, les paramètres tels que :

  • augmenter le nombre d'instructions en ligne
  • utiliser un répartiteur de registre plus "avancé" (colorateur de backtracking de type LLVM ou coloriseur complet)
  • faire plus de passes d'optimisations, peut-être certaines spécialisées avec les connaissances locales. Par exemple : autoriser le remplacement de l'allocation d'objet complète par l'allocation de pile si l'objet est déclaré dans la méthode et n'est pas affecté dans le corps d'une fonction en ligne plus grande.
  • utilisez PIC pour la plupart des objets touchés où CHA n'est pas possible. Même StringBuilder, par exemple, n'est très probablement pas remplacé, le code pourrait s'il est marqué comme chaque fois qu'il a été frappé avec un StringBuilder, toutes les méthodes appelées à l'intérieur peuvent être dévirtualisées en toute sécurité et un type-guard est défini devant l'accès du SB.

Ce serait aussi très bien, mais c'est peut-être mon rêve éveillé, que CompilerServices offre le "compilateur avancé" à exposer pour pouvoir être accessible via du code ou des métadonnées, donc des endroits comme les jeux ou les plateformes de trading pourraient bénéficier en commençant compilation à l'avance quelles classes et méthodes doivent être "compilées plus profondément". Ce n'est pas NGen, mais si un compilateur non hiérarchisé n'est pas nécessairement possible (souhaitable), au moins pour pouvoir utiliser le code optimisé plus lourd pour les parties critiques qui ont besoin de ces performances supplémentaires. Bien sûr, si une plate-forme n'offre pas les optimisations lourdes (disons Mono), les appels API seront essentiellement un NO-OP.

Nous avons maintenant une base solide pour la hiérarchisation en place grâce au travail acharné de @noahfalk , @kouvel et d'autres.

Je suggère que nous fermions ce problème et que nous ouvrions un problème "comment pouvons-nous améliorer le jitting à plusieurs niveaux". J'encourage toute personne intéressée par le sujet à essayer la hiérarchisation actuelle pour avoir une idée de la situation actuelle. Nous aimerions avoir des commentaires sur le comportement réel, qu'il soit bon ou mauvais.

Le comportement actuel est-il décrit quelque part ? Je n'ai trouvé que cela , mais il s'agit davantage des détails de mise en œuvre que de la hiérarchisation en particulier.

Je pense que nous allons bientôt disposer d'une sorte de résumé récapitulatif, avec certaines des données que nous avons recueillies.

La hiérarchisation peut être activée dans la version 2.1 en définissant COMPlus_TieredCompilation=1 . Si vous l'essayez, n'hésitez pas à faire part de ce que vous avez trouvé...

Avec les PR récents (https://github.com/dotnet/coreclr/pull/17840, https://github.com/dotnet/sdk/pull/2201), vous avez également la possibilité de spécifier une compilation à plusieurs niveaux en tant que runtimeconfig. propriété json ou une propriété de projet msbuild. L'utilisation de cette fonctionnalité nécessitera que vous soyez sur des versions très récentes alors que la variable d'environnement existe depuis un certain temps.

Comme nous en avons déjà discuté avec @jkotas , Tiered JIT peut améliorer le temps de démarrage. Est-ce que ça marche quand on utilise des images natives ?
Nous avons effectué des mesures pour plusieurs applications sur le téléphone Tizen et voici les résultats :

DLL système|DLL d'application|Échelonné|temps, s
-----------|--------|------|--------
R2R |R2R |non |2.68
R2R |R2R |oui |2,61 (-3%)
R2R |non |non |4.40
R2R |non |oui |3,63 (-17%)

Nous vérifierons également le mode FNV, mais il semble que cela fonctionne bien lorsqu'il n'y a pas d'images.

cc @gbalykov @nkaretnikov2

Pour votre information, la compilation à plusieurs niveaux est désormais la valeur par défaut pour .NET Core : https://github.com/dotnet/coreclr/pull/19525

@alpencolt , les améliorations du temps de démarrage peuvent être moindres lors de l'utilisation de la compilation AOT telle que R2R. L'amélioration du temps de démarrage provient actuellement d'un jitting plus rapide avec moins d'optimisations, et lors de l'utilisation de la compilation AOT, il y aurait moins de JIT. Certaines méthodes ne sont pas prégénérées, telles que certains génériques, les stubs IL et d'autres méthodes dynamiques. Certains génériques peuvent bénéficier de la hiérarchisation au démarrage, même lors de l'utilisation de la compilation AOT.

Je vais continuer à fermer ce sujet, car avec le commit de @kouvel je pense avoir atteint la demande dans le titre : D Les gens sont les bienvenus pour continuer la discussion et/ou ouvrir de nouveaux sujets sur des sujets plus spécifiques tels que les améliorations demandées, questions ou enquêtes particulières. Si quelqu'un pense qu'il est fermé prématurément, bien sûr, faites-le nous savoir.

@kouvel Désolé de commenter le problème clos. Je me demande, lors de l'utilisation d'une compilation AOT telle que crossgen, l'application bénéficiera-t-elle toujours de la compilation de deuxième niveau pour les chemins de code hot-spot ?

@daxian-dbw oui tout à fait; au moment de l'exécution, le Jit peut faire de l'intégration croisée (entre les dll); élimination de branche basée sur des constantes d'exécution ( readonly static ); etc

@benaadams Et un compilateur AOT bien conçu ne pourrait pas ?

J'ai trouvé des informations à ce sujet sur https://blogs.msdn.microsoft.com/dotnet/2018/08/02/tiered-compilation-preview-in-net-core-2-1/ :

les images précompilées ont des contraintes de version et des contraintes d'instruction CPU qui interdisent certains types d'optimisation. Pour toutes les méthodes de ces images qui sont appelées fréquemment Tiered Compilation demande au JIT de créer un code optimisé sur un thread d'arrière-plan qui remplacera la version pré-compilée.

Oui, c'est un exemple de "pas un AOT bien conçu". 😛

les images précompilées ont des contraintes de version et des contraintes d'instruction CPU qui interdisent certains types d'optimisation.

L'un des exemples est les méthodes qui utilisent le matériel intrinsèque. Le compilateur AOT (crossgen) suppose simplement que SSE2 est la cible codegen sur x86/x64, de sorte que toutes les méthodes qui utilisent le matériel intrinsèque seront rejetées par crossgen et compilées par JIT qui connaît les informations matérielles sous-jacentes.

Et un compilateur AOT bien conçu ne pourrait pas ?

Le compilateur AOT nécessite une optimisation du temps de liaison (pour l'intégration d'assemblages croisés) et une optimisation guidée par le profil (pour les constantes d'exécution). Pendant ce temps, le compilateur AOT a besoin d'informations matérielles "de base" (comme -mavx2 dans gcc/clang) au moment de la construction pour le code SIMD.

L'un des exemples est les méthodes qui utilisent le matériel intrinsèque. Le compilateur AOT (crossgen) suppose simplement que SSE2 est la cible codegen sur x86/x64, de sorte que toutes les méthodes qui utilisent le matériel intrinsèque seront rejetées par crossgen et compilées par JIT qui connaît les informations matérielles sous-jacentes.

Attends quoi? Je ne suis pas tout à fait ici. Pourquoi le compilateur AOT rejetterait-il les intrinsèques ?

Et un compilateur AOT bien conçu ne pourrait pas ?

Le compilateur AOT nécessite une optimisation du temps de liaison (pour l'intégration d'assemblages croisés) et une optimisation guidée par le profil (pour les constantes d'exécution). Pendant ce temps, le compilateur AOT a besoin d'informations matérielles "de base" (comme -mavx2 dans gcc/clang) au moment de la construction pour le code SIMD.

Oui, comme je l'ai dit, "un compilateur AOT bien conçu". 😁

@masonwheeler scénario différent ; crossgen est un AoT qui fonctionne avec le Jit et permet la maintenance/correction des dll sans nécessiter une recompilation et une redistribution complètes de l'application. Il offre une meilleure génération de code que Tier0 avec un démarrage plus rapide que Tier1 ; mais n'est pas neutre sur la plate-forme.

Tier0, crossgen et Tier1 fonctionnent tous ensemble comme un modèle cohérent en coreclr

Pour effectuer un assemblage croisé en ligne (non-Jit), cela nécessiterait la compilation d'un seul fichier exécutable lié statiquement et nécessiterait une recompilation et une redistribution complètes de l'application pour corriger toute bibliothèque utilisée ainsi que le ciblage de la plate-forme spécifique (quelle version de SSE, Avx, etc. à utiliser ; version commune la plus basse ou produit pour tous ?).

corert AoT ce style d'application.

Pourtant; faire certains types d'élimination de branche que le Jit peut faire nécessiterait une grande quantité de génération supplémentaire d'asm pour les chemins alternatifs ; et le correctif d'exécution de l'arborescence correcte

par exemple, tout code utilisant une méthode comme (où le Tier1 Jit supprimera tous les if s)

readonly static _numProcs = Environment.ProcessorCount;

public void DoThing()
{
    if (_numProcs == 1) 
    {
       // Single proc path
    }
    else if (_numProcs == 2) 
    {
       // Two proc path
    }
    else
    {
       // Multi proc path
    }
}

@benaadams

Pour effectuer un assemblage croisé en ligne (non-Jit), cela nécessiterait la compilation d'un seul fichier exécutable lié statiquement et nécessiterait une recompilation et une redistribution complètes de l'application pour corriger toute bibliothèque utilisée ainsi que le ciblage de la plate-forme spécifique (quelle version de SSE, Avx, etc. à utiliser ; version commune la plus basse ou produit pour tous ?).

Cela ne devrait pas nécessiter une redistribution complète de l'application. Regardez le système de compilation ART d'Android : vous distribuez l'application sous forme de code géré (Java dans leur cas, mais les mêmes principes s'appliquent) et le compilateur, qui vit sur le système local, AOT compile le code géré dans un exécutable natif super optimisé.

Si vous modifiez une petite bibliothèque, tout le code géré est toujours là et vous n'auriez pas à tout redistribuer, juste la chose avec le correctif, puis l'AOT peut être réexécuté pour produire un nouvel exécutable. (Évidemment, c'est là que l'analogie Android s'effondre, en raison du modèle de distribution d'applications APK d'Android, mais cela ne s'applique pas au développement de bureau/serveur.)

et le compilateur, qui vit sur le système local, AOT compile le code managé...

C'est le modèle NGen précédent utilisé par le framework complet ; mais ne pensez-vous pas non plus qu'il a créé un seul assemblage incorporant le code du cadre dans le code des applications? La différence entre les deux approches a été mise en évidence dans les exécutions de Bing.com sur .NET Core 2.1 ! article de blog

Images prêtes à fonctionner

Les applications gérées peuvent souvent avoir des performances de démarrage médiocres car les méthodes doivent d'abord être compilées JIT en code machine. .NET Framework dispose d'une technologie de précompilation, NGEN. Cependant, NGEN requiert que l'étape de précompilation se produise sur la machine sur laquelle le code s'exécutera. Pour Bing, cela signifierait NGEN sur des milliers de machines. Ceci, associé à un cycle de déploiement agressif, entraînerait une réduction significative de la capacité de service à mesure que l'application est précompilée sur les machines de service Web. De plus, l'exécution de NGEN nécessite des privilèges d'administrateur, qui sont souvent indisponibles ou fortement contrôlés dans un environnement de centre de données. Sur .NET Core, l'outil crossgen permet de précompiler le code en tant qu'étape de pré-déploiement, comme dans le laboratoire de construction, et les images déployées en production sont prêtes à fonctionner !

@masonwheeler AOT fait face à des vents contraires dans .Net complet en raison de la nature dynamique d'un processus .Net. Par exemple, les corps de méthode dans .Net peuvent être modifiés via un profileur à tout moment, les classes peuvent être chargées ou créées par réflexion, et un nouveau code peut être créé par le runtime selon les besoins pour des choses comme l'interopérabilité - donc les informations d'analyse interprocédurale reflètent au mieux un état transitoire au processus en cours d'exécution. Toute analyse ou optimisation interprocédurale (y compris l'inlining) dans .Net doit pouvoir être annulée au moment de l'exécution.

AOT fonctionne mieux lorsque l'ensemble des choses qui peuvent changer entre le temps AOT et le temps d'exécution est petit et que l'impact de ces changements est localisé, de sorte que la portée étendue disponible pour l'optimisation AOT reflète en grande partie des choses qui doivent toujours être vraies (ou avoir peut-être un petit nombre de variantes).

Si vous pouvez intégrer des mécanismes permettant de gérer ou de restreindre la nature dynamique des processus .Net, l'AOT pur peut très bien fonctionner - par exemple, .Net Native considère l'impact de la réflexion et de l'interopérabilité, et interdit le chargement de l'assemblage, l'émission de la réflexion et ( Je suppose) profil attaché. Mais ce n'est pas simple.

Des travaux sont en cours pour nous permettre d'étendre la portée de crossgen à plusieurs assemblys afin que nous puissions compiler AOT tous les frameworks de base (ou tous les assemblys asp.net) en tant que bundle. Mais ce n'est viable que parce que nous avons le jit comme solution de repli pour refaire le codegen lorsque les choses changent.

@AndyAyersMS Je n'ai jamais cru que la solution .NET AOT devrait être une solution "pure AOT uniquement", pour exactement les raisons que vous décrivez ici. Il est très important d'avoir le JIT autour pour créer du nouveau code selon les besoins. Mais les situations dans lesquelles il est nécessaire sont très minoritaires, et je pense donc que la règle d'Anders Hejlsberg pour les systèmes de types pourrait être appliquée avec profit ici :

Statique si possible, dynamique si nécessaire.

De System.Linq.Expressions
public TDelegate Compile(bool preferinterpretation);

La compilation à plusieurs niveaux continue-t-elle à fonctionner si preferinterpretation est vrai ?

Cette page vous a été utile?
0 / 5 - 0 notes