Die Möglichkeit, 4 Millionen Zeilen Python-Code zu überprüfen. Teil 2

Heute veröffentlichen wir den zweiten Teil der Übersetzung des Materials darüber, wie Dropbox die Typsteuerung von mehreren Millionen Zeilen Python-Code organisiert hat.



Lesen Sie den ersten Teil

Formale Typunterstützung (PEP 484)


Wir haben das erste ernsthafte Experiment mit mypy auf Dropbox während der Hack Week 2014 durchgeführt. Die Hack Week ist eine Veranstaltung, die Dropbox eine Woche lang veranstaltet. Zu diesem Zeitpunkt können Mitarbeiter an allem arbeiten! Einige der bekanntesten Technologieprojekte von Dropbox begannen bei ähnlichen Veranstaltungen. Als Ergebnis dieses Experiments kamen wir zu dem Schluss, dass mypy vielversprechend aussieht, obwohl dieses Projekt noch nicht für eine breite Anwendung bereit war.

Zu dieser Zeit lag die Idee, Hinweissysteme für Python-Typen zu standardisieren, in der Luft. Wie ich bereits sagte, konnten Sie ab Python 3.0 Typanmerkungen für Funktionen verwenden, dies waren jedoch nur beliebige Ausdrücke ohne spezifische Syntax und Semantik. Während der Programmausführung wurden diese Anmerkungen größtenteils einfach ignoriert. Nach der Hack Week haben wir begonnen, die Semantik zu standardisieren. Diese Arbeit führte zur Entstehung von PEP 484 (Guido van Rossum, Lukas Langa und ich haben an diesem Dokument zusammengearbeitet).

Unsere Motive konnten von zwei Seiten betrachtet werden. Zunächst hofften wir, dass das gesamte Python-Ökosystem einen allgemeinen Ansatz für die Verwendung von Typhinweisen verfolgen könnte (Typhinweise sind ein Begriff, der in Python als Analogon zu „Typanmerkungen“ verwendet wird). Dies wäre angesichts der möglichen Risiken besser, als viele miteinander inkompatible Ansätze zu verwenden. Zweitens wollten wir mit vielen Mitgliedern der Python-Community offen über Annotationsmechanismen für Typen diskutieren. Zum Teil wurde dieser Wunsch durch die Tatsache diktiert, dass wir in den Augen der breiten Masse der Python-Programmierer nicht wie "Abtrünnige" aus den Grundideen der Sprache aussehen wollen. Es ist eine dynamisch typisierte Sprache, die als "Ententypisierung" bekannt ist. In der Community konnte zu Beginn eine etwas misstrauische Haltung gegenüber der Idee der statischen Typisierung nicht anders, als aufzutreten. Aber diese Einstellung schwächte sich schließlich ab - nachdem klar wurde, dass statisches Tippen nicht obligatorisch sein sollte (und nachdem die Leute erkannten, dass es wirklich nützlich war).

Die resultierende Syntax für Typhinweise war der zu diesem Zeitpunkt unterstützten mypy sehr ähnlich. PEP 484 wurde 2015 mit Python 3.5 veröffentlicht. Python war keine Sprache mehr, die nur dynamisches Schreiben unterstützte. Ich betrachte dieses Ereignis gerne als einen bedeutenden Meilenstein in der Geschichte von Python.

Beginn der Migration


Ende 2015 wurde in Dropbox ein Team von drei Personen zusammengestellt, um an mypy zu arbeiten. Darunter waren Guido van Rossum, Greg Price und David Fisher. Von diesem Moment an begann sich die Situation extrem schnell zu entwickeln. Das erste Hindernis für das Wachstum von Mypy war die Leistung. Wie ich oben bereits angedeutet habe, habe ich in der frühen Phase der Projektentwicklung darüber nachgedacht, die Implementierung von mypy in C zu übersetzen, aber diese Idee wurde bisher aus den Listen gestrichen. Wir bleiben bei der Tatsache, dass wir den CPython-Interpreter zum Starten des Systems verwendet haben, was für Tools wie mypy nicht schnell genug ist. (Das PyPy-Projekt, eine alternative Implementierung von Python mit einem JIT-Compiler, hat uns auch nicht geholfen.)

