Le chemin vers la vérification de type de 4 millions de lignes de code Python. Partie 2

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

Le chemin vers la vérification de type de 4 millions de lignes de code Python. Partie 2

Lire la première partie

Prise en charge officielle des types (PEP 484)

Nous avons mené nos premières expériences sérieuses avec mypy chez Dropbox lors de la Hack Week 2014. Hack Week est un événement d'une semaine organisé par Dropbox. Pendant ce temps, les collaborateurs peuvent travailler sur ce qu’ils veulent ! Certains des projets technologiques les plus célèbres de Dropbox ont débuté lors d'événements comme celui-ci. À la suite de cette expérience, nous avons conclu que mypy semble prometteur, même si le projet n’est pas encore prêt à être utilisé à grande échelle.

À l’époque, l’idée de standardiser les systèmes d’indications de type Python était dans l’air. Comme je l'ai dit, depuis Python 3.0, il était possible d'utiliser des annotations de type pour les fonctions, mais ce n'étaient que des expressions arbitraires, sans syntaxe ni sémantique définies. Lors de 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 standardisation de la sémantique. Ce travail a conduit à l’émergence PPE 484 (Guido van Rossum, Łukasz Langa et moi avons collaboré à ce document).

Nos motivations peuvent être vues de deux côtés. Premièrement, nous espérions que l'ensemble de l'écosystème Python pourrait adopter une approche commune pour utiliser les indices de type (un terme utilisé en Python comme l'équivalent des « annotations de type »). Compte tenu des risques possibles, cette solution serait préférable à l’utilisation de nombreuses approches incompatibles entre elles. Deuxièmement, nous voulions discuter ouvertement des mécanismes d’annotation de types avec de nombreux membres de la communauté Python. Ce désir était en partie dicté par le fait que nous ne voulions pas ressembler à des « apostats » des idées de base du langage aux yeux des larges masses de programmeurs Python. Il s'agit d'un langage typé dynamiquement, connu sous le nom de « typage canard ». Dans la communauté, au tout début, une attitude quelque peu méfiante à l'égard de l'idée de typage statique ne pouvait que surgir. Mais ce sentiment a fini par s’estomper après qu’il est devenu clair que la saisie statique n’allait pas être obligatoire (et après que les gens ont réalisé que c’était réellement utile).

La syntaxe des indices de type qui a finalement été adoptée était très similaire à celle prise en charge par mypy à l'époque. PEP 484 est sorti avec Python 3.5 en 2015. Python n'était plus un langage typé dynamiquement. J'aime considérer cet événement comme une étape importante dans l'histoire de Python.

Début de la migration

Fin 2015, Dropbox a créé une équipe de trois personnes pour travailler sur mypy. Ils comprenaient Guido van Rossum, Greg Price et David Fisher. À partir de ce moment, la situation a commencé à évoluer extrêmement rapidement. Le premier obstacle à la croissance de mypy était la performance. Comme je l'ai laissé entendre ci-dessus, au début du projet, j'avais pensé à traduire l'implémentation de mypy en C, mais cette idée a été rayée de la liste pour l'instant. Nous étions obligés d'exécuter le système à l'aide de l'interpréteur CPython, qui n'est pas assez rapide pour des outils comme mypy. (Le projet PyPy, une implémentation alternative de Python avec un compilateur JIT, ne nous a pas aidé non plus.)

Heureusement, certaines améliorations algorithmiques nous sont venues en aide ici. Le premier « accélérateur » puissant a été la mise en œuvre d’une vérification incrémentielle. L'idée derrière cette amélioration était simple : si toutes les dépendances du module n'ont pas changé depuis l'exécution précédente de mypy, alors nous pouvons utiliser les données mises en cache lors de l'exécution précédente tout en travaillant avec les dépendances. Il nous suffisait d'effectuer une vérification de type sur les fichiers modifiés et sur les fichiers qui en dépendaient. Mypy est même allé un peu plus loin : si l'interface externe d'un module ne changeait pas, mypy supposait que les autres modules ayant importé ce module n'avaient pas besoin d'être vérifiés à nouveau.

La vérification incrémentielle nous a beaucoup aidé lors de l'annotation de grandes quantités de code existant. Le fait est que ce processus implique généralement de nombreuses exécutions itératives de mypy à mesure que les annotations sont progressivement ajoutées au code et progressivement améliorées. La première exécution de mypy était encore très lente car il y avait beaucoup de dépendances à vérifier. Ensuite, 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 pour l'intégralité de la base de code à partir du référentiel centralisé. Il effectue ensuite une vérification incrémentielle à l'aide de cet instantané. Cela nous a fait un grand pas de plus vers l'augmentation des performances de mypy.

Ce fut une période d’adoption rapide et naturelle de la vérification de type chez Dropbox. Fin 2016, nous disposions déjà d’environ 420000 XNUMX lignes de code Python avec des annotations de type. De nombreux utilisateurs étaient enthousiasmés par la vérification de type. De plus en plus d'équipes de développement utilisaient Dropbox mypy.

Tout semblait bien alors, mais il nous restait encore beaucoup à faire. Nous avons commencé à mener des enquêtes périodiques auprès des utilisateurs internes afin d'identifier les domaines problématiques du projet et de comprendre quels problèmes doivent être résolus en premier (cette pratique est encore utilisée dans l'entreprise aujourd'hui). Il est apparu clairement que les plus importantes étaient deux tâches. Premièrement, nous avions besoin d'une plus grande couverture de type du code, deuxièmement, nous avions besoin de mypy pour travailler plus rapidement. Il était absolument clair que notre travail visant à 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 efforcés de les résoudre.

Plus de productivité !

Les vérifications incrémentielles ont rendu mypy plus rapide, mais l'outil n'était toujours pas assez rapide. De nombreuses vérifications incrémentielles ont duré environ une minute. La raison en était les importations cycliques. Cela ne surprendra probablement personne ayant travaillé avec de grandes bases de code écrites en Python. Nous avions des ensembles de centaines de modules, dont chacun importait indirectement tous les autres. Si un fichier d'une boucle d'importation était modifié, mypy devait traiter tous les fichiers de cette boucle, et souvent tous les modules qui importaient des modules de cette boucle. L’un de ces cycles a été le tristement célèbre « enchevêtrement de dépendances » qui a causé beaucoup de problèmes chez Dropbox. Autrefois cette structure contenait plusieurs centaines de modules, tandis qu'elle importait, directement ou indirectement, de nombreux tests, elle était également utilisée dans le code de production.

Nous avons envisagé la possibilité de « démêler » les dépendances circulaires, mais nous n’avions pas les ressources pour le faire. Il y avait trop de code que nous ne connaissions pas. En conséquence, nous avons proposé une approche alternative. Nous avons décidé de faire fonctionner mypy rapidement même en présence d'« enchevêtrements de dépendances ». Nous avons atteint cet objectif en utilisant le démon mypy. Un démon est un processus serveur qui implémente deux fonctionnalités intéressantes. Premièrement, il stocke des informations sur l’ensemble de la base de code en mémoire. Cela signifie que chaque fois que vous exécutez mypy, vous n'avez pas besoin de charger les données mises en cache liées à des milliers de dépendances importées. Deuxièmement, il analyse soigneusement, au niveau des petites unités structurelles, les dépendances entre les fonctions et les autres entités. Par exemple, si la fonction foo appelle une fonction bar, alors il y a une dépendance foo à partir de bar. Lorsqu'un fichier est modifié, le démon traite d'abord, de manière isolée, uniquement le fichier modifié. Il examine ensuite les modifications visibles de l'extérieur apportées à ce fichier, telles que les signatures de fonction modifiées. Le démon utilise des informations détaillées sur les importations uniquement pour revérifier les fonctions qui utilisent réellement la fonction modifiée. Généralement, avec cette approche, vous devez vérifier très peu de fonctions.

Implémenter tout cela n'a pas été facile, car l'implémentation originale de mypy était fortement axée sur le traitement d'un fichier à la fois. Nous avons dû faire face à de nombreuses situations limites, dont la survenance nécessitait des contrôles répétés en cas de modification du code. Par exemple, cela se produit lorsqu’une classe se voit attribuer une nouvelle classe de base. Une fois que nous avons fait ce que nous voulions, nous avons pu réduire le temps d’exécution de la plupart des vérifications incrémentielles à quelques secondes seulement. Cela nous a semblé être une grande victoire.

Encore plus de productivité !

Avec la mise en cache à distance dont j'ai parlé ci-dessus, le démon mypy a presque complètement résolu les problèmes qui surviennent lorsqu'un programmeur exécute fréquemment une vérification de type, apportant des modifications à un petit nombre de fichiers. Cependant, les performances du système dans le cas d’utilisation le moins favorable étaient encore loin d’être optimales. Un démarrage propre de mypy peut prendre plus de 15 minutes. Et c’était bien plus que ce dont nous aurions été satisfaits. Chaque semaine, la situation empirait à mesure que les programmeurs continuaient à écrire du nouveau code et à ajouter des annotations au code existant. Nos utilisateurs étaient toujours avides de performances, mais nous étions heureux de les rencontrer à mi-chemin.

Nous avons décidé de revenir à l'une des idées précédentes concernant mypy. À savoir, pour convertir le code Python en code C. Expérimenter avec Cython (un système qui permet de traduire du code écrit en Python en code C) ne nous a donné aucune accélération visible, nous avons donc décidé de relancer l'idée d'écrire notre propre compilateur. Étant donné que la base de code mypy (écrite en Python) contenait déjà toutes les annotations de type nécessaires, nous avons pensé qu'il serait intéressant d'essayer d'utiliser ces annotations pour accélérer le système. J'ai rapidement créé un prototype pour tester cette idée. Il a montré une performance plus de 10 fois supérieure sur divers micro-benchmarks. Notre idée était de compiler des modules Python en modules C à l'aide de Cython et de transformer les annotations de type en vérifications de type au moment de l'exécution (généralement, les annotations de type sont ignorées au moment de l'exécution et utilisées uniquement par les systèmes de vérification de type). Nous avions en fait prévu de traduire l'implémentation mypy de Python dans un langage conçu pour être typé statiquement, qui ressemblerait (et, pour la plupart, fonctionnerait) exactement comme Python. (Ce type de migration multilingue est devenu une sorte de tradition du projet mypy. L'implémentation originale de mypy a été écrite en Alore, puis il y avait un hybride syntaxique de Java et Python).

