Chemin vers la vérification des types de 4 millions de lignes de code Python. Partie 2

Aujourd'hui, nous publions la deuxième partie de la traduction d'un article sur la façon dont Dropbox a organisé le contrôle des types de plusieurs millions de lignes de code Python.

Chemin vers la vérification des types de 4 millions de lignes de code Python. Partie 2

→ Lire la première partie

Support officiel des types (PEP 484)

Nous avons mené nos premières expériences sérieuses avec mypy chez Dropbox pendant la Hack Week 2014. La Hack Week est un événement organisé par Dropbox durant une semaine où les employés peuvent travailler sur tout ce qu'ils souhaitent ! Certains des projets technologiques les plus célèbres de Dropbox ont commencé lors de ces événements. À l'issue de cette expérience, nous avons conclu que mypy semblait prometteur, bien que ce projet ne soit pas encore prêt pour une utilisation généralisée.

À l'époque, l'idée de normaliser les systèmes de suggestions de types Python flottait dans l'air. Comme je l'ai mentionné, depuis Python 3.0, il était possible d'utiliser des annotations de types pour les fonctions, mais il ne s'agissait que d'expressions aléatoires, sans syntaxe et sémantique définies. Pendant l'exécution du programme, ces annotations étaient pour la plupart simplement ignorées. Après la Hack Week, nous avons commencé à travailler sur la normalisation de la sémantique. Ce travail a conduit à la création de PEP 484 (ce document a été élaboré en collaboration avec Guido van Rossum, Łukasz Langa et moi-même).

Nos motivations pouvaient être considérées sous deux angles. Premièrement, nous espérions que tout l'écosystème Python pourrait adopter une approche commune pour l'utilisation des suggestions de types (type hints — terme utilisé en Python comme équivalent des « annotations de types »). Cela, malgré les risques possibles, serait mieux que d'utiliser de nombreuses approches incompatibles. Deuxièmement, nous souhaitions discuter ouvertement des mécanismes d'annotation des types avec de nombreux représentants de la communauté Python. En partie, ce désir était dicté par le fait que nous ne voulions pas apparaître comme des « renégats » des idées fondamentales du langage aux yeux de la grande masse des programmeurs Python. C'est un langage à typage dynamique, connu pour sa « typage de canard ». Au sein de la communauté, au début, il y avait sans doute une certaine suspicion vis-à-vis de l'idée de typage statique. Mais cette attitude a finalement diminué — une fois qu'il a été clair que le typage statique ne serait pas rendu obligatoire (et après que les gens ont compris qu'il était réellement utile).

La syntaxe des annotations de types qui a finalement été adoptée ressemblait beaucoup à celle prise en charge par mypy à l'époque. Le document PEP 484 est sorti avec Python 3.5 en 2015. Python n'était plus un langage qui ne supportait que la typage dynamique. J'aime considérer cet événement comme une étape importante dans l'histoire de Python.

Début de la migration

À la fin de 2015, une équipe de trois personnes a été créée chez Dropbox pour travailler sur mypy. Elle comprenait Guido van Rossum, Greg Price et David Fisher. À partir de ce moment, la situation a évolué très rapidement. Le premier obstacle à la croissance de mypy a été la performance. Comme je l'ai déjà évoqué, durant la phase de développement initiale du projet, j'envisageais de traduire l'implémentation de mypy en langage C, mais cette idée a été mise de côté pour le moment. Nous étions limités par le fait que le système était exécuté sur l'interpréteur CPython, qui n'a pas une vitesse suffisante pour des outils comme mypy. (Le projet PyPy, une implémentation alternative de Python avec un compilateur JIT, ne nous a pas non plus aidés.)

Heureusement, certaines améliorations algorithmiques nous ont aidés. La première amélioration majeure a été l'implémentation de la vérification incrémentielle. L'idée de cette amélioration était simple : si toutes les dépendances d'un module n'avaient pas changé depuis le dernier lancement de mypy, nous pouvions utiliser, au cours du travail sur les dépendances, les données mises en cache lors de la session précédente. Nous devions simplement effectuer une vérification des types dans les fichiers modifiés et dans ceux qui en dépendaient. Mypy est allé même un peu plus loin : si l'interface externe d'un module ne changeait pas, mypy considérait qu'il n'était pas nécessaire de vérifier à nouveau d'autres modules qui importent ce module.

La vérification incrémentielle nous a énormément aidés lors de l'annotation de grandes quantités de code existant. En effet, ce processus nécessite généralement de nombreuses exécutions itératives de mypy, car les annotations sont progressivement ajoutées au code et améliorées au fil du temps. La première exécution de mypy était encore très lente, car il fallait vérifier de nombreuses dépendances. Pour améliorer la situation, nous avons mis en place un mécanisme de mise en cache à distance. Si mypy détecte que le cache local est probablement obsolète, il télécharge l'instantané actuel du cache de l'ensemble de la base de code depuis un dépôt centralisé. Ensuite, il exécute une vérification incrémentielle en utilisant cet instantané. Cela nous a fait faire un pas significatif vers l'augmentation des performances de mypy.