Glücklicherweise kamen uns hier einige algorithmische Verbesserungen zu Hilfe. Der erste leistungsstarke „Beschleuniger“ war die Implementierung einer inkrementellen Verifizierung. Die Idee dieser Verbesserung war einfach: Wenn sich seit dem vorherigen Start von mypy nicht alle Abhängigkeiten des Moduls geändert haben, können wir die während der vorherigen Sitzung zwischengespeicherten Daten verwenden, während wir mit Abhängigkeiten arbeiten. Alles, was wir tun mussten, war die Typprüfung in den geänderten Dateien und in den Dateien, die von ihnen abhingen. Mypy ging noch ein bisschen weiter: Wenn sich die externe Schnittstelle des Moduls nicht änderte, glaubte mypy, dass andere Module, die dieses Modul importieren, nicht erneut überprüft werden müssen.

Die inkrementelle Validierung hat uns bei der Annotation großer Mengen vorhandenen Codes sehr geholfen. Tatsache ist, dass dieser Prozess normalerweise viele iterative Läufe von mypy umfasst, da Anmerkungen dem Code schrittweise hinzugefügt und schrittweise verbessert werden. Der erste Start von mypy war immer noch sehr langsam, da Sie beim Ausführen viele Abhängigkeiten überprüfen mussten. Um die Situation zu verbessern, haben wir dann einen Remote-Caching-Mechanismus implementiert. Wenn mypy feststellt, dass der lokale Cache wahrscheinlich veraltet ist, lädt es den aktuellen Cache-Snapshot für die gesamte Codebasis aus einem zentralen Repository herunter. Anschließend führt er mithilfe dieses Schnappschusses eine inkrementelle Überprüfung durch. Dies ist ein weiterer großer Schritt, der uns dazu bewegt hat, die Produktivität von mypy zu steigern.

Dies war eine Zeit der schnellen und natürlichen Einführung des Dropbox-Typprüfsystems. Bis Ende 2016 hatten wir bereits ungefähr 420.000 Zeilen Python-Code mit Typanmerkungen. Viele Benutzer waren von der Typprüfung begeistert. Dropbox mypy wurde von immer mehr Entwicklungsteams verwendet.

Damals sah alles gut aus, aber wir hatten noch viel zu tun. Wir haben begonnen, regelmäßige interne Benutzerumfragen durchzuführen, um die Problembereiche des Projekts zu identifizieren und zu verstehen, welche Probleme zuerst angegangen werden müssen (diese Praxis wird heute im Unternehmen angewendet). Das Wichtigste waren, wie sich herausstellte, zwei Aufgaben. Das erste - Sie brauchten mehr Codeabdeckung mit Typen, das zweite - Sie mussten mypy schneller arbeiten lassen. Es war völlig klar, dass unsere Arbeit zur Beschleunigung von mypy und seiner Umsetzung in den Projekten des Unternehmens noch lange nicht abgeschlossen war. Wir waren uns der Bedeutung dieser beiden Aufgaben voll bewusst und haben ihre Lösung aufgegriffen.

Mehr Leistung!


Inkrementelle Überprüfungen beschleunigten mypy, aber dieses Tool war immer noch nicht schnell genug. Viele inkrementelle Überprüfungen dauerten ungefähr eine Minute. Der Grund dafür waren zyklische Importe. Dies wird wahrscheinlich niemanden überraschen, der mit großen Codebasen gearbeitet hat, die in Python geschrieben wurden. Wir hatten Sätze von Hunderten von Modulen, von denen jedes indirekt alle anderen importierte. Wenn sich herausstellte, dass eine Datei im Importzyklus geändert wurde, musste mypy alle in diesem Zyklus enthaltenen Dateien und häufig auch alle Module verarbeiten, die Module aus diesem Zyklus importieren. Ein solcher Zyklus war das berüchtigte „Gewirr von Abhängigkeiten“, das in Dropbox große Probleme verursachte. Nachdem diese Struktur mehrere hundert Module enthielt, während sie direkt oder indirekt viele Tests importierte, wurde sie auch im Produktionscode verwendet.

