{"id":84900,"date":"2020-10-14T13:10:43","date_gmt":"2020-10-14T13:10:43","guid":{"rendered":"https:\/\/blog.jetbrains.com\/?post_type=kotlin&#038;p=84900"},"modified":"2020-10-15T06:29:46","modified_gmt":"2020-10-15T06:29:46","slug":"les-secrets-caches-de-la-compilation-rapide-pour-kotlin","status":"publish","type":"kotlin","link":"https:\/\/blog.jetbrains.com\/fr\/kotlin\/2020\/10\/les-secrets-caches-de-la-compilation-rapide-pour-kotlin\/","title":{"rendered":"Les secrets cach\u00e9s de la compilation rapide pour Kotlin"},"content":{"rendered":"<p>Il est difficile de compiler rapidement beaucoup de code, surtout lorsque le compilateur doit effectuer des analyses complexes telles que la r\u00e9solution de surcharge et l&#8217;inf\u00e9rence de type avec des g\u00e9n\u00e9riques. Dans cet article, j&#8217;aborde un aspect peu visible mais essentiel de Kotlin qui rend beaucoup plus rapide sa compilation sur les petites modifications qui se produisent souvent dans les activit\u00e9s quotidiennes d&#8217;ex\u00e9cution\/test\/d\u00e9bogage.<\/p>\n<\/p>\n<p>Je profite de cette occasion pour vous informer que nous recherchons des D\u00e9veloppeurs Seniors pour rejoindre l&#8217;\u00e9quipe de JetBrains qui travaille sur la compilation rapide pour Kotlin. Si vous \u00eates int\u00e9ress\u00e9\u00b7e, consultez l&#8217;offre d&#8217;emploi qui figure <a href=\"https:\/\/blog.jetbrains.com\/kotlin\/2020\/09\/the-dark-secrets-of-fast-compilation-for-kotlin\/#join-the-team\">\u00e0 la fin de cet article<\/a>.<\/p>\n<p><span id=\"more-78378\"><\/span><\/p>\n<h3><strong>Commen\u00e7ons par l&#8217;incontournable <a href=\"https:\/\/xkcd.com\/303\/\" target=\"_blank\" rel=\"noopener\">bande dessin\u00e9e XKCD #303<\/a><\/strong><\/h3>\n<p><img decoding=\"async\" src=\"https:\/\/imgs.xkcd.com\/comics\/compiling.png\" alt=\"Bande dessin\u00e9e XKCD #303\u00a0: Compiler\" \/><\/p>\n<p>Cet article traite d&#8217;un aspect tr\u00e8s important du travail de tout d\u00e9veloppeur\u00a0: <em>combien de temps faut-il pour effectuer un test (ou juste atteindre la premi\u00e8re ligne de votre programme) apr\u00e8s avoir effectu\u00e9 une modification dans le code.<\/em> C&#8217;est ce qu&#8217;on appelle le <strong>temps de test<\/strong>.<\/p>\n<p>Pourquoi est-ce si important\u00a0:<\/p>\n<ul>\n<li>Si la dur\u00e9e du test est trop courte, vous n&#8217;avez m\u00eame pas le temps de prendre un caf\u00e9 (ou de commencer un combat \u00e0 l&#8217;\u00e9p\u00e9e avec un coll\u00e8gue),<\/li>\n<li>Si elle est trop longue, vous vous perdez sur les r\u00e9seaux sociaux ou \u00eates distrait\u00b7e par autre chose et finalement vous en arrivez \u00e0 oublier quelle modification vous avez apport\u00e9e.<\/li>\n<\/ul>\n<p>On peut consid\u00e9rer que les deux situations ont leurs avantages et leurs inconv\u00e9nients, mais je pense qu&#8217;il est pr\u00e9f\u00e9rable de faire des pauses quand vous le souhaitez et non lorsque votre compilateur vous l&#8217;impose. Les compilateurs sont des logiciels intelligents, mais pas au point de pouvoir d\u00e9finir les meilleurs horaires de travail pour les humains.<\/p>\n<p>En tant que d\u00e9veloppeurs, nous sommes g\u00e9n\u00e9ralement plus satisfaits lorsque nous nous sentons <strong>productifs<\/strong>. Les pauses dues \u00e0 la compilation interrompent le flow et nous donnent l&#8217;impression d&#8217;\u00eatre bloqu\u00e9s, stopp\u00e9s dans notre \u00e9lan, improductifs. Peu de gens appr\u00e9cient cela.<\/p>\n<h3>Pourquoi la compilation prend-elle autant de temps\u00a0?<\/h3>\n<p>Il y a g\u00e9n\u00e9ralement trois raisons principales pour lesquelles les temps de compilation sont longs\u00a0:<\/p>\n<ol>\n<li>La taille de la base de code\u00a0: la compilation d&#8217;un MLOC prend g\u00e9n\u00e9ralement plus de temps que celle d&#8217;un KLOC.<\/li>\n<li>Le niveau d&#8217;optimisation de votre cha\u00eene d&#8217;outils, ce qui inclut le compilateur lui-m\u00eame et tous les outils de build que vous utilisez.<\/li>\n<li>Le niveau d&#8217;intelligence du compilateur\u00a0: trouve-t-il beaucoup d\u2019\u00e9l\u00e9ments seul ou a-t-il constamment besoin de l&#8217;utilisateur et de code standard .<\/li>\n<\/ol>\n<p>Les deux premiers facteurs \u00e9tant assez \u00e9vidents, parlons plut\u00f4t du troisi\u00e8me\u00a0: l&#8217;intelligence du compilateur. Avec Kotlin, nous avons opt\u00e9 pour un code lisible, propre et s\u00fbr. Cela implique que le compilateur puisse \u00eatre assez intelligent, et voici pourquoi.<\/p>\n<h3>La position de Kotlin<\/h3>\n<p>Kotlin est con\u00e7u pour \u00eatre utilis\u00e9 dans un cadre &#8220;industriel&#8221;, pour des projets qui ont une longue dur\u00e9e de vie, \u00e9voluent et impliquent beaucoup de monde. Nous voulons donc une s\u00e9curit\u00e9 de type statique pour d\u00e9tecter rapidement les bugs et disposer d&#8217;outils pr\u00e9cis (saisie semi-automatique, refactorisations, recherche d&#8217;utilisations dans l&#8217;EDI, navigation pr\u00e9cise dans le code, etc.). Nous souhaitons \u00e9galement avoir un code propre et lisible, sans &#8220;bruit&#8221; inutile. C&#8217;est pourquoi, entre autres, nous ne voulons pas de types partout dans le code et avons des algorithmes d&#8217;inf\u00e9rence de type et de r\u00e9solution de surcharge intelligents qui prennent en charge les fonctions de types lambdas et extensions, la conversion intelligente (typage bas\u00e9 sur le flux), etc. Dans le m\u00eame temps, le compilateur Kotlin trouve un grand nombre d&#8217;\u00e9l\u00e9ments par lui-m\u00eame pour garder le code propre.<\/p>\n<h3>Un compilateur peut-il \u00eatre intelligent et rapide \u00e0 la fois\u00a0?<\/h3>\n<p>Pour qu&#8217;un compilateur intelligent fonctionne rapidement, vous devez imp\u00e9rativement optimiser chaque \u00e9l\u00e9ment de la cha\u00eene d&#8217;outils, et c&#8217;est un \u00e9l\u00e9ment sur lequel nous travaillons en permanence. Nous travaillons entre autres sur un compilateur Kotlin nouvelle g\u00e9n\u00e9ration qui fonctionnera beaucoup plus vite que le compilateur actuel. Mais il ne s&#8217;agit pas de cela dans cet article.<\/p>\n<p>Aussi rapide que soit un compilateur, il ne sera jamais trop rapide pour un gros projet. Et recompiler l&#8217;ensemble de la base de code sur chaque petit changement effectu\u00e9 pendant le d\u00e9bogage repr\u00e9sente une perte de temps \u00e9norme. Nous essayons donc de r\u00e9utiliser autant que possible les compilations pr\u00e9c\u00e9dentes et de ne compiler que lorsque c&#8217;est absolument n\u00e9cessaire.<\/p>\n<p>Il y a principalement deux approches pour r\u00e9duire la quantit\u00e9 de code \u00e0 recompiler\u00a0:<\/p>\n<ul>\n<li><strong> \u00c9viter la compilation<\/strong>\u00a0: ne recompiler que les <strong>modules<\/strong> affect\u00e9s,<\/li>\n<li><strong>La compilation incr\u00e9mentale<\/strong> : ne recompiler que les <strong>fichiers<\/strong> affect\u00e9s.<\/li>\n<\/ul>\n<p>(On pourrait penser \u00e0 une approche encore plus pr\u00e9cise qui permettrait de suivre les changements dans les fonctions ou les classes individuelles et donc de recompiler encore moins qu&#8217;un fichier, mais je ne vois pas de mise en \u0153uvre pratique pour une telle approche dans les langages industriels, et dans l&#8217;ensemble cela ne semble pas n\u00e9cessaire.)<\/p>\n<p>Examinons maintenant plus en d\u00e9tail les deux approches.<\/p>\n<h2>\u00c9viter de compiler<\/h2>\n<p>Cette approche consiste principalement \u00e0\u00a0:<\/p>\n<ul>\n<li>Trouver les fichiers modifi\u00e9s (\u00ab\u00a0dirty files\u00a0\u00bb)<\/li>\n<li>Recompiler les <strong>modules<\/strong> auxquels ces fichiers appartiennent (en utilisant les r\u00e9sultats de la compilation pr\u00e9c\u00e9dente des autres modules comme d\u00e9pendances binaires)<\/li>\n<li>D\u00e9terminer quels autres modules peuvent \u00eatre affect\u00e9s par les modifications\n<ul>\n<li>Les recompiler \u00e9galement et v\u00e9rifier aussi leurs ABI<\/li>\n<li>R\u00e9p\u00e9ter l&#8217;op\u00e9ration jusqu&#8217;\u00e0 ce que tous les modules concern\u00e9s soient recompil\u00e9s<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>L&#8217;algorithme est plus ou moins facile \u00e0 comprendre si vous savez comment comparer les ABI. Sinon, il faut recompiler les modules qui ont \u00e9t\u00e9 affect\u00e9s par les modifications. Bien entendu, une modification d&#8217;un module dont personne ne d\u00e9pend se compilera plus rapidement qu&#8217;une modification dans le module \u00ab\u00a0util\u00a0\u00bb dont tout le monde d\u00e9pend (si cela affecte son ABI).<\/p>\n<h3>Suivi les modifications de l&#8217;ABI<\/h3>\n<p>Une ABI (interface binaire d&#8217;application) \u00e9quivaut \u00e0 une API mais pour les binaires. L&#8217;ABI est la seule partie du binaire qui int\u00e9resse les modules d\u00e9pendants (Kotlin ayant une <em>compilation s\u00e9par\u00e9e<\/em>, mais nous n&#8217;aborderons pas ce point ici). <\/p>\n<p>En gros, un binaire Kotlin (que ce soit un fichier de classe JVM ou un KLib) contient des d\u00e9clarations et des corps de fonctions. D&#8217;autres modules peuvent r\u00e9f\u00e9rencer des d\u00e9clarations, mais pas toutes les d\u00e9clarations. Ainsi, les classes priv\u00e9es et les membres, par exemple, ne font pas partie de l&#8217;ABI. Un corps de fonction peut-il faire partie d&#8217;une ABI\u00a0? Oui, si ce corps est inline \u00e0 l&#8217;endroit de l&#8217;appel. Kotlin dispose de fonctions inline et de constantes de temps de compilation (\u00ab\u00a0const\u00a0val\u00a0\u00bb). Si un corps d&#8217;une fonction inline ou une valeur d&#8217;une constante change, il peut \u00eatre n\u00e9cessaire de recompiler les modules d\u00e9pendants.<\/p>\n<p>Ainsi, l&#8217;ABI d&#8217;un module Kotlin est constitu\u00e9 de d\u00e9clarations, de corps de fonctions inline et de valeurs \u00ab\u00a0const val\u00a0\u00bb visibles \u00e0 partir d&#8217;autres modules.<\/p>\n<p>Pour d\u00e9tecter facilement une modification de l&#8217;ABI\u00a0:<\/p>\n<ul>\n<li>Stockez l&#8217;ABI de la compilation pr\u00e9c\u00e9dente (stockez les hachages par souci d&#8217;efficacit\u00e9),<\/li>\n<li>Apr\u00e8s avoir compil\u00e9 un module, comparez le r\u00e9sultat avec l&#8217;ABI stock\u00e9e\u00a0:\n<ul>\n<li>Si le r\u00e9sultat est le m\u00eame, c&#8217;est fini\u00a0;<\/li>\n<li>Sinon, recompilez les modules d\u00e9pendants.<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<h3>Avantages et inconv\u00e9nients de l&#8217;\u00e9vitement de compilation<\/h3>\n<p>Le principal avantage de l&#8217;\u00e9vitement de compilation est sa relative simplicit\u00e9.<\/p>\n<p>Cette approche est vraiment utile lorsque les modules sont petits, car l&#8217;unit\u00e9 de recompilation est un module entier. Pour les gros modules, les recompilations seront longues. \u00c9viter la compilation requiert donc d&#8217;avoir un nombre important de petits modules et ce n&#8217;est pas forc\u00e9ment ce qu&#8217;un d\u00e9veloppeurs veut. Les petits modules ne sont pas n\u00e9cessairement mal con\u00e7us, mais je pr\u00e9f\u00e8re structurer mon code pour les personnes et non pour les machines.<\/p>\n<p>Par ailleurs, de nombreux projets ont un module \u00ab\u00a0util\u00a0\u00bb qui comprend de nombreuses petites fonctions utiles. Et pratiquement tous les autres modules d\u00e9pendent de ce module \u00ab\u00a0util\u00a0\u00bb, au moins de fa\u00e7on transitoire. Supposons maintenant que je veuille ajouter une autre petite fonction utile qui est utilis\u00e9e trois fois dans ma base de code. Elle s&#8217;ajoute au module ABI, tous les modules d\u00e9pendants sont affect\u00e9s et je me retrouve \u00e0 attendre car tout mon projet est recompil\u00e9.<\/p>\n<p>De plus, en ayant beaucoup de petits modules (chacun d&#8217;eux d\u00e9pendant de plusieurs autres) la configuration de mon projet peut devenir \u00e9norme car chaque module comporte un ensemble unique de d\u00e9pendances (sources et binaires). La configuration de chaque module dans Gradle prend normalement entre 50 et 100\u00a0ms. Il n&#8217;est pas rare que les grands projets comportent plus de 1\u00a0000\u00a0modules, le temps total de configuration peut alors largement d\u00e9passer une minute. Et la configuration doit \u00eatre ex\u00e9cut\u00e9e pour chaque build et \u00e0 chaque fois que le projet est import\u00e9 dans l&#8217;EDI (par exemple lorsqu&#8217;une nouvelle d\u00e9pendance est ajout\u00e9e). <\/p>\n<p>Un certain nombre de fonctionnalit\u00e9s de Gradle att\u00e9nuent quelques-uns des inconv\u00e9nients li\u00e9s \u00e0 l&#8217;\u00e9vitement de compilation\u00a0: par exemple, les configurations peuvent \u00eatre mises en cache. Cependant, la marge d&#8217;am\u00e9lioration reste encore assez importante et c&#8217;est pourquoi nous utilisons la compilation incr\u00e9mentale dans Kotlin.<\/p>\n<h2>La compilation incr\u00e9mentale<\/h2>\n<p>La compilation incr\u00e9mentale est une approche plus granulaire que l&#8217;\u00e9vitement de compilation : elle s&#8217;applique aux fichiers individuels plut\u00f4t qu&#8217;aux modules. En cons\u00e9quence, elle ne se pr\u00e9occupe pas de la taille des modules et ne recompile pas l&#8217;ensemble du projet lorsqu&#8217;un ABI d&#8217;un module \u00ab\u00a0populaire\u00a0\u00bb est modifi\u00e9 de mani\u00e8re insignifiante. En g\u00e9n\u00e9ral, cette approche est moins restrictive pour l&#8217;utilisateur et r\u00e9duit le temps de test. Les d\u00e9veloppeurs sont ainsi moins enclins \u00e0 se lancer dans des combats l&#8217;\u00e9p\u00e9e.<\/p>\n<p>La compilation incr\u00e9mentale est depuis toujours prise en charge par JPS, le syst\u00e8me de build int\u00e9gr\u00e9 d&#8217;IntelliJ. Gradle ne prend en charge que les \u00e9vitements de compilation par d\u00e9faut. \u00c0 partir de la version 1.4, le plugin Kotlin Gradle apporte \u00e0 Gradle une impl\u00e9mentation quelque peu limit\u00e9e de la compilation incr\u00e9mentale et la marge de progression est encore importante.<\/p>\n<p>Id\u00e9alement, il suffirait d&#8217;examiner les fichiers modifi\u00e9s, de d\u00e9terminer exactement quels fichiers en d\u00e9pendent et de recompiler tous ces fichiers. Cela peut sembler facile, mais en r\u00e9alit\u00e9, il n&#8217;est pas du tout \u00e9vident de d\u00e9terminer avec pr\u00e9cision cet ensemble de fichiers d\u00e9pendants. En effet, il peut y avoir des d\u00e9pendances circulaires entre les fichiers sources, ce qui n&#8217;est pas autoris\u00e9 pour les modules dans la plupart des syst\u00e8mes de build modernes. De plus, les d\u00e9pendances des fichiers individuels ne sont pas d\u00e9clar\u00e9es explicitement. Notez que les importations ne sont pas suffisantes pour d\u00e9terminer les d\u00e9pendances en raison des r\u00e9f\u00e9rences au m\u00eame paquet et aux m\u00eames appels de cha\u00eene\u00a0: pour A.b.c(), nous devons importer au maximum A, mais les changements au niveau de B nous affecteront \u00e9galement. <\/p>\n<p>En raison de toutes ces complications, la compilation incr\u00e9mentale tente de d\u00e9terminer approximativement l&#8217;ensemble des fichiers concern\u00e9s en proc\u00e9dant en plusieurs cycles qui consistent \u00e0\u00a0:<\/p>\n<ul>\n<li>Trouver les fichiers modifi\u00e9s (\u00ab\u00a0dirty files\u00a0\u00bb)<\/li>\n<li>Les recompiler (en utilisant les r\u00e9sultats de la compilation pr\u00e9c\u00e9dente comme d\u00e9pendances binaires au lieu de compiler d&#8217;autres fichiers sources)<\/li>\n<li>V\u00e9rifier si l&#8217;ABI correspondant \u00e0 ces fichiers a chang\u00e9\n<ul>\n<li>Si cela n&#8217;est pas le cas, c&#8217;est bon\u00a0!<\/li>\n<li>Si c&#8217;est le cas, trouvez les fichiers affect\u00e9s par les modifications, ajoutez-les \u00e0 l&#8217;ensemble de fichiers modifi\u00e9s et recompilez<\/li>\n<li>R\u00e9p\u00e9ter l&#8217;op\u00e9ration jusqu&#8217;\u00e0 ce que l&#8217;ABI se stabilise (c&#8217;est ce qu&#8217;on appelle un \u00ab\u00a0point fixe\u00a0\u00bb)<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>Comme nous savons d\u00e9j\u00e0 comment comparer les ABI, il n&#8217;y a en fait que deux probl\u00e8mes qui peuvent se poser\u00a0:<\/p>\n<ol>\n<li>utiliser les r\u00e9sultats de la compilation pr\u00e9c\u00e9dente pour compiler un sous-ensemble arbitraire de sources, et<\/li>\n<li>identifier les fichiers affect\u00e9s par un ensemble de modifications donn\u00e9 de l&#8217;ABI.<\/li>\n<\/ol>\n<p>Les deux sont au moins en partie des fonctionnalit\u00e9s de la compilation incr\u00e9mentale de Kotlin. Examinons-les plus en d\u00e9tail.<\/p>\n<h3>Compiler les fichiers ayant \u00e9t\u00e9 modifi\u00e9s depuis la derni\u00e8re compilation (dirty files)<\/h3>\n<p>Le compilateur sait comment utiliser un sous-ensemble des r\u00e9sultats de la compilation pr\u00e9c\u00e9dente afin d&#8217;\u00e9viter la compilation des fichiers n&#8217;ayant \u00e9t\u00e9 modifi\u00e9s depuis (non dirty files) et de charger seulement les symboles d\u00e9finis dans ces derniers pour produire les binaires pour les fichiers ayant \u00e9t\u00e9 modifi\u00e9s (dirty files). Un compilateur ne serait pas forc\u00e9ment capable de faire cela en l&#8217;absence d&#8217;incr\u00e9mentalit\u00e9\u00a0: produire un gros binaire \u00e0 partir d&#8217;un module au lieu d&#8217;un petit binaire par fichier source n&#8217;est pas si courant en dehors du monde de la JVM. Et il ne s&#8217;agit pas d&#8217;une fonctionnalit\u00e9 du langage Kotlin, c&#8217;est un d\u00e9tail de l&#8217;impl\u00e9mentation de la compilation incr\u00e9mentale.<\/p>\n<p>En comparant les ABI des fichiers ayant \u00e9t\u00e9 modifi\u00e9s aux r\u00e9sultats pr\u00e9c\u00e9dents, nous pouvons d\u00e9couvrir que nous avons de la chance et qu&#8217;aucun autre cycle de recompilation n&#8217;est n\u00e9cessaire. Voici quelques exemples de modifications qui requi\u00e8rent seulement la recompilation des fichiers ayant \u00e9t\u00e9 modifi\u00e9s (dirty files) car l&#8217;ABI n&#8217;est pas affect\u00e9e :<\/p>\n<ul>\n<li>Commentaires, litt\u00e9raux de cha\u00eene (sauf pour les \u00ab\u00a0const val\u00a0\u00bb) et autres\n<ul>\n<li>Exemple\u00a0: modifier quelque chose dans le r\u00e9sultat du d\u00e9bogage<\/li>\n<\/ul>\n<\/li>\n<li>Modifications se limitant aux corps de fonction qui ne sont pas inline et n&#8217;affectant pas l&#8217;inf\u00e9rence du type de retour\n<ul>\n<li>Exemple\u00a0: ajouter\/supprimer un r\u00e9sultat de d\u00e9bogage ou modifier la logique interne d&#8217;une fonction<\/li>\n<\/ul>\n<\/li>\n<li>Modifications limit\u00e9es aux d\u00e9clarations priv\u00e9es (elles peuvent \u00eatre priv\u00e9es au niveau des classes ou des fichiers)\n<ul>\n<li>Exemple : introduire ou renommer une fonction priv\u00e9e<\/li>\n<\/ul>\n<\/li>\n<li>R\u00e9organisation les d\u00e9clarations<\/li>\n<\/ul>\n<p>Comme vous pouvez le constater, ces cas sont assez fr\u00e9quents lors du d\u00e9bogage et de l&#8217;am\u00e9lioration it\u00e9rative du code.<\/p>\n<h3>\u00c9tendre l&#8217;ensemble de fichiers modifi\u00e9s (dirty files)<\/h3>\n<p>Si nous ne sommes pas tr\u00e8s chanceux et qu&#8217;une d\u00e9claration a \u00e9t\u00e9 modifi\u00e9e, certains fichiers qui d\u00e9pendent de ceux qui ont \u00e9t\u00e9 modifi\u00e9s pourraient produire des r\u00e9sultats diff\u00e9rents lors de la recompilation, m\u00eame lorsque aucune ligne de leur code n&#8217;a \u00e9t\u00e9 modifi\u00e9e.<\/p>\n<p>La strat\u00e9gie la plus simple serait d&#8217;abandonner \u00e0 ce stade et de recompiler tout le module. Cela met en \u00e9vidence tous les probl\u00e8mes li\u00e9s \u00e0 l&#8217;\u00e9vitement de compilation\u00a0: les modules volumineux qui deviennent probl\u00e9matiques d\u00e8s que vous modifiez une d\u00e9claration et de nombreux petits modules qui ont \u00e9galement un co\u00fbt en terme de performance, comme d\u00e9crit plus haut. Il faut donc avoir une approche plus granulaire\u00a0: trouver les fichiers concern\u00e9s et les recompiler.<\/p>\n<p>Nous voulons donc identifier les fichiers qui d\u00e9pendent des parties de l&#8217;ABI qui ont effectivement chang\u00e9. Par exemple, si l&#8217;utilisateur a renomm\u00e9 <code>foo<\/code> en <code>bar<\/code>, il faut seulement recompiler les fichiers contenant les noms <code>foo<\/code> et <code>bar<\/code> et ne pas toucher aux autres fichiers, m\u00eame s&#8217;ils font r\u00e9f\u00e9rence \u00e0 d&#8217;autres parties de cette ABI. Le compilateur incr\u00e9mental se souvient des fichiers qui d\u00e9pendent de telle ou telle d\u00e9claration de la compilation pr\u00e9c\u00e9dente et nous pouvons utiliser ces donn\u00e9es un peu comme un graphique de d\u00e9pendance de module. L\u00e0 encore, il s&#8217;agit d&#8217;une op\u00e9ration que les compilateurs non incr\u00e9mentaux n&#8217;effectuent pas normalement.<\/p>\n<p>Id\u00e9alement, pour chaque fichier, nous devrions stocker les fichiers qui en d\u00e9pendent et les parties de l&#8217;ABI qui les int\u00e9ressent. En pratique, il est trop co\u00fbteux de stocker toutes les d\u00e9pendances avec autant de pr\u00e9cision. Et dans de nombreux cas, il est inutile de conserver des signatures compl\u00e8tes.<\/p>\n<p>Prenons cet exemple\u00a0:<\/p>\n<p><strong>Fichier\u00a0: dirty.kt<\/strong><\/p>\n<pre class=\"kotlin-code\" data-highlight-only=\"true\" theme=\"idea\" indent=\"4\" style=\"visibility: hidden; padding: 36px 0;\">\r\n\/\/ rename this to be &#039;fun foo(i: Int)&#039;\r\nfun changeMe(i: Int) = if (i == 1) 0 else bar().length\r\n<\/pre>\n<p><strong>Fichier\u00a0: clean.kt<\/strong><\/p>\n<pre class=\"kotlin-code\" data-highlight-only=\"true\" theme=\"idea\" indent=\"4\" style=\"visibility: hidden; padding: 36px 0;\">\r\nfun foo(a: Any) = &quot;&quot;\r\nfun bar() =  foo(1)\r\n<\/pre>\n<p>Supposons que l&#8217;utilisateur ait renomm\u00e9 la fonction <code>changeMe<\/code> en <code>foo<\/code>. Notez que, bien que clean.kt ne soit pas modifi\u00e9, le corps de <code>bar()<\/code> changera \u00e0 la recompilation : il appellera d\u00e9sormais <code>foo(Int)<\/code> de dirty.kt, et non plus <code>foo(Any)<\/code> de clean.kt, et son type de retour changera \u00e9galement. Il faut donc recompiler les fichiers dirty.kt et les fichiers clean.kt. Comment le compilateur incr\u00e9mental peut-il trouver cela ?<\/p>\n<p>Nous commen\u00e7ons par recompiler le fichier modifi\u00e9\u00a0: dirty.kt. Nous constatons ensuite que quelque chose a chang\u00e9 dans l&#8217;ABI\u00a0:<\/p>\n<ul>\n<li>Il n&#8217;y a plus de fonction <code>changeMe<\/code>,<\/li>\n<li>La fonction <code>foo<\/code> prend un <code>Int<\/code> et renvoie un <code>Int<\/code>.<\/li>\n<\/ul>\n<p>Nous constatons maintenant que clean.kt d\u00e9pend du nom <code>foo<\/code>. Cela signifie que nous devons recompiler <strong>les deux fichiers<\/strong> clean.kt et dirty.kt une nouvelle fois. Pourquoi\u00a0? Parce qu&#8217;on ne peut pas faire confiance aux types.<\/p>\n<p>La compilation incr\u00e9mentale doit produire le m\u00eame r\u00e9sultat qu&#8217;une recompilation compl\u00e8te de toutes les sources. Consid\u00e9rez le type de retour du nouveau <code>foo<\/code> dans dirty.kt. Il est d\u00e9duit, et en r\u00e9alit\u00e9 d\u00e9pend du type de <code>bar<\/code> de clean.kt qui est une d\u00e9pendance circulaire entre les fichiers. Le type de retour pourrait donc changer lorsque nous ajoutons clean.kt. Dans ce cas, nous obtiendrons une erreur de compilation, mais <em>jusqu&#8217;\u00e0 ce que nous recompilions clean.kt avec dirty.kt<\/em>, nous n&#8217;en savons rien.<\/p>\n<p>La r\u00e8gle majeure de la compilation incr\u00e9mentale pour Kotlin\u00a0est : <strong>ne faites confiance qu&#8217;aux noms<\/strong>. Et c&#8217;est pourquoi pour chaque fichier, nous stockons\u00a0:<\/p>\n<ul>\n<li>l&#8217;ABI qu&#8217;il produit, et<\/li>\n<li>les noms (et non les d\u00e9clarations compl\u00e8tes) qui ont \u00e9t\u00e9 recherch\u00e9s lors de sa compilation.<\/li>\n<\/ul>\n<p>Certaines optimisations sont possibles dans la fa\u00e7on dont nous stockons tout cela. Par exemple, certains noms ne sont jamais recherch\u00e9s en dehors du fichier, comme les noms des variables locales et, dans certains cas, des fonctions locales. Nous pourrions donc les exclure de l&#8217;index. Pour affiner l&#8217;algorithme, nous enregistrons les fichiers qui ont \u00e9t\u00e9 consult\u00e9s lors de la recherche de chacun des noms. Et pour compresser l&#8217;index, on utilise le hachage. Il est possible d&#8217;apporter d&#8217;autres am\u00e9liorations.<\/p>\n<p>Comme vous l&#8217;avez sans doute remarqu\u00e9, nous devons recompiler plusieurs fois l&#8217;ensemble de fichiers qui ont \u00e9t\u00e9 modifi\u00e9s. Il n&#8217;y a malheureusement pas d&#8217;autre solution\u00a0: il peut y avoir des d\u00e9pendances circulaires et seule la compilation de tous les fichiers concern\u00e9s en une seule fois donnera un r\u00e9sultat correct. Dans le pire des cas, la compilation incr\u00e9mentale peut faire plus que l&#8217;\u00e9vitement de compilation, il devrait donc y avoir des heuristiques en place pour l&#8217;\u00e9viter.<\/p>\n<h3>La compilation incr\u00e9mentale au-del\u00e0 des limites des modules<\/h3>\n<p>La compilation incr\u00e9mentale peut d\u00e9passer les limites des modules.<\/p>\n<p>Supposons que nous ayons des fichiers qui ont \u00e9t\u00e9 modifi\u00e9s dans un module, que nous fassions quelques tests et que nous atteignions un point fixe. Nous avons maintenant la nouvelle ABI de ce module et nous devons faire quelque chose pour les modules d\u00e9pendants.<\/p>\n<p>Bien s\u00fbr, nous savons quels noms ont \u00e9t\u00e9 affect\u00e9s dans l&#8217;ABI de notre module initial et quels fichiers dans les modules d\u00e9pendants ont recherch\u00e9 ces noms. Nous pourrions appliquer essentiellement le m\u00eame algorithme incr\u00e9mental mais en partant du changement d&#8217;ABI et non d&#8217;un ensemble de dirty files. Au fait, s&#8217;il n&#8217;y a pas de d\u00e9pendances circulaires entre les modules, il suffit de recompiler les fichiers d\u00e9pendants seuls. Mais si leur ABI a chang\u00e9, il faudra ajouter d&#8217;autres fichiers du m\u00eame module \u00e0 l&#8217;ensemble et recompiler de nouveau les m\u00eames fichiers.<\/p>\n<p>Impl\u00e9menter ceci dans Gradle repr\u00e9sente un v\u00e9ritable d\u00e9fi. Il faudra probablement apporter quelques modifications \u00e0 l&#8217;architecture de Gradle, mais nous savons par exp\u00e9rience que c&#8217;est possible et bien accueilli par l&#8217;\u00e9quipe Gradle.<\/p>\n<h2>Points non abord\u00e9s dans cet article<\/h2>\n<p>Mon but ici \u00e9tait de vous donner un aper\u00e7u de la fascinante machinerie de la compilation rapide pour Kotlin. Il y a beaucoup d&#8217;autres \u00e9l\u00e9ments \u00e0 prendre en compte que j&#8217;ai d\u00e9lib\u00e9r\u00e9ment laiss\u00e9s de c\u00f4t\u00e9, notamment\u00a0<\/p>\n<ul>\n<li><a href=\"https:\/\/docs.gradle.org\/current\/userguide\/build_cache.html\" target=\"_blank\" rel=\"noopener\">Cache de build<\/a><\/li>\n<li><a href=\"https:\/\/docs.gradle.org\/current\/userguide\/configuration_cache.html\" target=\"_blank\" rel=\"noopener\">Cache de configuration<\/a><\/li>\n<li><a href=\"https:\/\/docs.gradle.org\/current\/userguide\/task_configuration_avoidance.html\" target=\"_blank\" rel=\"noopener\">\u00c9viter la configuration de t\u00e2che<\/a><\/li>\n<li>Stocker efficacement les index de compilation incr\u00e9mentale et autres caches sur le disque<\/li>\n<li>La compilation incr\u00e9mentale pour les projets mixtes Kotlin+Java<\/li>\n<li>R\u00e9utilisation des structures de donn\u00e9es javac en m\u00e9moire pour \u00e9viter de lire deux fois les d\u00e9pendances Java<\/li>\n<li>L&#8217;incr\u00e9mentalit\u00e9 dans <a href=\"https:\/\/kotlinlang.org\/docs\/reference\/kapt.html\" target=\"_blank\" rel=\"noopener\">KAPT<\/a> et <a href=\"https:\/\/github.com\/android\/kotlin\/tree\/ksp\/libraries\/tools\/kotlin-symbol-processing-api\" target=\"_blank\" rel=\"noopener\">KSP<\/a><\/li>\n<li>Les file watchers pour identifier rapidement les dirty files<\/li>\n<\/ul>\n<h2>En r\u00e9sum\u00e9<\/h2>\n<p>Maintenant, vous avez une meilleure id\u00e9e des d\u00e9fis de la compilation rapide dans un langage de programmation moderne. Notez que certains langages ont d\u00e9lib\u00e9r\u00e9ment choisi de rendre leurs compilateurs peu intelligents pour \u00e9viter d&#8217;avoir \u00e0 faire tout cela. Pour le meilleur ou pour le pire, Kotlin a pris une autre voie, et il semble que les fonctionnalit\u00e9s qui rendent le compilateur Kotlin si intelligent soient celles qui sont le plus appr\u00e9ci\u00e9es des utilisateurs car elles fournissent \u00e0 la fois des abstractions puissantes et un code lisible et concis.<\/p>\n<p>Alors que nous travaillons sur un compilateur front-end de nouvelle g\u00e9n\u00e9ration qui rendra la compilation beaucoup plus rapide en repensant l&#8217;impl\u00e9mentation des algorithmes de base de contr\u00f4le de type et de r\u00e9solution de noms, nous savons que tout ce qui est d\u00e9crit dans cet article ne dispara\u00eetra jamais. L&#8217;une des raisons \u00e0 cela est l&#8217;exp\u00e9rience avec le langage de programmation Java, qui b\u00e9n\u00e9ficie des capacit\u00e9s de compilation incr\u00e9mentale d&#8217;IntelliJ IDEA m\u00eame en ayant un compilateur beaucoup plus rapide que celui de kotlinc aujourd&#8217;hui. Une autre raison est que notre objectif est de nous rapprocher le plus possible du d\u00e9veloppement des cycles des langages interpr\u00e9t\u00e9s qui b\u00e9n\u00e9ficient d&#8217;une prise en charge instantan\u00e9e des modifications sans aucune compilation. Ainsi, la strat\u00e9gie de Kotlin pour une compilation rapide consiste en : un compilateur optimis\u00e9 + une cha\u00eene d&#8217;outils performante + une incr\u00e9mentalit\u00e9 sophistiqu\u00e9e. <\/p>\n<h2 id=\"join-the-team\">Rejoignez notre \u00e9quipe!<\/h2>\n<p>Si vous aimez travailler sur ce genre de sujets, consultez l&#8217;offre d&#8217;emploi \u00e0 pourvoir actuellement au sein de l&#8217;\u00e9quipe JetBrains d\u00e9di\u00e9e \u00e0 la compilation rapide de Kotlin. Vous trouverez ici l&#8217;offre d&#8217;emploi en <a href=\"https:\/\/www.jetbrains.com\/careers\/jobs\/senior-software-developer-fast-compilation-for-kotlin-470\/\" target=\"_blank\" rel=\"noopener\">anglais<\/a> et en <a href=\"https:\/\/spb.hh.ru\/vacancy\/31673822\" target=\"_blank\" rel=\"noopener\">russe<\/a>. Une exp\u00e9rience pr\u00e9alable sur des compilateurs ou des outils de build n&#8217;est PAS requise. Nous recrutons dans tous les bureaux de JetBrains (<a href=\"https:\/\/spb.hh.ru\/vacancy\/31673822\" target=\"_blank\" rel=\"noopener\">Saint Petersbourg<\/a>, <a href=\"https:\/\/www.jetbrains.com\/careers\/jobs\/senior-software-developer-fast-compilation-for-kotlin-470\/\" target=\"_blank\" rel=\"noopener\">Munich<\/a>, <a href=\"https:\/\/www.jetbrains.com\/careers\/jobs\/senior-software-developer-fast-compilation-for-kotlin-470\/\" target=\"_blank\" rel=\"noopener\">Amsterdam<\/a>, <a href=\"https:\/\/www.jetbrains.com\/careers\/jobs\/senior-software-developer-fast-compilation-for-kotlin-470\/\" target=\"_blank\" rel=\"noopener\">Boston<\/a>, <a href=\"https:\/\/www.jetbrains.com\/careers\/jobs\/senior-software-developer-fast-compilation-for-kotlin-470\/\" target=\"_blank\" rel=\"noopener\">Prague<\/a>, <a href=\"https:\/\/spb.hh.ru\/vacancy\/38801756\" target=\"_blank\" rel=\"noopener\">Moscou<\/a> et <a href=\"https:\/\/spb.hh.ru\/vacancy\/38801762\" target=\"_blank\" rel=\"noopener\">Novossibirsk<\/a>) ou vous pouvez <a href=\"https:\/\/www.jetbrains.com\/careers\/jobs\/senior-software-developer-fast-compilation-for-kotlin-470\/\" target=\"_blank\" rel=\"noopener\"><strong> travailler \u00e0 distance<\/strong><\/a> <strong>partout dans le monde<\/strong>. N&#8217;h\u00e9sitez pas \u00e0 nous contacter\u00a0!<\/p>\n<p><em>Auteur de l&#8217;article original en anglais : <a href=\"https:\/\/blog.jetbrains.com\/author\/andrey-breslav\/\" target=\"_blank\" rel=\"noopener\">Andrey Breslav<\/a><\/em><\/p>\n","protected":false},"author":813,"featured_media":85089,"comment_status":"open","ping_status":"closed","template":"","categories":[4113],"tags":[91],"cross-post-tag":[],"acf":[],"_links":{"self":[{"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/kotlin\/84900"}],"collection":[{"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/kotlin"}],"about":[{"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/types\/kotlin"}],"author":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/users\/813"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/comments?post=84900"}],"version-history":[{"count":4,"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/kotlin\/84900\/revisions"}],"predecessor-version":[{"id":85165,"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/kotlin\/84900\/revisions\/85165"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/media\/85089"}],"wp:attachment":[{"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/media?parent=84900"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/categories?post=84900"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/tags?post=84900"},{"taxonomy":"cross-post-tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/cross-post-tag?post=84900"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}