Der Weg zur Typprüfung von 4 Millionen Zeilen Python-Code. Teil 2

Heute veröffentlichen wir den zweiten Teil der Übersetzung von Material darüber, wie Dropbox die Typkontrolle für mehrere Millionen Zeilen Python-Code organisiert hat.

Der Weg zur Typprüfung von 4 Millionen Zeilen Python-Code. Teil 2

Lesen Sie den ersten Teil

Offizielle Typunterstützung (PEP 484)

Unsere ersten ernsthaften Experimente mit mypy haben wir bei Dropbox während der Hack Week 2014 durchgeführt. Die Hack Week ist eine einwöchige Veranstaltung, die von Dropbox veranstaltet wird. In dieser Zeit können die Mitarbeiter arbeiten, was sie wollen! Einige der berühmtesten Technologieprojekte von Dropbox begannen bei Veranstaltungen wie diesen. Als Ergebnis dieses Experiments kamen wir zu dem Schluss, dass mypy vielversprechend aussieht, obwohl das Projekt noch nicht für den breiten Einsatz bereit ist.

Zu dieser Zeit lag die Idee in der Luft, Hinweissysteme vom Typ Python zu standardisieren. Wie gesagt, seit Python 3.0 war es möglich, Typanmerkungen für Funktionen zu verwenden, aber das waren nur willkürliche Ausdrücke, ohne definierte Syntax und Semantik. Während der Programmausführung wurden diese Anmerkungen größtenteils einfach ignoriert. Nach der Hack Week begannen wir mit der Standardisierung der Semantik. Diese Arbeit führte zur Entstehung PEP 484 (Guido van Rossum, Łukasz Langa und ich haben an diesem Dokument zusammengearbeitet).

Unsere Motive konnten von zwei Seiten betrachtet werden. Erstens hofften wir, dass das gesamte Python-Ökosystem einen gemeinsamen Ansatz für die Verwendung von Typhinweisen übernehmen könnte (ein Begriff, der in Python als Äquivalent von „Typanmerkungen“ verwendet wird). Dies wäre angesichts der möglichen Risiken besser als die Verwendung vieler miteinander inkompatibler Ansätze. Zweitens wollten wir mit vielen Mitgliedern der Python-Community offen über Typannotationsmechanismen diskutieren. Dieser Wunsch wurde teilweise durch die Tatsache bestimmt, dass wir in den Augen der breiten Masse der Python-Programmierer nicht wie „Abtrünnige“ von den Grundideen der Sprache aussehen wollten. Es handelt sich um eine dynamisch typisierte Sprache, die als „Duck-Typing“ bekannt ist. In der Community kam es gleich zu Beginn zu einer etwas misstrauischen Haltung gegenüber der Idee des statischen Tippens. Aber diese Stimmung ließ schließlich nach, als klar wurde, dass statisches Tippen nicht obligatorisch sein würde (und nachdem die Leute erkannt hatten, dass es tatsächlich nützlich war).

Die schließlich übernommene Typhinweissyntax war der damals von mypy unterstützten sehr ähnlich. PEP 484 wurde 3.5 mit Python 2015 veröffentlicht. Python war keine dynamisch typisierte Sprache mehr. Ich betrachte dieses Ereignis gerne als einen bedeutenden Meilenstein in der Geschichte von Python.

Beginn der Migration

Ende 2015 stellte Dropbox ein dreiköpfiges Team zusammen, das an mypy arbeiten sollte. Zu ihnen gehörten Guido van Rossum, Greg Price und David Fisher. Von diesem Moment an begann sich die Situation äußerst schnell zu entwickeln. Das erste Hindernis für das Wachstum von mypy war die Leistung. Wie ich oben angedeutet habe, habe ich in den frühen Tagen des Projekts darüber nachgedacht, die Mypy-Implementierung in C zu übersetzen, aber diese Idee wurde vorerst von der Liste gestrichen. Wir mussten das System mit dem CPython-Interpreter ausführen, der für Tools wie mypy nicht schnell genug ist. (Das PyPy-Projekt, eine alternative Python-Implementierung mit einem JIT-Compiler, hat uns auch nicht geholfen.)

Glücklicherweise sind uns hier einige algorithmische Verbesserungen zu Hilfe gekommen. Der erste leistungsstarke „Beschleuniger“ war die Implementierung der inkrementellen Prüfung. Die Idee hinter dieser Verbesserung war einfach: Wenn sich alle Abhängigkeiten des Moduls seit der letzten Ausführung von mypy nicht geändert haben, können wir die während der vorherigen Ausführung zwischengespeicherten Daten verwenden, während wir mit Abhängigkeiten arbeiten. Wir mussten lediglich eine Typprüfung für die geänderten Dateien und die davon abhängigen Dateien durchführen. Mypy ging sogar noch einen Schritt weiter: Wenn sich die externe Schnittstelle eines Moduls nicht änderte, ging mypy davon aus, dass andere Module, die dieses Modul importierten, nicht erneut überprüft werden mussten.

Die inkrementelle Prüfung hat uns beim Annotieren großer Mengen vorhandenen Codes sehr geholfen. Der Punkt ist, dass dieser Prozess normalerweise viele iterative Durchläufe von mypy umfasst, da dem Code nach und nach Anmerkungen hinzugefügt und schrittweise verbessert werden. Der erste Lauf von mypy war immer noch sehr langsam, da viele Abhängigkeiten überprüft werden mussten. Um die Situation zu verbessern, haben wir dann einen Remote-Caching-Mechanismus implementiert. Wenn mypy erkennt, dass der lokale Cache wahrscheinlich veraltet ist, lädt es den aktuellen Cache-Snapshot für die gesamte Codebasis aus dem zentralen Repository herunter. Anschließend wird anhand dieses Snapshots eine inkrementelle Prüfung durchgeführt. Dies hat uns einen weiteren großen Schritt zur Steigerung der Leistung von mypy gebracht.

Dies war eine Zeit der schnellen und natürlichen Einführung der Typprüfung bei Dropbox. Bis Ende 2016 hatten wir bereits etwa 420000 Zeilen Python-Code mit Typanmerkungen. Viele Anwender waren von der Typprüfung begeistert. Immer mehr Entwicklungsteams nutzten Dropbox mypy.

Damals sah alles gut aus, aber wir hatten noch viel zu tun. Wir begannen mit der Durchführung regelmäßiger interner Benutzerbefragungen, um Problembereiche des Projekts zu identifizieren und zu verstehen, welche Probleme zuerst gelöst werden müssen (diese Praxis wird im Unternehmen auch heute noch angewendet). Am wichtigsten waren, wie sich herausstellte, zwei Aufgaben. Erstens brauchten wir eine bessere Typabdeckung des Codes, zweitens brauchten wir mypy, um schneller zu arbeiten. Es war völlig klar, dass unsere Arbeit, mypy zu beschleunigen und in Unternehmensprojekte zu implementieren, noch lange nicht abgeschlossen war. Wir sind uns der Bedeutung dieser beiden Aufgaben voll bewusst und machen uns an deren Lösung.

Mehr Produktivität!

Inkrementelle Prüfungen machten mypy schneller, aber das Tool war immer noch nicht schnell genug. Viele inkrementelle Überprüfungen dauerten etwa eine Minute. Der Grund dafür waren zyklische Importe. Dies wird wahrscheinlich niemanden überraschen, der mit großen, in Python geschriebenen Codebasen gearbeitet hat. Wir hatten Sätze von Hunderten von Modulen, von denen jedes indirekt alle anderen importierte. Wenn eine Datei in einer Importschleife geändert wurde, musste mypy alle Dateien in dieser Schleife und häufig auch alle Module verarbeiten, die Module aus dieser Schleife importierten. Ein solcher Zyklus war das berüchtigte „Abhängigkeitsgewirr“, das bei Dropbox für viel Ärger sorgte. Sobald diese Struktur mehrere hundert Module enthielt, wurde sie während vieler Tests direkt oder indirekt importiert und auch im Produktionscode verwendet.

Wir haben über die Möglichkeit nachgedacht, zirkuläre Abhängigkeiten zu „entwirren“, hatten aber nicht die Ressourcen dafür. Es gab zu viel Code, mit dem wir nicht vertraut waren. Als Ergebnis haben wir einen alternativen Ansatz entwickelt. Wir haben beschlossen, dass mypy auch bei „Abhängigkeitsgewirr“ schnell funktioniert. Dieses Ziel 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 laden müssen, die sich auf Tausende importierter Abhängigkeiten beziehen. Zweitens analysiert er sorgfältig auf der Ebene kleiner Struktureinheiten die Abhängigkeiten zwischen Funktionen und anderen Einheiten. Wenn beispielsweise die Funktion foo ruft eine Funktion auf bar, dann liegt eine Abhängigkeit vor foo aus bar. Wenn sich eine Datei ändert, verarbeitet der Daemon zunächst isoliert nur die geänderte Datei. Anschließend werden äußerlich sichtbare Änderungen an dieser Datei untersucht, beispielsweise geänderte Funktionssignaturen. Der Daemon verwendet detaillierte Informationen zu Importen nur, um die Funktionen zu überprüfen, die die geänderte Funktion tatsächlich verwenden. Typischerweise müssen Sie bei diesem Ansatz nur sehr wenige Funktionen überprüfen.

All dies zu implementieren war nicht einfach, da sich die ursprüngliche mypy-Implementierung stark auf die Verarbeitung einer Datei nach der anderen konzentrierte. Wir hatten es mit vielen Grenzsituationen zu tun, deren Auftreten wiederholte Kontrollen erforderte, wenn sich etwas am Code änderte. Dies geschieht beispielsweise, wenn einer Klasse eine neue Basisklasse zugewiesen wird. Nachdem wir getan hatten, was wir wollten, konnten wir die Ausführungszeit der meisten inkrementellen Prüfungen auf nur wenige Sekunden reduzieren. Für uns schien das ein großer Sieg zu sein.

Noch mehr Produktivität!

Zusammen mit dem oben besprochenen Remote-Caching hat der mypy-Daemon die Probleme, die auftreten, wenn ein Programmierer häufig Typprüfungen durchführt und dabei Änderungen an einer kleinen Anzahl von Dateien vornimmt, fast vollständig gelöst. Allerdings war die Systemleistung im ungünstigsten Anwendungsfall noch lange nicht optimal. Ein sauberer Start von mypy kann über 15 Minuten dauern. Und das war viel mehr, als wir uns gewünscht hätten. Mit jeder Woche verschlimmerte sich die Situation, da die Programmierer weiterhin neuen Code schrieben und dem vorhandenen Code Anmerkungen hinzufügten. Unsere Nutzer waren immer noch hungrig nach mehr Leistung, aber wir waren froh, sie auf halbem Weg zu treffen.

Wir beschlossen, zu einer der früheren Ideen bezüglich Mypy zurückzukehren. Nämlich, um Python-Code in C-Code umzuwandeln. Das Experimentieren mit Cython (einem System, mit dem Sie in Python geschriebenen Code in C-Code übersetzen können) brachte uns keine sichtbare Beschleunigung, daher beschlossen wir, die Idee, einen eigenen Compiler zu schreiben, wieder aufleben zu lassen. Da die mypy-Codebasis (in Python geschrieben) bereits alle notwendigen Typanmerkungen enthielt, dachten wir, dass es sich lohnen würde, diese Anmerkungen zu verwenden, um das System zu beschleunigen. Ich habe schnell einen Prototyp erstellt, um diese Idee zu testen. Bei verschiedenen Mikro-Benchmarks zeigte sich eine mehr als zehnfache Leistungssteigerung. Unsere Idee bestand darin, Python-Module mit Cython in C-Module zu kompilieren und Typanmerkungen in Laufzeit-Typprüfungen umzuwandeln (normalerweise werden Typanmerkungen zur Laufzeit ignoriert und nur von Typprüfungssystemen verwendet). Eigentlich hatten wir vor, die mypy-Implementierung von Python in eine Sprache zu übersetzen, die für die statische Typisierung konzipiert ist und genau wie Python aussieht (und größtenteils auch funktioniert). (Diese Art der sprachübergreifenden Migration ist zu einer Art Tradition des Mypy-Projekts geworden. Die ursprüngliche Mypy-Implementierung 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 dazu, die Projektmanagementfähigkeiten nicht zu verlieren. Wir mussten keine virtuelle Maschine oder Bibliotheken implementieren, die Mypy benötigte. Darüber hinaus hätten wir weiterhin Zugriff auf das gesamte Python-Ökosystem und alle Tools (z. B. Pytest). Dies bedeutete, dass wir während der Entwicklung weiterhin interpretierten Python-Code verwenden konnten, was es uns ermöglichte, mit einem sehr schnellen Muster von Codeänderungen und -tests weiterzuarbeiten, anstatt auf die Kompilierung des Codes zu warten. Es sah so aus, als ob wir sozusagen einen tollen Job machten, indem wir auf zwei Stühlen saßen, und es hat uns sehr gut gefallen.

Der Compiler, den wir mypyc nannten (da er mypy als Frontend für die Analyse von Typen verwendet), erwies sich als sehr erfolgreiches Projekt. Insgesamt haben wir bei häufigen Mypy-Läufen ohne Caching eine etwa vierfache Beschleunigung erreicht. Für die Entwicklung des Kerns des mypyc-Projekts brauchte ein kleines Team bestehend aus Michael Sullivan, Ivan Levkivsky, Hugh Hahn und mir etwa vier Kalendermonate. Dieser Arbeitsaufwand war viel geringer als der, der nötig gewesen wäre, um mypy beispielsweise in C++ oder Go neu zu schreiben. Und wir mussten viel weniger Änderungen am Projekt vornehmen, als wenn wir es in einer anderen Sprache umschreiben müssten. Wir hofften auch, dass wir mypyc auf ein solches Niveau bringen könnten, dass andere Dropbox-Programmierer es zum Kompilieren und Beschleunigen ihres Codes verwenden könnten.

Um dieses Leistungsniveau zu erreichen, mussten wir einige interessante technische Lösungen anwenden. Somit kann der Compiler viele Operationen beschleunigen, indem er schnelle C-Konstrukte auf niedriger Ebene verwendet. Beispielsweise wird ein kompilierter Funktionsaufruf in einen C-Funktionsaufruf übersetzt. Und ein solcher Aufruf ist viel schneller als der Aufruf einer interpretierten Funktion. Einige Vorgänge, wie z. B. Wörterbuchsuchen, erforderten immer noch die Verwendung regulärer C-API-Aufrufe von CPython, die beim Kompilieren nur unwesentlich schneller waren. Die zusätzliche Belastung des Systems durch die Interpretation konnten wir eliminieren, was in diesem Fall jedoch nur zu einem geringen Leistungsgewinn führte.

Um die häufigsten „langsamen“ Vorgänge zu identifizieren, haben wir eine Code-Profilierung durchgeführt. Mit diesen Daten bewaffnet haben wir versucht, mypyc entweder 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 für dieses oder andere Problem). . Das Umschreiben des Python-Codes war oft eine einfachere Lösung des Problems, als den Compiler automatisch dieselbe Transformation durchführen zu lassen. Langfristig wollten wir viele dieser Transformationen automatisieren, aber damals konzentrierten wir uns darauf, mypy mit minimalem Aufwand zu beschleunigen. Und um dieses Ziel zu erreichen, haben wir einige Abstriche gemacht.

To be continued ...

Liebe Leser! Welchen Eindruck hatten Sie vom mypy-Projekt, als Sie von seiner Existenz erfuhren?

Der Weg zur Typprüfung von 4 Millionen Zeilen Python-Code. Teil 2
Der Weg zur Typprüfung von 4 Millionen Zeilen Python-Code. Teil 2

Source: habr.com

Kommentar hinzufügen