Wir haben die Möglichkeit in Betracht gezogen, zyklische Abhängigkeiten zu „entwirren“, aber wir hatten nicht die Ressourcen, um dies zu tun. Es gab zu viel Code, mit dem wir nicht vertraut waren. Infolgedessen haben wir einen alternativen Ansatz gewählt. Wir haben uns entschlossen, mypy schnell arbeiten zu lassen, auch wenn es „Abhängigkeitsbälle“ gibt. Dies haben wir mit dem mypy-Daemon erreicht. Ein Daemon ist ein Serverprozess, der zwei interessante Funktionen implementiert. Erstens speichert es Informationen über die gesamte Codebasis im Speicher. Dies bedeutet, dass Sie nicht jedes Mal, wenn Sie mypy ausführen, zwischengespeicherte Daten herunterladen müssen, die sich auf Tausende importierter Abhängigkeiten beziehen. Zweitens analysiert er sorgfältig auf der Ebene kleiner Struktureinheiten die Beziehungen zwischen Funktionen und anderen Einheiten. Wenn die Funktion foo beispielsweise die Funktionsleiste aufruft, besteht eine Abhängigkeit von foo von der bar . Wenn eine Datei geändert wird, verarbeitet der Dämon zunächst isoliert nur die geänderte Datei. Dann betrachtet er die Änderungen an dieser Datei, die von außen sichtbar sind, wie z. B. die geänderten Funktionssignaturen. Der Dämon verwendet detaillierte Importinformationen nur, um die Funktionen zu überprüfen, die die geänderte Funktion tatsächlich verwenden. Typischerweise müssen bei diesem Ansatz nur sehr wenige Funktionen überprüft werden.

Die Implementierung all dessen war nicht einfach, da sich die ursprüngliche Implementierung von mypy stark auf die Verarbeitung einer Datei nach der anderen konzentrierte. Wir mussten uns mit vielen Grenzsituationen auseinandersetzen, deren Auftreten in den Fällen, in denen sich etwas im Code änderte, wiederholte Überprüfungen erforderte. Dies geschieht beispielsweise, wenn einer Klasse eine neue Basisklasse zugewiesen wird. Nachdem wir das getan hatten, was wir wollten, konnten wir die Ausführungszeit der meisten inkrementellen Überprüfungen auf einige Sekunden reduzieren. Es schien uns ein großer Sieg zu sein.

Mehr Leistung!


Zusammen mit dem oben beschriebenen Remote-Caching löste der mypy-Daemon die Probleme, die auftreten, wenn der Programmierer häufig eine Typprüfung durchführt und Änderungen an einer kleinen Anzahl von Dateien vornimmt, fast vollständig. Die Systemleistung in der ungünstigsten Variante seiner Verwendung war jedoch noch lange nicht optimal. Ein sauberer Start von mypy kann mehr als 15 Minuten dauern. Und es war viel mehr als wir möchten. Jede Woche verschlechterte sich die Situation, da Programmierer weiterhin neuen Code schrieben und dem vorhandenen Code Anmerkungen hinzufügten. Unsere Benutzer sehnten sich immer noch nach mehr Leistung, aber wir waren froh, bereit zu sein, sie zu treffen.

Wir beschlossen, zu einer meiner frühesten Ideen in Bezug auf Mypy zurückzukehren. Die Konvertierung von Python-Code in C-Code. Experimente mit Cython (dies ist ein System, mit dem Sie Python-Code in C-Code übersetzen können) haben uns keine sichtbare Beschleunigung gebracht, daher haben wir beschlossen, die Idee, einen eigenen Compiler zu schreiben, wiederzubeleben. Da die in Python geschriebene mypy-Codebasis bereits alle erforderlichen Typanmerkungen enthielt, schien es sinnvoll, diese Anmerkungen zur Beschleunigung des Systems zu verwenden. Ich habe schnell einen Prototyp erstellt, um diese Idee zu testen. Er zeigte auf verschiedenen Mikro-Benchmarks eine mehr als 10-fache Produktivitätssteigerung. Unsere Idee war es, Python-Module mit Cython in C-Module zu kompilieren und Typanmerkungen in zur Laufzeit durchgeführte Typprüfungen umzuwandeln (normalerweise werden Typanmerkungen zur Laufzeit ignoriert und nur von Typprüfungssystemen verwendet ) Wir hatten tatsächlich vor, die mypy-Implementierung von Python in eine Sprache zu übersetzen, die statisch typisiert erstellt wurde und genau wie Python aussehen (und größtenteils funktionieren) würde. (Diese Art der sprachübergreifenden Migration ist zu einer Tradition des mypy-Projekts geworden. Die anfängliche Implementierung von mypy wurde in Alore geschrieben, dann gab es eine syntaktische Mischung aus Java und Python.)

