La façon de taper la vérification de 4 millions de lignes de code Python. 2e partie

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



Lire la première partie

Prise en charge du type formel (PEP 484)


Nous avons fait la première expérience sérieuse avec mypy sur Dropbox pendant Hack Week 2014. Hack Week est un événement organisé par Dropbox pendant une semaine. En ce moment, les employés peuvent travailler sur n'importe quoi! Certains des projets technologiques les plus célèbres de Dropbox ont commencé lors d'événements similaires. À la suite de cette expérience, nous sommes arrivés à la conclusion que mypy semble prometteur, bien que ce projet ne soit pas encore prêt pour une utilisation généralisée.

À cette époque, l'idée de standardiser les systèmes d'indices pour les types Python était dans l'air. Comme je l'ai dit, à partir de Python 3.0, vous pouvez utiliser des annotations de type pour les fonctions, mais ce ne sont que des expressions arbitraires, sans syntaxe ni sémantique spécifiques. Pendant l'exécution du programme, ces annotations, pour la plupart, ont simplement été ignorées. Après la Hack Week, nous avons commencé à travailler sur la standardisation de la sémantique. Ce travail a conduit à l'émergence du PEP 484 (Guido van Rossum, Lukas Langa et moi avons collaboré sur ce document).

Nos motivations pouvaient être vues de deux côtés. Tout d'abord, nous espérions que l'ensemble de l'écosystème Python pourrait adopter une approche générale de l'utilisation des indications de type (les indications de type sont un terme utilisé en Python comme analogue des «annotations de type»). Compte tenu des risques possibles, ce serait mieux que d'utiliser de nombreuses approches mutuellement incompatibles. Deuxièmement, nous voulions discuter ouvertement des mécanismes d'annotation de type avec de nombreux membres de la communauté Python. En partie, ce désir était dicté par le fait que nous ne voudrions pas ressembler à des «apostats» des idées de base de la langue aux yeux des larges masses de programmeurs Python. Il s'agit d'un langage typé dynamiquement appelé «typage du canard». Dans la communauté, au tout début, une attitude quelque peu méfiante à l'égard de l'idée du typage statique ne pouvait que naître. Mais cette attitude s'est finalement affaiblie - après qu'il est devenu clair que la saisie statique n'était pas prévue pour être obligatoire (et après que les gens ont réalisé qu'elle était vraiment utile).

La syntaxe résultante pour les indications de type é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 qui ne supportait que la frappe dynamique. J'aime à considérer cet événement comme une étape importante dans l'histoire de Python.

Début de la migration


Fin 2015, une équipe de trois personnes a été créée dans Dropbox pour travailler sur mypy. Il comprenait Guido van Rossum, Greg Price et David Fisher. A partir de ce moment, la situation a commencé à évoluer très rapidement. Le premier obstacle à la croissance de Mypy a été la performance. Comme je l'ai déjà laissé entendre ci-dessus, au début de la phase de développement du projet, je pensais à traduire la mise en œuvre de mypy en C, mais cette idée a été supprimée des listes jusqu'à présent. Nous sommes coincés avec le fait que nous avons utilisé l'interpréteur CPython pour démarrer le système, ce 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, ici quelques améliorations algorithmiques sont venues à notre aide. Le premier «accélérateur» puissant a été la mise en œuvre de la vérification incrémentale. L'idée de cette amélioration était simple: si toutes les dépendances du module n'ont pas changé depuis le lancement précédent de mypy, alors nous pouvons utiliser les données mises en cache lors de la session précédente tout en travaillant avec les dépendances. Tout ce que nous avions à faire était de taper les fichiers modifiés et ceux qui en dépendaient. Mypy est même allé un peu plus loin: si l'interface externe du module ne changeait pas - mypy pensait que les autres modules qui importent ce module n'avaient pas besoin d'être vérifiés à nouveau.