Se concentrer sur l'API de l'extension CPython était essentiel pour ne pas perdre les capacités de gestion de projet. Nous n'avons pas eu besoin d'implémenter une machine virtuelle ou des bibliothèques dont mypy avait besoin. De plus, nous aurions toujours accès à tout l’écosystème Python et à tous les outils (comme pytest). Cela signifiait que nous pouvions continuer à utiliser du code Python interprété pendant le développement, ce qui nous permettait de continuer à travailler avec un modèle très rapide consistant à apporter des modifications au code et à le tester, plutôt que d'attendre que le code soit compilé. Il semblait que nous faisions un excellent travail en étant assis sur deux chaises, pour ainsi dire, et nous avons adoré.

Le compilateur, que nous avons appelé mypyc (car il utilise mypy comme frontal pour analyser les types), s'est avéré être un projet très réussi. Dans l'ensemble, nous avons obtenu une accélération d'environ 4 fois pour les exécutions fréquentes de mypy sans mise en cache. Le développement du cœur du projet mypyc a pris environ 4 mois à une petite équipe composée de Michael Sullivan, Ivan Levkivsky, Hugh Hahn et moi-même. Cette quantité de travail était bien inférieure à ce qui aurait été nécessaire pour réécrire mypy, par exemple en C++ ou Go. Et nous avons dû apporter beaucoup moins de modifications au projet que si nous l'avions réécrit dans une autre langue. Nous espérions également pouvoir amener mypyc à un niveau tel que d'autres programmeurs Dropbox pourraient l'utiliser pour compiler et accélérer leur code.