Cela a été une période d'adoption rapide et naturelle du système de vérification de types chez Dropbox. À la fin de 2016, nous avions déjà environ 420000 lignes de code Python annotées. De nombreux utilisateurs ont accueilli avec enthousiasme la vérification des types. À Dropbox, mypy était de plus en plus utilisé par les équipes de développement.

Tout semblait alors bien, mais nous avions encore beaucoup à faire. Nous avons commencé à réaliser des sondages internes périodiques auprès des utilisateurs pour identifier les points problématiques du projet et comprendre quelles questions devaient être résolues en priorité (cette pratique est encore utilisée dans l'entreprise aujourd'hui). Deux tâches se sont révélées être les plus importantes. La première était d'augmenter la couverture du code par des types, et la seconde était d'accélérer le fonctionnement de mypy. Il était clair que notre travail pour accélérer mypy et le mettre en œuvre dans les projets de l'entreprise était encore loin d'être terminé. Conscients de l'importance de ces deux tâches, nous nous sommes attelés à leur résolution.

Plus de performance !

Les vérifications incrémentielles ont accéléré mypy, mais cet outil n'était toujours pas assez rapide. De nombreuses vérifications incrémentielles prenaient environ une minute. La raison en était des importations cycliques. Cela ne surprendra probablement pas ceux qui ont déjà travaillé avec de grandes bases de code écrites en Python. Nous avions des ensembles de centaines de modules, chacun d'eux important indirectement tous les autres. Si un fichier dans la boucle d'importations était modifié, mypy devait traiter tous les fichiers inclus dans cette boucle, et souvent, aussi tous les modules important des modules de cette boucle. L'une de ces boucles était le tristement célèbre « nœud de dépendances », qui a causé de nombreux désagréments chez Dropbox. Un jour, cette structure contenait plusieurs centaines de modules, importés, directement ou indirectement, par de nombreux tests, et elle était également utilisée dans le code de production.

Nous avons envisagé de « démêler » les dépendances cycliques, mais nous n'avions pas les ressources nécessaires pour cela. Il y avait trop de code avec lequel nous n'étions pas familiers. Finalement, nous avons opté pour une approche alternative. Nous avons décidé de faire en sorte que mypy fonctionne rapidement même en présence de « nœuds de dépendances ». Nous avons atteint cet objectif grâce à un démon mypy. Un démon est un processus serveur qui met en œuvre deux fonctionnalités intéressantes. Premièrement, il conserve en mémoire des informations sur toute la base de code. Cela signifie que chaque fois que mypy est lancé, il n'est pas nécessaire de charger les données mises en cache concernant des milliers de dépendances importées. Deuxièmement, il analyse minutieusement, au niveau des unités structurelles, les dépendances entre les fonctions et d'autres entités. Par exemple, si une fonction foo appelle la fonction bar, alors il existe une dépendance foo à partir de bar. Lorsque le fichier est modifié, le démon traite d'abord, de manière isolée, uniquement le fichier modifié. Ensuite, il examine les modifications de ce fichier, visibles de l'extérieur, telles que les signatures de fonctions modifiées. Le démon utilise des informations détaillées sur les imports uniquement pour revérifier les fonctions qui utilisent réellement la fonction modifiée. En général, avec cette approche, il y a très peu de fonctions à vérifier.

La mise en œuvre de tout cela n'a pas été une tâche facile, car la version initiale de mypy était fortement axée sur le traitement d'un seul fichier à la fois. Nous avons dû faire face à de nombreuses situations limites, dont l'apparition nécessitait des vérifications répétées lorsqu'une modification était apportée au code. Par exemple, cela se produit lorsque l'on assigne une nouvelle classe de base à une classe. Une fois que nous avons réalisé ce que nous voulions, nous avons pu réduire le temps d'exécution de la plupart des vérifications incrémentielles à quelques secondes. Cela nous semblait être une grande victoire.

Encore plus de performances !

Avec la mise en cache distante dont j'ai parlé précédemment, le démon mypy a pratiquement résolu les problèmes rencontrés lorsque le programmeur lance fréquemment une vérification de type en effectuant des modifications dans un nombre limité de fichiers. Cependant, la performance du système dans le scénario le moins favorable demeurait encore loin d'être optimale. Un démarrage propre de mypy pouvait prendre plus de 15 minutes. C'était bien plus que ce que nous aurions accepté. Chaque semaine, la situation empirait, car les programmeurs continuaient d'écrire du nouveau code et d'ajouter des annotations au code existant. Nos utilisateurs aspiraient toujours à une plus grande performance, et nous étions heureux de pouvoir satisfaire cette demande.