La validation incrémentale nous a grandement aidés à annoter de gros volumes de code existant. Le fait est que ce processus implique généralement de nombreuses exécutions itératives de mypy, car les annotations sont progressivement ajoutées au code et sont progressivement améliorées. Le premier lancement de mypy était encore très lent, car il avait besoin de vérifier beaucoup de dépendances lors de son exécution. Ensuite, pour améliorer la situation, nous avons implémenté 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é de cache actuel pour la base de code entière à partir d'un référentiel centralisé. Il effectue ensuite une vérification incrémentielle à l'aide de cet instantané. Il s'agit d'un autre grand pas qui nous a poussés à augmenter la productivité de Mypy.

Ce fut une période d'introduction rapide et naturelle du système de vérification de type Dropbox. Fin 2016, nous disposions déjà d'environ 420 000 lignes de code Python avec des annotations de type. De nombreux utilisateurs étaient enthousiastes à propos de la vérification de type. Dropbox mypy a été utilisé par de plus en plus d'équipes de développement.

Tout semblait bien alors, mais nous avions encore beaucoup à faire. Nous avons commencé à mener des enquêtes internes périodiques auprès des utilisateurs afin d'identifier les domaines problématiques du projet et de comprendre quels problèmes doivent être résolus en premier (cette pratique est utilisée dans l'entreprise aujourd'hui). Comme il est devenu clair, les plus importants étaient deux tâches. Le premier - vous aviez besoin d'une plus grande couverture de code avec les types, le second - il était nécessaire que mypy fonctionne plus rapidement. Il était parfaitement clair que notre travail sur l'accélération de mypy et sa mise en œuvre dans les projets de l'entreprise était encore loin d'être terminé. Nous, pleinement conscients de l'importance de ces deux tâches, avons repris leur solution.

Plus de performances!


Les vérifications incrémentielles ont accéléré mypy, mais cet outil n'était pas encore assez rapide. De nombreux contrôles incrémentiels ont duré environ une minute. La raison en était les importations cycliques. Cela ne surprendra probablement personne qui a travaillé avec de grandes bases de code écrites en Python. Nous avions des ensembles de centaines de modules, chacun important indirectement tous les autres. Si un fichier du cycle d'importation s'avérait être modifié, mypy devait traiter tous les fichiers inclus dans ce cycle, et souvent aussi tous les modules qui importaient des modules de ce cycle. L'un de ces cycles était l'infâme «enchevêtrement de dépendances», qui a causé beaucoup de problèmes dans Dropbox. Une fois que cette structure contenait plusieurs centaines de modules, alors qu'elle était importée, directement ou indirectement, beaucoup de tests, elle était également utilisée dans le code de production.

Nous avons envisagé la possibilité de «démêler» les dépendances cycliques, 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 adopté une approche alternative. Nous avons décidé de faire fonctionner mypy rapidement même s'il y avait des «boules de dépendance». Nous avons accompli cela avec le démon mypy. Un démon est un processus serveur qui implémente deux fonctionnalités intéressantes. Premièrement, il conserve en mémoire des informations sur l'ensemble de la base de code. Cela signifie que chaque fois que vous exécutez mypy, vous n'avez pas à télécharger de données en cache liées à des milliers de dépendances importées. Deuxièmement, il analyse attentivement, au niveau des petites unités structurelles, les relations entre les fonctions et les autres entités. Par exemple, si la fonction foo appelle la bar fonctions, alors il y a une dépendance de foo sur la bar . Lorsqu'un fichier est modifié, le démon d'abord, isolément, traite uniquement le fichier modifié. Il examine ensuite les modifications de ce fichier qui sont visibles de l'extérieur, telles que les signatures de fonction modifiées. Le démon utilise des informations d'importation détaillées uniquement pour revérifier les fonctions qui utilisent vraiment la fonction modifiée. En règle générale, avec cette approche, très peu de fonctions doivent être vérifiées.

La mise en œuvre de tout cela n'a pas été facile, car la mise en œuvre 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 l'occurrence nécessitait des vérifications répétées dans les cas où quelque chose changeait dans le code. Par exemple, cela se produit lorsqu'une nouvelle classe de base est affectée à une classe. Après avoir 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. Cela nous a paru une grande victoire.

Plus de performances!