Pour atteindre ce niveau de performance, nous avons dû appliquer des solutions d'ingénierie intéressantes. Ainsi, le compilateur peut accélérer de nombreuses opérations en utilisant des constructions C rapides et de bas niveau. Par exemple, un appel de fonction compilé est traduit en appel de fonction C. Et un tel appel est beaucoup plus rapide que l’appel d’une fonction interprétée. Certaines opérations, telles que les recherches dans un dictionnaire, impliquaient toujours l'utilisation d'appels C-API réguliers de CPython, qui n'étaient que légèrement plus rapides une fois compilés. Nous avons pu éliminer la charge supplémentaire sur le système créée par l'interprétation, mais dans ce cas, cela n'a apporté qu'un faible gain en termes de performances.

Pour identifier les opérations « lentes » les plus courantes, nous avons effectué un profilage de code. Armés de ces données, nous avons essayé soit de modifier mypyc afin qu'il génère du 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 assez simple pour ce problème ou un autre) . La réécriture du code Python était souvent une solution plus simple au problème que de demander au compilateur d'effectuer automatiquement la même transformation. À long terme, nous voulions automatiser bon nombre de ces transformations, mais à l'époque, nous nous concentrions sur l'accélération de mypy avec un minimum d'effort. Et en progressant vers cet objectif, nous avons pris plusieurs raccourcis.

A suivre ...

Chers lecteurs, Quelles ont été vos impressions sur le projet mypy lorsque vous avez appris son existence ?

Le chemin vers la vérification de type de 4 millions de lignes de code Python. Partie 2
Le chemin vers la vérification de type de 4 millions de lignes de code Python. Partie 2

Source: habr.com

Ajouter un commentaire