Die Konzentration auf die CPython-Erweiterungs-API war der Schlüssel, um die Projektverwaltungsfunktionen nicht zu verlieren. Wir mussten keine virtuelle Maschine oder Bibliotheken implementieren, die mypy benötigte. Außerdem würde uns weiterhin das gesamte Python-Ökosystem zur Verfügung stehen, alle Tools (wie z. B. pytest) wären verfügbar. Dies bedeutete, dass wir während der Entwicklung weiterhin interpretierten Python-Code verwenden konnten, sodass wir weiterhin mit einem sehr schnellen Schema arbeiten konnten, um Änderungen am Code vorzunehmen und ihn zu testen, anstatt auf die Kompilierung des Codes zu warten. Es sah so aus, als könnten wir sozusagen hervorragend auf zwei Stühlen sitzen, und es hat uns gefallen.

Der Compiler, den wir mypyc nannten (da er mypy als Frontend für die Typanalyse verwendet), erwies sich als sehr erfolgreiches Projekt. Alles in allem erreichten wir eine etwa 4-mal schnellere Beschleunigung der häufigen Läufe von mypy ohne Caching. Die Entwicklung des Kerns des mypyc-Projekts dauerte ungefähr 4 Kalendermonate von einem kleinen Team, zu dem Michael Sullivan, Ivan Levkivsky, Hugh Han und ich gehörten. Dieser Arbeitsaufwand war weitaus weniger ehrgeizig als das, was zum Umschreiben von mypy erforderlich wäre, beispielsweise in C ++ oder Go. Und wir mussten viel weniger Änderungen am Projekt vornehmen, als wir beim Umschreiben in einer anderen Sprache vornehmen müssten. Wir hofften auch, dass wir mypyc auf ein solches Niveau bringen könnten, dass andere Dropbox-Programmierer es verwenden könnten, um ihren Code zu kompilieren und zu beschleunigen.

Um dieses Leistungsniveau zu erreichen, mussten wir einige interessante technische Lösungen anwenden. So kann der Compiler viele Operationen mithilfe der schnellen C-Konstrukte auf niedriger Ebene beschleunigen. Beispielsweise wird ein Aufruf einer kompilierten Funktion in einen Aufruf einer C-Funktion übersetzt. Und ein solcher Aufruf erfolgt viel schneller als der Aufruf einer interpretierten Funktion. Einige Vorgänge, wie z. B. die Wörterbuchsuche, beschränkten sich immer noch auf die Verwendung regulärer C-API-Aufrufe von CPython, die sich nach der Kompilierung als nur wenig schneller herausstellten. Wir konnten die zusätzliche Belastung des durch die Interpretation verursachten Systems beseitigen, aber dies führte in diesem Fall nur zu einem geringen Leistungsgewinn.

Um die häufigsten „langsamen“ Vorgänge zu identifizieren, haben wir Codeprofile erstellt. Mit den Daten ausgestattet haben wir versucht, entweder mypyc so zu optimieren, dass es schnelleren C-Code für solche Operationen generiert, oder den entsprechenden Python-Code mit schnelleren Operationen neu zu schreiben (und manchmal hatten wir einfach keine ausreichend einfache Lösung dafür oder ein anderes Problem). Das Umschreiben von Python-Code erwies sich häufig als einfachere Lösung für das Problem als das automatische Implementieren derselben Umwandlung im Compiler. Auf lange Sicht wollten wir viele dieser Transformationen automatisieren, aber in diesem Moment wollten wir mypy mit minimalem Aufwand beschleunigen. Und als wir uns diesem Ziel näherten, schnitten wir mehrere Ecken ab.

Fortsetzung folgt…

Liebe Leser! Was waren Ihre Eindrücke von dem mypy-Projekt, als Sie von seiner Existenz erfuhren?


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


All Articles