Avec la mise en cache à distance, que j'ai décrite ci-dessus, le démon mypy a presque complètement résolu les problèmes qui surviennent lorsque le programmeur exécute souvent une vérification de type, apportant des modifications à un petit nombre de fichiers. Cependant, les performances du système dans la variante la moins favorable de son utilisation é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 que nous souhaiterions. Chaque semaine, la situation empirait, les programmeurs continuant d'écrire du nouveau code et d'ajouter des annotations au code existant. Nos utilisateurs attendaient toujours plus de performances, mais nous étions heureux d'être prêts à les rencontrer.

Nous avons décidé de revenir à l'une de mes premières idées concernant mypy. A savoir, la conversion du code Python en code C. Les expériences avec Cython (c'est un système qui vous permet de traduire du code Python en code C) ne nous ont 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, une tentative d'utiliser ces annotations pour accélérer le système semblait utile. J'ai rapidement créé un prototype pour tester cette idée. Il a montré sur divers micro-repères plus de 10 fois la productivité. Notre idée était de compiler des modules Python en modules C à l'aide d'outils Cython et de transformer les annotations de type en vérifications de type effectuées au moment de l'exécution (généralement les annotations de type sont ignorées au moment de l'exécution et ne sont utilisées que par les systèmes de vérification de type ) Nous avions en fait prévu de traduire l'implémentation mypy de Python en un langage créé typé statiquement, qui ressemblerait (et, pour la plupart, fonctionnerait) exactement à Python. (Ce type de migration multilingue est devenu une sorte de tradition du projet mypy. L'implémentation initiale de mypy a été écrite dans Alore, puis il y a eu un hybride syntaxique de Java et Python).

Se concentrer sur l'API d'extension CPython était la clé pour ne pas perdre les capacités de gestion de projet. Nous n'avions pas besoin d'implémenter une machine virtuelle ou les bibliothèques dont mypy avait besoin. De plus, l'ensemble de l'écosystème Python serait toujours à notre disposition, tous les outils (tels que pytest) seraient disponibles. Cela signifiait que nous pouvions continuer à utiliser du code Python interprété pendant le développement, ce qui nous permettrait de continuer à travailler en utilisant un schéma très rapide pour apporter des modifications au code et le tester, plutôt que d'attendre que le code soit compilé. On aurait dit que nous étions superbement capables, pour ainsi dire, de nous asseoir sur deux chaises, et cela nous a plu.

Le compilateur, que nous avons nommé mypyc (car il utilise mypy comme interface pour l'analyse de type), s'est avéré être un projet très réussi. Dans l'ensemble, nous avons atteint une accélération environ 4x plus rapide des courses fréquentes de mypy sans mise en cache. Le développement du cœur du projet mypyc a pris environ 4 mois civils à une petite équipe comprenant Michael Sullivan, Ivan Levkivsky, Hugh Han et moi. Cette quantité de travail était beaucoup moins ambitieuse que ce qui serait nécessaire pour réécrire mypy, par exemple, en C ++ ou Go. Et nous avons dû apporter beaucoup moins de modifications au projet que nous n'aurions dû le faire en le réécrivant 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 rapides de bas niveau C. Par exemple, un appel à une fonction compilée se traduit par un appel à une fonction C. Et un tel appel est fait beaucoup plus rapidement que d'appeler une fonction interprétée. Certaines opérations, telles que les recherches dans les dictionnaires, se résumaient toujours à l'utilisation d'appels C-API réguliers à partir de CPython, qui après la compilation s'est avéré être un peu plus rapide. Nous avons pu nous débarrasser de la charge supplémentaire sur le système créée par l'interprétation, mais cela dans ce cas n'a donné qu'un petit gain en termes de performances.

Pour identifier les opérations «lentes» les plus courantes, nous avons effectué un profilage de code. Armé des données obtenues, nous avons essayé soit de modifier 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 assez simple pour cela ou autre problème). La réécriture du code Python s'est souvent avérée être une solution plus facile au problème que l'implémentation automatique de la même transformation dans le compilateur. À long terme, nous voulions automatiser bon nombre de ces transformations, mais à ce moment-là, nous visions à accélérer mypy avec un minimum d'effort. Et nous, en allant vers cet objectif, avons coupé plusieurs coins.

À suivre ...

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


Source: https://habr.com/ru/post/fr468235/


All Articles