La compilation à la volée (just-in-time) ne serait pas ergonomique, selon Abe Winter
Qui propose des améliorations
Le 2020-04-01 17:01:24, par Stéphane le calme, Chroniqueur Actualités
Les ordinateurs n'exécutent pas le code source que nous écrivons dans les langages de programmation, ils exécutent le code machine. Dans des circonstances normales, il existe deux façons de passer d'un langage de programmation au code machine :
Les langages compilés sont plus rapides et plus sûrs. Les langages interprétés sont plus flexibles, plus productifs et plus faciles à apprendre.
La compilation à la volée (just-in-time compilation ou JIT compilation en anglais) est une technique visant à améliorer la performance de systèmes compilés en bytecode par la traduction de bytecode en code machine natif au moment de l'exécution. La compilation à la volée se fonde sur deux anciennes idées : la compilation de bytecode et la compilation dynamique. Elle apporte donc le meilleur des deux mondes, en théorie.
La compilation à la volée s'adapte dynamiquement à la charge de travail courante du logiciel, en compilant le code « chaud », c'est-à-dire le code le plus utilisé à un moment donné (ce qui peut représenter tout le programme, mais souvent seules certaines parties du programme sont traitées par compilation à la volée). Obtenir du code machine optimisé se fait beaucoup plus rapidement depuis du bytecode que depuis du code source. Comme le bytecode déployé est portable, la compilation à la volée est envisageable pour tout type d'architecture, à la condition d'avoir un compilateur JIT pour cette architecture, ce qui est facilité par le fait que les compilateurs de bytecode en code machine sont plus faciles à écrire que les compilateurs code source - code natif.
La compilation à la volée ne serait pas ergonomique selon un développeur
Abe Winter est un développeur reconverti qui a des années d'expérience professionnelle en tant que programmeur avec différents paradigmes de langage (statique, dynamique, JIT). Il pense que JIT n’est pas ergonomique :
« Le travail sur les performances en 2020 peut concerner autant la mise en cache et la conception intelligente de RPC que les performances linéaires d'une fonction. Mais les performances en ligne droite comptent toujours. Et l'ergonomie de mesure de la performance d'un morceau de code en ligne droite en prod est mauvaise.
« J’accuse le JIT.
« J'aime l'idée de JIT, mais l'outillage est sous-développé. Le battage médiatique dit "compétitif avec C pour les fonctions numériques courtes", mais la réalité est "quelque part entre Python et Java". C’est comme un bus qui vous dépose à mi-chemin et ne vous laisse pas emporter de vélo. Il serait plus rapide de parcourir tout le chemin à vélo plutôt que d’en parcourir la moitié à pied ».
Voici les arguments qu’il avance pour soutenir son affirmation :
JS JIT rend le test de performance impossible
« Je ne sais pas comment faire ces choses en JavaScript :
« En l'absence d'un sous-ensemble de ces astuces, je n'ai aucun moyen de tester à la perfection mon code JS sans tester la charge à pleine échelle. Cela finit par être une énorme perte de temps et c'est la raison pour laquelle les gens réécrivent les services JS dans d'autres langages lorsqu'ils grandissent. »
Les benchmarks mentent
« C'est peut-être une façon forte de le dire. Mais les langages JIT rendent difficile l'utilisation du micro-benchmarking pour prédire les performances du même morceau de code en prod, pour plusieurs raisons:
« Ne pas savoir combien de temps prend la réalité mène au vaudou et à la gymnastique.
« Oui, les langages compilés ont leur propre version de ceci avec une cohérence de cache. Oui, le JIT peut conduire à de meilleures performances que la précompilation en théorie à cause du collecteur de statistiques, mais je ne contrôle aucune de ces choses. »
Il est plus facile d'écrire du code performant (choisissez votre poison)
« Et plus important encore, maintenir le code performant. Ne vous méprenez pas, je ne dis pas que C++, Go ou Java sont des langages plus productifs que JS. Mais si votre budget est tel que les serveurs coûtent plus cher que la paie, il est difficile de conserver votre logique de base à grande échelle dans un langage interprété / JIT.
Pour deux raisons :
Abe Winter ne demande pas d’abandonner le système JIT, mais de l’améliorer
« Tout ce que je demande, c'est des informations et un contrôle. La prémisse de base de JIT de compiler uniquement des parties de votre programme me convient. Je voudrais pouvoir contrôler les parties en questions et le moment de compilation.
« Je voudrais une collecte de statistiques efficace sur les dépôts en prod, donc je sais quelles fonctions traiter comme des hot paths.
« Les langages compilés ont un paquet d'astuces qui sont en quelque sorte sur ce sujet : optimisation guidée statistiquement, optimisation de programme entier. Les compilations à la volée y arriveront. »
Voici une liste d’éléments qu’il voudrait voir implémentés :
Source : Abe Winter
Et vous ?
Partagez-vous son avis ? Dans quelle mesure ?
Que pensez-vous des arguments qu'il a avancés ?
En avez-vous quelques-un pour soutenir ou relever les limites de son affirmation ?
Que pensez-vous des propositions qu'il a faites ? En avez-vous d'autres ?
- les compilateurs, qui créent une fois un exécutable en code machine à partir de votre code source, et vous pouvez exécuter ce blob encore et encore. Compilation lente, exécution rapide
- les interpréteurs, qui lisent le programme ligne par ligne lors de son exécution et exécutent un autre programme qui exécute ces lignes. Démarrage plus lent, performances plus lentes, mais aucune étape de compilation lente après les modifications.
Les langages compilés sont plus rapides et plus sûrs. Les langages interprétés sont plus flexibles, plus productifs et plus faciles à apprendre.
La compilation à la volée (just-in-time compilation ou JIT compilation en anglais) est une technique visant à améliorer la performance de systèmes compilés en bytecode par la traduction de bytecode en code machine natif au moment de l'exécution. La compilation à la volée se fonde sur deux anciennes idées : la compilation de bytecode et la compilation dynamique. Elle apporte donc le meilleur des deux mondes, en théorie.
La compilation à la volée s'adapte dynamiquement à la charge de travail courante du logiciel, en compilant le code « chaud », c'est-à-dire le code le plus utilisé à un moment donné (ce qui peut représenter tout le programme, mais souvent seules certaines parties du programme sont traitées par compilation à la volée). Obtenir du code machine optimisé se fait beaucoup plus rapidement depuis du bytecode que depuis du code source. Comme le bytecode déployé est portable, la compilation à la volée est envisageable pour tout type d'architecture, à la condition d'avoir un compilateur JIT pour cette architecture, ce qui est facilité par le fait que les compilateurs de bytecode en code machine sont plus faciles à écrire que les compilateurs code source - code natif.
La compilation à la volée ne serait pas ergonomique selon un développeur
Abe Winter est un développeur reconverti qui a des années d'expérience professionnelle en tant que programmeur avec différents paradigmes de langage (statique, dynamique, JIT). Il pense que JIT n’est pas ergonomique :
« Le travail sur les performances en 2020 peut concerner autant la mise en cache et la conception intelligente de RPC que les performances linéaires d'une fonction. Mais les performances en ligne droite comptent toujours. Et l'ergonomie de mesure de la performance d'un morceau de code en ligne droite en prod est mauvaise.
« J’accuse le JIT.
« J'aime l'idée de JIT, mais l'outillage est sous-développé. Le battage médiatique dit "compétitif avec C pour les fonctions numériques courtes", mais la réalité est "quelque part entre Python et Java". C’est comme un bus qui vous dépose à mi-chemin et ne vous laisse pas emporter de vélo. Il serait plus rapide de parcourir tout le chemin à vélo plutôt que d’en parcourir la moitié à pied ».
Voici les arguments qu’il avance pour soutenir son affirmation :
JS JIT rend le test de performance impossible
« Je ne sais pas comment faire ces choses en JavaScript :
- savoir si une fonction est optimisée dans une exécution donnée ;
- savoir si une fonction sera optimisée en prod ;
- exiger statiquement qu'une fonction soit optimisée en prod ;
- être informé des dépôts dans les chemins critiques (hot paths).
« En l'absence d'un sous-ensemble de ces astuces, je n'ai aucun moyen de tester à la perfection mon code JS sans tester la charge à pleine échelle. Cela finit par être une énorme perte de temps et c'est la raison pour laquelle les gens réécrivent les services JS dans d'autres langages lorsqu'ils grandissent. »
Les benchmarks mentent
« C'est peut-être une façon forte de le dire. Mais les langages JIT rendent difficile l'utilisation du micro-benchmarking pour prédire les performances du même morceau de code en prod, pour plusieurs raisons:
- les benchmarks (qui exécutent la même fonction avec les mêmes entrées des millions de fois de suite) sont très amies avec la gigue (jitter en anglais, variation de la latence au fil du temps) ;
- la fonction que vous testez peut être appelée avec différentes entrées en prod. Dans JS, même l'ordre des clés dans un objet peut confondre le typer JIT ;
- si vous comparez une fonction JIT / native et que votre configuration de produit finit par utiliser du code interprété, vous n'aurez rien appris.
« Ne pas savoir combien de temps prend la réalité mène au vaudou et à la gymnastique.
« Oui, les langages compilés ont leur propre version de ceci avec une cohérence de cache. Oui, le JIT peut conduire à de meilleures performances que la précompilation en théorie à cause du collecteur de statistiques, mais je ne contrôle aucune de ces choses. »
Il est plus facile d'écrire du code performant (choisissez votre poison)
« Et plus important encore, maintenir le code performant. Ne vous méprenez pas, je ne dis pas que C++, Go ou Java sont des langages plus productifs que JS. Mais si votre budget est tel que les serveurs coûtent plus cher que la paie, il est difficile de conserver votre logique de base à grande échelle dans un langage interprété / JIT.
Pour deux raisons :
- l’optimisation des hot paths : il y aura un moment où vous aurez besoin d'un chemin de code plus rapide et le JIT fera que cela marche comme si vous étiez en train de vous servir de la gélatine avec des pincettes ;
- des régressions de performances : quelqu'un va à un moment donné effectuer un changement moyen ou grand dans le code qui va dégrader subtilement les performances et nuire à la qualité du produit, et à moins que votre surveillance soit excellente, vous ne le saurez pas
Abe Winter ne demande pas d’abandonner le système JIT, mais de l’améliorer
« Tout ce que je demande, c'est des informations et un contrôle. La prémisse de base de JIT de compiler uniquement des parties de votre programme me convient. Je voudrais pouvoir contrôler les parties en questions et le moment de compilation.
« Je voudrais une collecte de statistiques efficace sur les dépôts en prod, donc je sais quelles fonctions traiter comme des hot paths.
« Les langages compilés ont un paquet d'astuces qui sont en quelque sorte sur ce sujet : optimisation guidée statistiquement, optimisation de programme entier. Les compilations à la volée y arriveront. »
Voici une liste d’éléments qu’il voudrait voir implémentés :
Source : Abe Winter
Et vous ?
-
abriotdeMembre chevronnéEst-ce donc vraiment une critique de JIT globale
Les arguments avancés sont tout a fais justifiés. Il ne dis pas que JIT est inutile il dis juste que dans du code réel (ou il n'y a pas une grande majorité de boucle répétés un grand nombre de fois mais plutôt beaucoup de branchement conditionnel et petites boucle), certes JIT accélère les grande boucle mais il empêche de traiter aussi efficacement qu'on l'aurait fais sans JIT les autres parties du code. Et il explique aussi que c'est très difficile a analyser.
En ce sens il a raison. Il ne faut pas le voir comme un refus du JIT, mais comme une critique constructive pour focaliser la recherche autour du JIT plutôt que sur le JIT.le 02/04/2020 à 8:51 -
gangsoleilModérateurHello,
JIT : smalltalk, 1983 (https://en.wikipedia.org/wiki/Just-i...lation#History)
Pour moi, tout ce que dit ce monsieur est d'une évidence navrante.
J'ai bossé un peu chez Sun Microsystems sur la JVM Java (début des années 2000, OK ça fait un bail, et non vous ne pouvez pas le vérifier), ces problématiques de JIT m'ont clairement été expliquées, et les problématiques étaient exactement les mêmes : que compiler et pourquoi ? Comment tu choisis que tu vas plutôt compiler cette fonction plutôt qu'une autre, et tu peux pousser sur les différentes optimisations de compilation, et pleins d'autres problématiques super intéressantes, mais qui n'ont rien de nouveau.« Je voudrais une collecte de statistiques efficace sur les dépôts en prod, donc je sais quelles fonctions traiter comme des hot paths.le 08/02/2021 à 17:30 -
walfratMembre émériteLe titre suggère une attaque sur le JIT en général, cependant les arguments donnés ne semblent concernés que le Javascript.
Est-ce donc vraiment une critique de JIT globale, ou juste de l'état de celui en JavaScript ?
Je peux comprendre la difficulté de parler de performance avec du JIT en jeu mais j'imagine que ces questions doivent avoir des réponses avec par exemple le JIT en Java depuis le temps que ça existe non ?
Enfin, les arguments tel qu'énoncé me rappelle vaguement le genre qu'on peut entendre contre le garbage collector, ou contre quelqu'un qui se plaindrait qu'un compilateur réécrit sa fonction récursive en boucle a la compilation. "Je ne contrôle pas", oui, c'est fait exprès, parce que généralement les éléments en jeu feront mieux que "toi" (toi = le développeur moyen ici), si tu as un besoin absolu de contrôle, utilise un langage qui est fait pour ça, tu peux même écrire ta propre librairie C++ et l'utiliser dans Java, pareil côté nodeJS. Je ne suis pas un expert loin de là, mais il me semble bien que c'est prévu ainsi, ce n'est pas étonnant d'avoir des soucis si on veut faire autrement que comment c'est prévu.le 02/04/2020 à 0:27 -
MingolitoMembre extrêmement actifFaux, il était possible de faire un package code VB6 plus runtime mais cela n'à rien à voir avec la compilation native C++ ou Delphi qui eux disposent d'un vrai compilateur.
C'était juste du bullshit marketing.le 29/01/2021 à 16:20 -
MingolitoMembre extrêmement actifTu n'a rien prouvé du tout avec ton blabla qui mélange tout.
Des benchmarks ont été faits qui ont montré que la différence de performance entre compiler en pcode ou en natif avec VB6 est minime, genre 1% d'amélioration, parce que c'est bidon, c'est du bullshit marketing, qui a été inventé à l'époque par le marketing justement pour faire croire que c'est du natif comme C++ ou Delphi, mais c'est faux.
Créer un package .exe qui encapsule à la fois un runtime et du pcode n'à rien à voir avec de la compilation native, comme le fait un compilateur C, C++ ou Delphi.
De fait les éditeurs de logiciels font généralement des logiciels avec C++ ou parfois Delphi mais absolument pas avec VB6.
Par exemple Windev c'est pas de la compilation native, par contre Windev est développé en C++.le 30/01/2021 à 14:51 -
rt15Membre éclairéMon blabla ne prouve rien en effet. Tout comme le tien.
Mais le code assembleur ne ment pas. La réalité technique est que le .exe contient mon algo en code natif, pas en bytecode.
Par contre je suis entièrement d'accord depuis le début pour dire que le VB6 est un escargot paraplégique comparé à du C ou du Delphi.
[edit]
Même exercice en C, compilation du code suivant avec gcc en 32 bits sans optimisations :
Code c : 1
2
3
4
5
6
7
8
9
10
11
12int first, second, temp, i; first = 0; second = 1; for (i = 0; i < n; i++) { temp = first + second; first = second; second = temp; } return first;
Désassemblage du .exe :
Code : 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
220040153f c745f400000000 mov dword ptr [ebp-0Ch],0 first = 0 00401546 c745f001000000 mov dword ptr [ebp-10h],1 second = 1 0040154d c745ec00000000 mov dword ptr [ebp-14h],0 i = 0 00401554 eb1b jmp fibonnaci+0x1571 (00401571) jmp second_label first_label 00401556 8b55f4 mov edx,dword ptr [ebp-0Ch] edx = first 00401559 8b45f0 mov eax,dword ptr [ebp-10h] eax = second 0040155c 01d0 add eax,edx eax = first + second 0040155e 8945e8 mov dword ptr [ebp-18h],eax temp = eax 00401561 8b45f0 mov eax,dword ptr [ebp-10h] eax = second 00401564 8945f4 mov dword ptr [ebp-0Ch],eax first = second 00401567 8b45e8 mov eax,dword ptr [ebp-18h] eax = temp 0040156a 8945f0 mov dword ptr [ebp-10h],eax second = temp 0040156d 8345ec01 add dword ptr [ebp-14h],1 i++ second_label 00401571 8b45ec mov eax,dword ptr [ebp-14h] eax = i 00401574 3b4508 cmp eax,dword ptr [ebp+8] comparaison de i avec n 00401577 7cdd jl fibonnaci+0x1556 (00401556) jmp first_label si i < n 00401579 8b45f4 mov eax,dword ptr [ebp-0Ch] Mise en place du résultat dans eax
Mais est ce fondamentalement différent ? Non. Dans les deux cas bin c'est des instructions jmp/mov/lea sur des registres eax/edx/ebp... Du code natif x86 quoi.
Si vous désassemblez du bytecode Java ou .NET ça sera très différent au niveau des instructions et des opérandes, et du Chinois pour un processeur.
[edit 2]
Et pour le C#, ça donne quoi ?
Comme Abe Winter le remarque, c'est plus difficile de travailler avec un langage qui compile en bytecode:
- Il n'y a pas de code natif dans l'exécutable.
- Le code natif généré à l'exécution peut varier d'une exécution à l'autre et dépend bien sûr du compilo JIT et de sa version.
Mais en théorie, à l'exécution, le bytecode est compilé en code natif dans la mémoire du processus puis exécuté.
Donc avec un bon débogueur genre WinDbg et un point d'arrêt bien senti, on peut retrouver le code natif que voici (.Net Core 5) :
Code : 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
3500007ffe`0b69c003 33c0 xor eax,eax eax = 0 00007ffe`0b69c005 8945fc mov dword ptr [rbp-4],eax first = 0 00007ffe`0b69c008 c745f801000000 mov dword ptr [rbp-8],1 second = 1 00007ffe`0b69c00f 8945f0 mov dword ptr [rbp-10h],eax i = 0 00007ffe`0b69c012 90 nop 00007ffe`0b69c013 eb1f jmp 00007ffe`0b69c034 jmp second_label first_label: 00007ffe`0b69c015 90 nop 00007ffe`0b69c016 8b45fc mov eax,dword ptr [rbp-4] eax = first 00007ffe`0b69c019 0345f8 add eax,dword ptr [rbp-8] eax = eax + second 00007ffe`0b69c01c 8945f4 mov dword ptr [rbp-0Ch],eax temp = eax 00007ffe`0b69c01f 8b45f8 mov eax,dword ptr [rbp-8] eax = second 00007ffe`0b69c022 8945fc mov dword ptr [rbp-4],eax first = eax 00007ffe`0b69c025 8b45f4 mov eax,dword ptr [rbp-0Ch] eax = temp 00007ffe`0b69c028 8945f8 mov dword ptr [rbp-8],eax second = temp 00007ffe`0b69c02b 90 nop 00007ffe`0b69c02c 8b45f0 mov eax,dword ptr [rbp-10h] eax = i 00007ffe`0b69c02f ffc0 inc eax eax++ 00007ffe`0b69c031 8945f0 mov dword ptr [rbp-10h],eax i = eax second_label: 00007ffe`0b69c034 8b45f0 mov eax,dword ptr [rbp-10h] eax = i 00007ffe`0b69c037 3b4510 cmp eax,dword ptr [rbp+10h] comparaison entre i et n 00007ffe`0b69c03a 0f9cc0 setl al al = 1 si i < n, donc s'il faut boucler 00007ffe`0b69c03d 0fb6c0 movzx eax,al eax = al 00007ffe`0b69c040 8945ec mov dword ptr [rbp-14h],eax [rbp-14h] = eax 00007ffe`0b69c043 837dec00 cmp dword ptr [rbp-14h],0 comparaison de 0 avec 1 ou 0 00007ffe`0b69c047 75cc jne 00007ffe`0b69c015 jmp first_label si [rbp-14h] != 0 00007ffe`0b69c049 8b45fc mov eax,dword ptr [rbp-4] Mise en place du résultat dans eax
Niveau performance, il devrait être assez proche du code natif produit par le C. Il est plus fastidieux notamment pour le test de la boucle mais ce n'est pas aussi affreux que le code natif produit par VB6.
Attention cette petite comparaison ne veut pas du tout dire que le C# est presque aussi rapide que le C... Il y a plein d'autres facteurs, trop pour en parler ici.
Java au prochain épisode ?le 30/01/2021 à 15:05 -
SodiumMembre extrêmement actifPar sûr ils veulent dire qu'un programme plante à la compilation s'il y a une erreur, un langage interprété il faut utiliser une librairie pour çale 01/04/2020 à 18:16
-
FatAgnusMembre chevronnéTu confonds courbe d'apprentissage et productivité. Le langage Perl est certainement plus difficile à apprendre que le langage Python, mais une fois le langage Perl maîtrisé, un développeur Perl écrira un programme en Perl beaucoup plus rapidement que le même programme qu'en langage C.le 02/04/2020 à 9:24
-
moldaviInactifBonjour.
Le jour où l'auteur comprendra que les OS ne sont pas temps réel, il va se faire de sacrés nœuds au cerveau, en plus du JIT.le 10/05/2020 à 5:42 -
StringBuilderExpert éminentJe ne suis pas d'accord.
L'article ne parle QUE de JavaScript et de problématiques PUREMENT JAVASCRIPT.
Le meilleur exemple, c'est que le JIT est expliqué comme mode de fonctionnement du Java (le .class, le bytecode, etc.)
Et pourtant, il ne fait que comparer le JIT merdique de JavaScript avec... Java ! Qui tout d'un coup ne serait plus JIT, comme par enchantement !
Si JavaScript n'est pas optimisable à cause du JIT, et que c'est le principe même du JIT qui pose problème, alors Java souffre du même problème (tout comme .NET qui, si je ne m'abuse, est le père du JIT, je trouve ça assez surprenant qu'il ne soit même pas mentionné).
Enfin, je ne suis pas d'accord avec la description du JIT : faite dans l'article. Le bytecode n'est pas interprété (ce que faisait VB6 ou Java 1.4) mais bien compilé en code natif au moment du premier appel (ce que fait .NET depuis la version 1.0).le 13/05/2020 à 16:54