Nous avons décidé de revenir à l'une des premières idées concernant mypy. Plus précisément, à la conversion du code Python en code C. Les expériences avec Cython (qui est un système permettant de traduire du code écrit en Python en code C) ne nous ont pas apporté d'accélération visible, c'est pourquoi nous avons décidé de revitaliser l'idée d'écrire notre propre compilateur. Comme la base de code de mypy (écrite en Python) contenait déjà toutes les annotations de types nécessaires, il nous a semblé pertinent d'essayer d'utiliser ces annotations pour accélérer le fonctionnement du système. J'ai rapidement créé un prototype pour tester cette idée. Il a montré, sur différents micro-benchmarks, une augmentation de performance de plus de 10 fois. Notre idée était de compiler des modules Python en modules C à l'aide de Cython, et de transformer les annotations de types en vérifications de types effectuées lors de l'exécution du programme (en général, les annotations de types sont ignorées pendant l'exécution des programmes et ne sont utilisées que par les systèmes de vérification de types). En fait, nous prévoyions de traduire l'implémentation de mypy de Python vers un langage qui serait statiquement typé, qui semblerait (et fonctionnerait dans sa majeure partie) exactement comme Python. (Cette forme de migration interlangage est devenue en quelque sorte une tradition du projet mypy. L'implémentation initiale de mypy a été écrite en Alore, puis un hybride syntaxique de Java et de Python a été utilisé).

L'orientation vers l'API des extensions CPython a été la clé pour ne pas perdre de capacités en matière de gestion de projet. Nous n'avions pas besoin de mettre en œuvre une machine virtuelle ni les bibliothèques nécessaires à mypy. De plus, nous aurions toujours accès à tout l'écosystème Python, tous les outils (comme pytest) seraient disponibles. Cela signifiait que nous pourrions continuer à utiliser le code Python interprété au cours du développement, ce qui nous permettrait de travailler avec un processus très rapide de modification du code et de test, plutôt que d'attendre la compilation du code. Cela semblait fonctionner comme si nous réussissions à, disons, rester sur deux chaises, et cela nous plaisait.

Le compilateur que nous avons nommé mypyc (car il utilise mypy pour l'analyse des types en tant que frontend) s'est révélé être un projet très réussi. En général, nous avons atteint une accélération d'environ quatre fois pour les lancements fréquents de mypy sans utiliser de mise en cache. Le développement du noyau du projet mypyc a duré environ 4 mois civils pour une petite équipe composée de Michael Sullivan, Ivan Levkiysky, Hugh Han et moi-même. Ce volume de travail était beaucoup moins important que celui qui aurait été nécessaire pour réécrire mypy, par exemple, en C++ ou en Go. En plus, les changements que nous avons dû apporter au projet étaient bien moindres que ceux requis pour une réécriture dans un autre langage. Nous espérions également pouvoir amener mypyc à un niveau où d'autres programmeurs de Dropbox pourraient l'utiliser pour compiler et accélérer leur code.

Pour atteindre un tel niveau de performance, nous avons dû appliquer certaines solutions d'ingénierie intéressantes. Par exemple, le compilateur peut accélérer de nombreuses opérations grâce à l'utilisation de constructions bas niveau rapides en C. Ainsi, l'appel d'une fonction compilée est traduit en appel d'une fonction C. Et cet appel s'exécute beaucoup plus rapidement qu'un appel de fonction interprétée. Certaines opérations, comme la recherche dans les dictionnaires, se basaient encore sur l'utilisation des appels C-API classiques depuis CPython, qui, après compilation, n'étaient qu'un peu plus rapides. Nous avons pu éliminer la surcharge supplémentaire sur le système créée par l'interprétation, mais cela n'a donné qu'un léger gain de performance dans ce cas.

Pour identifier les opérations « lentes » les plus courantes, nous avons effectué un profilage du code. Armés des données recueillies, nous avons tenté soit d'ajuster mypyc pour qu'il génère un code C plus rapide pour de telles opérations, soit de réécrire le code Python correspondant en utilisant des opérations plus rapides (et parfois nous n'avions tout simplement pas de solution simple pour tel ou tel problème). La réécriture du code Python s'est souvent révélée être une solution plus facile que la mise en œuvre d'une exécution automatique de la même transformation dans le compilateur. À long terme, nous souhaitions automatiser de nombreuses transformations, mais à ce moment-là, notre objectif était d'accélérer mypy avec le minimum d'efforts. Et à l'approche de cet objectif, nous avons coupé quelques coins.

À suivre...

Chers lecteurs ! Quelles impressions le projet mypy a-t-il suscitées chez vous lorsque vous avez pris connaissance de son existence ?

Chemin vers la vérification des types de 4 millions de lignes de code Python. Partie 2
Chemin vers la vérification des types de 4 millions de lignes de code Python. Partie 2

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster