Heute veröffentlichen wir den zweiten Teil der Übersetzung des Materials darüber, wie Dropbox die Typenkontrolle für mehrere Millionen Zeilen Python-Code organisiert hat.
→
Offizielle Unterstützung für Typen (PEP 484)
Während der Hack Week 2014 haben wir in Dropbox erste ernsthafte Experimente mit mypy durchgeführt. Die Hack Week ist eine Veranstaltung, die Dropbox eine Woche lang abhalten kann. In dieser Zeit können Mitarbeiter an allem arbeiten, was sie wollen! Einige der bekanntesten Technologieprojekte von Dropbox haben genau in solchen Veranstaltungen ihren Anfang genommen. Aus diesem Experiment zogen wir die Schlussfolgerung, dass mypy vielversprechend aussieht, auch wenn dieses Projekt noch nicht für den breiten Einsatz bereit war.
Zu dieser Zeit kursierte die Idee einer Standardisierung der Typ-Hinweis-Systeme von Python. Wie bereits erwähnt, konnten ab Python 3.0 Typannotationen für Funktionen verwendet werden, aber es handelte sich lediglich um willkürliche Ausdrücke, ohne festgelegte Syntax und Semantik. Während der Programmausführung wurden diese Annotationen größtenteils einfach ignoriert. Nach der Hack Week begannen wir mit der Arbeit an der Standardisierung der Semantik. Diese Arbeit führte zur Entstehung von (An diesem Dokument arbeiteten Guido van Rossum, Łukasz Langa und ich gemeinsam).
Unsere Motive können aus zwei Perspektiven betrachtet werden. Zum einen hofften wir, dass das gesamte Python-Ökosystem einen einheitlichen Ansatz für die Verwendung von Typanmerkungen (type hints – ein Begriff, der in Python als Äquivalent zu ‚Typanmerkungen‘ verwendet wird) akzeptieren könnte. Dies wäre in Anbetracht möglicher Risiken besser, als mit einer Vielzahl von nicht kompatiblen Ansätzen zu arbeiten. Zum anderen wollten wir die Mechanismen der Typanmerkung offen mit vielen Vertretern der Python-Community diskutieren. Teilweise wurde dieser Wunsch dadurch motiviert, dass wir nicht als ‚Abtrünnige‘ von den grundlegenden Ideen der Sprache in den Augen der breiten Masse der Python-Entwickler erscheinen wollten. Es handelt sich um eine dynamisch typisierte Sprache, die für ihre ‚Enten-Typisierung‘ bekannt ist. Zu Beginn gab es in der Gemeinschaft eine gewisse Skepsis gegenüber der Idee der statischen Typisierung. Doch diese Einstellung schwächte sich schließlich ab, nachdem klar wurde, dass die statische Typisierung nicht verpflichtend gemacht werden sollte (und nachdem die Leute erkannten, dass sie tatsächlich nützlich ist).
Der angenommene Syntax für Typ-Hinweise ähnelte stark dem, was zu dieser Zeit von mypy unterstützt wurde. Das Dokument PEP 484 wurde zusammen mit Python 3.5 im Jahr 2015 veröffentlicht. Python war nicht länger eine Sprache, die nur dynamische Typisierung unterstützte. Ich betrachte diesen Moment als einen bedeutenden Meilenstein in der Geschichte von Python.
Beginn der Migration
Ende 2015 wurde bei Dropbox ein Team von drei Personen gegründet, um an mypy zu arbeiten. Dazu gehörten Guido van Rossum, Greg Price und David Fischer. Von diesem Zeitpunkt an entwickelte sich die Situation sehr schnell. Das erste Hindernis für das Wachstum von mypy war die Leistung. Wie ich bereits angedeutet habe, dachte ich in der frühen Phase der Entwicklung des Projekts darüber nach, die Implementierung von mypy in C zu übersetzen, aber diese Idee wurde vorerst von der Liste gestrichen. Wir waren darauf angewiesen, dass das System mit dem CPython-Interpreter betrieben wurde, der nicht die Geschwindigkeit aufwies, die für Werkzeuge wie mypy erforderlich ist. (Das PyPy-Projekt, eine alternative Implementierung von Python mit einem JIT-Compiler, half uns auch nicht weiter.)
Zum Glück haben uns einige algorithmische Verbesserungen unterstützt. Der erste starke „Beschleuniger“ war die Implementierung einer inkrementellen Prüfung. Die Idee dieser Verbesserung war einfach: Wenn sich alle Abhängigkeiten eines Moduls seit dem letzten Start von mypy nicht geändert haben, können wir während der Arbeit mit den Abhängigkeiten die während der vorherigen Sitzung zwischengespeicherten Daten verwenden. Wir mussten nur die Typprüfungen in den geänderten Dateien und in den Dateien durchführen, die von ihnen abhingen. Mypy ging sogar noch einen Schritt weiter: Wenn sich die äußere Schnittstelle des Moduls nicht geändert hat, ging mypy davon aus, dass andere Module, die dieses Modul importieren, nicht erneut geprüft werden müssen.
Die inkrementelle Überprüfung hat uns erheblich bei der Annotation großer Codebasen geholfen. Der Grund dafür ist, dass dieser Prozess normalerweise viele iterative Ausführungen von mypy umfasst, da Annotationen schrittweise zum Code hinzugefügt und kontinuierlich verbessert werden. Der erste mypy-Durchlauf war immer noch sehr langsam, da viele Abhängigkeiten überprüft werden mussten. Um die Situation zu verbessern, haben wir einen Mechanismus zur Remote-Caching implementiert. Wenn mypy erkennt, dass der lokale Cache wahrscheinlich veraltet ist, lädt es den aktuellen Snapshot des Caches für die gesamte Codebasis aus einem zentralen Repository. Anschließend führt es mit diesem Snapshot eine inkrementelle Überprüfung durch. Dies hat uns einen weiteren erheblichen Schritt in Richtung Leistungssteigerung von mypy gebracht.
Dies war eine Phase der schnellen und natürlichen Einführung des Typsystems bei Dropbox. Bis Ende 2016 hatten wir bereits etwa 420.000 Zeilen Python-Code mit Typannotationen. Viele Benutzer begrüßten die Typüberprüfung enthusiastisch. In Dropbox wurde mypy von immer mehr Entwicklerteams genutzt.
Alles sah anfangs gut aus, aber es gab noch viel zu tun. Wir begannen mit regelmäßigen internen Befragungen der Nutzer, um Probleme im Projekt zu identifizieren und herauszufinden, welche Fragen prioritär zu lösen waren (diese Praxis wird bis heute im Unternehmen angewendet). Es wurde klar, dass zwei Aufgaben besonders wichtig waren. Erstens benötigten wir eine bessere Codeabdeckung durch Typen, und zweitens musste mypy schneller arbeiten. Es war offensichtlich, dass unsere Bemühungen, mypy zu beschleunigen und es in die Projekte des Unternehmens zu integrieren, noch lange nicht abgeschlossen waren. In vollem Bewusstsein der Bedeutung dieser beiden Aufgaben machten wir uns an ihre Lösung.
Mehr Leistung!
Inkrementelle Prüfungen haben mypy beschleunigt, aber das Werkzeug war dennoch nicht schnell genug. Viele inkrementelle Prüfungen dauerten etwa eine Minute. Der Grund dafür waren zyklische Importe. Das dürfte niemanden überraschen, der schon einmal mit großen Codebasen in Python gearbeitet hat. Wir hatten Sets aus hunderten von Modulen, die alle anderen indirekt importierten. Wenn eine Datei in einem Importzyklus verändert wurde, musste mypy alle Dateien in diesem Zyklus verarbeiten und oft auch alle Module, die Module aus diesem Zyklus importierten. Einer dieser Zyklen war der berüchtigte "Abhängigkeitsknäuel", der bei Dropbox für viele Probleme sorgte. Einmal enthielt diese Struktur mehrere hundert Module, die von zahlreichen Tests direkt oder indirekt importiert wurden und auch im Produktionscode verwendet wurden.
Wir haben die Möglichkeit in Betracht gezogen, zyklische Abhängigkeiten aufzulösen, aber uns fehlen die Ressourcen dafür. Es gab zu viel Code, mit dem wir nicht vertraut waren. Letztlich haben wir einen alternativen Ansatz gewählt. Wir entschieden uns dafür, mypy so zu gestalten, dass es auch bei ‚verwickelten Abhängigkeiten‘ schnell arbeitet. Dieses Ziel haben wir mit einem mypy-Daemon erreicht. Der Daemon ist ein Serverprozess, der zwei interessante Funktionen implementiert. Erstens speichert er Informationen über die gesamte Codebasis im Arbeitsspeicher. Das bedeutet, dass beim Start von mypy keine zwischengespeicherten Daten zu Tausenden von importierten Abhängigkeiten geladen werden müssen. Zweitens analysiert er sorgfältig, auf einer feinen strukturellen Ebene, die Abhängigkeiten zwischen Funktionen und anderen Entitäten. Zum Beispiel, wenn eine Funktion foo eine Funktion aufruft bar, dann besteht eine Abhängigkeit foo ab bar. Wenn sich eine Datei ändert, verarbeitet der Daemon zunächst isoliert nur die geänderte Datei. Anschließend betrachtet er die für außen Sichtbaren Veränderungen dieser Datei, wie etwa geänderte Funktionssignaturen. Der Daemon verwendet detaillierte Informationen zu den Importen nur zur Überprüfung der Funktionen, die tatsächlich die geänderte Funktion verwenden. In der Regel müssen dabei nur wenige Funktionen überprüft werden.
Die Umsetzung all dessen war eine anspruchsvolle Aufgabe, da die ursprüngliche Implementierung von mypy stark auf die Verarbeitung einer Datei zur Zeit ausgerichtet war. Wir mussten uns mit zahlreichen Grenzfällen auseinandersetzen, die erneute Überprüfungen erforderten, wenn sich etwas im Code änderte. Beispielsweise geschieht dies, wenn einer Klasse eine neue Basisklasse zugewiesen wird. Nachdem wir das, was wir wollten, umgesetzt hatten, konnten wir die Ausführungszeit der meisten inkrementellen Prüfungen auf nur wenige Sekunden reduzieren. Das schien uns ein großer Erfolg zu sein.
Noch mehr Leistung!
Zusammen mit dem bereits erwähnten Remote-Caching hat mypy praktisch alle Probleme gelöst, die beim häufigen Ausführen der Typüberprüfung durch Programmierer auftreten, wenn nur wenige Dateien geändert werden. Dennoch blieb die Systemleistung in der ungünstigsten Nutzungssituation nach wie vor suboptimal. Ein reiner mypy-Durchlauf konnte über 15 Minuten in Anspruch nehmen. Das war weit mehr, als wir uns gewünscht hätten. Jede Woche verschlechterte sich die Situation weiter, da die Programmierer weiterhin neuen Code schrieben und Annotationen zu bestehendem Code hinzufügten. Unsere Benutzer verlangten nach mehr Leistung, und wir waren gerne bereit, ihren Bedürfnissen nachzukommen.
Wir haben uns entschieden, zu einer der frühen Ideen von mypy zurückzukehren. Nämlich zur Umwandlung von Python-Code in C-Code. Experimente mit Cython (einer Plattform, die es ermöglicht, in Python geschriebenen Code in C-Code zu übersetzen) haben uns kein sichtbares Beschleunigungsresultat gebracht, also beschlossen wir, die Idee zu erneuern, einen eigenen Compiler zu entwickeln. Da der mypy-Code (in Python geschrieben) bereits alle notwendigen Typannotationen enthielt, schien es uns sinnvoll, diese Annotationsmöglichkeiten zu nutzen, um die Systemleistung zu optimieren. Ich habe schnell einen Prototyp zur Überprüfung dieser Idee erstellt. Dieser zeigte bei verschiedenen Mikro-Benchmarks ein über zehnmal besseres Leistungswachstum. Unsere Idee war es, Python-Module mit Cython in C-Module zu kompilieren und die Typannotationen in Typprüfungen umzuwandeln, die zur Laufzeit des Programms durchgeführt werden (normalerweise werden Typannotationen während der Programmausführung ignoriert und nur von Typprüfungs-Systemen verwendet). Wir planten tatsächlich, die mypy-Implementierung von Python in eine statisch typisierte Sprache zu übertragen, die genau so aussehen sollte (und größtenteils wie Python funktionieren sollte). (Diese Form der intersprachlichen Migration wurde zu einer Art Tradition des mypy-Projekts. Die ursprüngliche Implementierung von mypy wurde in Alore geschrieben, gefolgt von einer syntaktischen Hybridversion von Java und Python.)
Die Fokussierung auf die API-Erweiterungen von CPython war entscheidend, um die Projektmanagementfähigkeiten nicht zu verlieren. Es war nicht notwendig, eine virtuelle Maschine oder spezielle Bibliotheken für mypy zu implementieren. Zudem blieb uns die gesamte Python-Ökosystem zugänglich, einschließlich aller Werkzeuge wie pytest. Das bedeutete, dass wir weiterhin interpretierten Python-Code während der Entwicklung nutzen konnten, was uns ermöglichte, mit einem sehr schnellen Schema zur Codeänderung und -tests fortzufahren, anstatt auf die Kompilierung des Codes zu warten. Es fühlte sich an, als ob wir es erfolgreich schafften, sozusagen auf zwei Hochzeiten gleichzeitig zu tanzen, und das gefiel uns.
Der Compiler, den wir mypyc genannt haben (da er als Frontend mypy zur Typanalyse verwendet), hat sich als sehr erfolgreiches Projekt erwiesen. Insgesamt haben wir eine etwa vierfache Beschleunigung der häufigen Ausführungen von mypy ohne den Einsatz von Caching erreicht. Die Entwicklung des Kernprojekts mypyc nahm bei unserem kleinen Team, zu dem Michael Sullivan, Ivan Levkivskyi, Hugh Han und ich gehörten, etwa 4 Kalendermonate in Anspruch. Dieser Aufwand war deutlich geringer als der, der nötig gewesen wäre, um mypy beispielsweise in C++ oder Go neu zu schreiben. Auch die notwendigen Änderungen im Projekt waren weitaus weniger umfangreich, als sie bei einer Neuprogrammierung in einer anderen Sprache gewesen wären. Darüber hinaus hatten wir die Hoffnung, dass wir mypyc so weit bringen könnten, dass auch andere Programmierer von Dropbox es zur Kompilierung und Beschleunigung ihres Codes nutzen können.
Um ein solches Leistungsniveau zu erreichen, mussten wir einige interessante ingenieurtechnische Lösungen anwenden. So kann der Compiler die Ausführung vieler Operationen durch die Nutzung schneller, grundlegender C-Konstrukte beschleunigen. Zum Beispiel wird der Aufruf einer kompilierten Funktion in einen C-Funktionsaufruf übersetzt. Ein solcher Aufruf erfolgt viel schneller als der einer interpretierten Funktion. Einige Operationen, wie z. B. die Suche in Dictionaries, basierten jedoch immer noch auf herkömmlichen Aufrufen der C-API aus CPython, die nach der Kompilierung nur wenig schneller waren. Wir konnten die zusätzliche Belastung des Systems, die durch die Interpretation verursacht wurde, beseitigen, aber dies brachte nur einen geringen Performance-Gewinn.
Um die häufigsten "langsamen" Operationen zu identifizieren, haben wir eine Code-Profilierung durchgeführt. Mit den gewonnenen Daten haben wir versucht, entweder mypyc so zu optimieren, dass es effizienteren C-Code für diese Operationen generiert, oder den entsprechenden Python-Code mit schnelleren Operationen neu zu schreiben (manchmal hatten wir einfach nicht die Möglichkeit, eine einfache Lösung für ein bestimmtes Problem zu finden). Das Überarbeiten des Python-Codes stellte sich oft als einfachere Lösung heraus, als die automatische Umsetzung derselben Transformationen im Compiler zu realisieren. Langfristig strebten wir an, viele dieser Transformationen zu automatisieren, doch in diesem Moment wollten wir mypy mit minimalem Aufwand beschleunigen. Auf dem Weg zu diesem Ziel haben wir einige Abkürzungen genommen.
Die Fortsetzung folgt…
Sehr geehrte Leser! Welche Eindrücke hatte das mypy-Projekt, als Sie von seiner Existenz erfuhren?
Quelle: habr.com
