Heute veröffentlichen wir den zweiten Teil der Übersetzung des Materials darüber, wie bei Dropbox die Typkontrolle für mehrere Millionen Zeilen Python-Code organisiert wurde.
→
Offizielle Typunterstützung (PEP 484)
Wir haben während der Hack Week 2014 die ersten ernsthaften Experimente mit mypy bei Dropbox durchgeführt. Die Hack Week ist eine Veranstaltung, die Dropbox eine Woche lang durchführt. In dieser Zeit können die Mitarbeiter an allem arbeiten, was sie möchten! Einige der berühmtesten technischen Projekte von Dropbox begannen an solchen Veranstaltungen. Aus diesen Experimenten haben wir die Erkenntnis gewonnen, dass mypy vielversprechend aussieht, obwohl dieses Projekt noch nicht für den breiten Einsatz bereit war.
Zu dieser Zeit schwebte die Idee, die Systeme zur Typunterstützung in Python zu standardisieren. Wie ich bereits erwähnt habe, konnten mit Python 3.0 Typannotations für Funktionen verwendet werden, 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 haben wir an der Standardisierung der Semantik gearbeitet. Diese Arbeit führte zur Entstehung von (dieses Dokument wurde gemeinsam von Guido van Rossum, Łukasz Langa und mir erstellt.)
Unsere Motive konnten aus zwei Perspektiven betrachtet werden. Erstens hofften wir, dass das gesamte Python-Ökosystem einen gemeinsamen Ansatz für die Verwendung von Typ-Hinweisen (type hints – ein in Python verwendeter Begriff, der als Äquivalent zu "Typannotationen" fungiert) annehmen könnte. Dies wäre, unter Berücksichtigung der möglichen Risiken, besser als die Verwendung vieler inkompatibler Ansätze. Zweitens wollten wir die Mechanismen zur Typannotierung offen mit vielen Vertretern der Python-Community diskutieren. Teils wurde dieses Verlangen davon diktiert, dass wir nicht als „Abtrünnige“ von den grundlegenden Ideen der Sprache in den Augen der breiten Masse der Python-Programmierer erscheinen wollten. Es handelt sich um eine dynamisch typisierte Sprache, die für ihre „Enten-Typisierung“ bekannt ist. In der Community gab es anfangs ein gewisses Misstrauen gegenüber der Idee der statischen Typisierung. Doch diese Haltung hat sich letztlich abgeschwächt – nachdem klar wurde, dass die statische Typisierung nicht verpflichtend gemacht werden soll (und nachdem die Menschen verstanden, dass sie wirklich nützlich ist).
Die letztlich angenommene Syntax der Typ-Hinweise ähnelte stark der, die 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 denke gern an dieses Ereignis als einen bedeutenden Meilenstein in der Geschichte von Python.
Beginn der Migration
Ende 2015 wurde bei Dropbox ein dreiköpfiges Team für die Arbeit an mypy gegründet. Dazu gehörten Guido van Rossum, Greg Price und David Fischer. Von diesem Zeitpunkt an begann sich die Situation sehr schnell zu entwickeln. Das erste Hindernis für das Wachstum von mypy war die Leistung. Wie ich bereits angedeutet habe, dachte ich in der frühen Entwicklungsphase des Projekts darüber nach, die Implementierung von mypy in die Programmiersprache C zu portieren, aber diese Idee wurde vorerst von der Liste gestrichen. Wir waren darauf angewiesen, dass das System mit dem Interpreter CPython betrieben wurde, der nicht die Geschwindigkeit aufweist, die für Werkzeuge wie mypy erforderlich ist. (Das Projekt PyPy, eine alternative Implementierung von Python mit einem JIT-Compiler, half uns auch nicht.)
Glücklicherweise kamen uns hier einige algorithmische Verbesserungen zur Hilfe. Der erste leistungsstarke „Beschleuniger“ war die Implementierung der inkrementellen Überprü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 des vorherigen Arbeitszyklus zwischengespeicherten Daten verwenden. Wir mussten nur die Typprüfung in den geänderten Dateien und in den Dateien, die von ihnen abhängen, durchführen. Mypy ging sogar noch einen Schritt weiter: Wenn sich die externe Schnittstelle des Moduls nicht geändert hat, betrachtet mypy es als unnötig, andere Module, die dieses Modul importieren, erneut zu überprüfen.
Die inkrementelle Überprüfung hat uns erheblich beim Annotieren großer Mengen bestehender Codes geholfen. Das liegt daran, dass dieser Prozess normalerweise viele iterative mypy-Durchläufe umfasst, da die Annotations schrittweise in den Code eingefügt und allmählich verbessert werden. Der erste mypy-Durchlauf war immer noch sehr langsam, da dabei viele Abhängigkeiten überprüft werden mussten. Um die Situation zu verbessern, haben wir einen Mechanismus für das Remote-Caching implementiert. Wenn mypy feststellt, dass der lokale Cache wahrscheinlich veraltet ist, lädt es den aktuellen Snapshot des Caches für die gesamte Codebasis aus einem zentralisierten Repository herunter. Anschließend führt es mit diesem Snapshot die inkrementelle Überprüfung durch. Das hat uns einen erheblichen Schritt in Richtung einer höheren mypy-Leistung gebracht.
Dies war eine Phase der schnellen und natürlichen Einführung des Typsystems bei Dropbox. Ende 2016 hatten wir bereits etwa 420000 Zeilen Python-Code mit Typannotationen. Viele Benutzer waren begeistert von der Typüberprüfung. Bei Dropbox wurde mypy immer mehr von Entwicklerteams genutzt.
Damals sah alles gut aus, aber es stand uns noch viel bevor. Wir begannen mit regelmäßigen internen Umfragen unter den Nutzern, um die Schwachstellen des Projekts zu identifizieren und zu verstehen, welche Fragen prioritär gelöst werden mussten (diese Praxis wird noch heute im Unternehmen angewendet). Die beiden wichtigsten Aufgaben wurden schnell klar. Erstens — wir benötigten eine größere Abdeckung des Codes mit Typen, zweitens — wir mussten sicherstellen, dass mypy schneller arbeitet. Es war offensichtlich, dass unser Bestreben, mypy zu beschleunigen und in die Projekte des Unternehmens zu integrieren, noch lange nicht abgeschlossen war. In vollem Bewusstsein der Bedeutung dieser beiden Aufgaben machten wir uns an deren Lösung.
Mehr Leistung!
Inkrementelle Überprüfungen haben mypy beschleunigt, aber dieses Werkzeug war immer noch nicht schnell genug. Viele inkrementelle Überprüfungen dauerten rund eine Minute. Der Grund dafür waren zirkuläre Importe. Dies wird wahrscheinlich niemanden überraschen, der schon einmal mit großen Codebasen in Python gearbeitet hat. Wir hatten Sätze mit Hunderten von Modulen, von denen jedes indirekt alle anderen importierte. Wenn eine Datei innerhalb eines Importzyklus geä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 das berüchtigte „Knäuel von Abhängigkeiten“, das viele Probleme bei Dropbox verursachte. Eines Tages umfasste diese Struktur mehrere Hundert Module, die direkt oder indirekt von vielen Tests importiert wurden und auch im Produktionscode verwendet wurden.
Wir haben die Möglichkeit der "Entwirrung" zyklischer Abhängigkeiten in Betracht gezogen, hatten jedoch nicht die Ressourcen, um dies umzusetzen. Es gab zu viel Code, mit dem wir nicht vertraut waren. Schließlich sind wir auf einen alternativen Ansatz gekommen. Wir haben beschlossen, dass mypy auch bei "Abhängigkeitsknäueln" schnell arbeiten sollte. Dies haben wir durch den mypy-Daemon erreicht. Der Daemon ist ein serverseitiger Prozess, der zwei interessante Funktionen bietet. Erstens speichert er alle Informationen über den gesamten Code in seinem Speicher. Das bedeutet, dass mypy bei jedem Start keine zwischengespeicherten Daten zu Tausenden von importierten Abhängigkeiten laden muss. Zweitens analysiert er die Abhängigkeiten zwischen Funktionen und anderen Entitäten auf einer feinen strukturellen Ebene. Zum Beispiel, wenn die Funktion foo die Funktion bar, besteht eine Abhängigkeit foo ab bar. Wenn sich eine Datei ändert, verarbeitet der Daemon zunächst isoliert nur die geänderte Datei. Dann überprüft er die sichtbaren Änderungen dieser Datei, wie etwa geänderte Funktionssignaturen. Der Daemon nutzt detaillierte Informationen über Importe nur zur Überprüfung der Funktionen, die tatsächlich die geänderte Funktion verwenden. In der Regel muss bei diesem Ansatz nur eine sehr geringe Anzahl von Funktionen überprüft werden.
Die Umsetzung all dessen war eine anspruchsvolle Aufgabe, da die ursprüngliche Implementierung von mypy stark darauf ausgelegt war, jeweils nur eine Datei zu verarbeiten. Wir mussten uns mit zahlreichen Grenzfällen auseinandersetzen, deren Auftreten wiederholte Überprüfungen erforderte, wenn sich im Code etwas änderte. Zum Beispiel geschieht dies, wenn einer Klasse eine neue Basisklasse zugewiesen wird. Nachdem wir erreicht hatten, was wir wollten, konnten wir die Ausführungszeit der meisten inkrementellen Überprüfungen auf einige Sekunden reduzieren. Das erschien uns als großer Erfolg.
Noch mehr Leistung!
Zusammen mit dem oben beschriebenen entfernten Caching hat die mypy-Demonstration nahezu alle Probleme beseitigt, die auftraten, wenn Programmierer häufig Typprüfungen durchführen und Änderungen an einer kleinen Anzahl von Dateien vornehmen. Allerdings war die Systemleistung im ungünstigsten Einsatzszenario nach wie vor weit von optimal entfernt. Ein sauberer mypy-Start konnte mehr als 15 Minuten in Anspruch nehmen. Das war weit mehr, als wir akzeptabel fanden. Jede Woche verschlechterte sich die Situation, da die Programmierer weiterhin neuen Code schrieben und Annotationen zu bestehendem Code hinzufügten. Unsere Benutzer sehnten sich immer noch nach höherer Leistung, und wir waren bereit, ihnen entgegenzukommen.
Wir haben beschlossen, zu einer der frühen Ideen bezüglich mypy zurückzukehren. Nämlich zur Umwandlung von Python-Code in C-Code. Experimente mit Cython (das ist ein System, das es ermöglicht, in Python geschriebenen Code in C-Code zu übersetzen) haben uns keine sichtbare Beschleunigung eingebracht, daher haben wir uns entschieden, die Idee, einen eigenen Compiler zu schreiben, wiederzubeleben. Da die mypy-Codebasis (die in Python geschrieben wurde) bereits alle erforderlichen Typannotationen enthielt, hielten wir es für wertvoll, zu versuchen, diese Annotationen zu nutzen, um die Systemleistung zu verbessern. Ich erstellte schnell einen Prototyp zur Überprüfung dieser Idee. Dieser zeigte bei verschiedenen Mikro-Benchmarks eine Leistungssteigerung von mehr als dem Zehnfachen. Unsere Idee war es, Python-Module mit Hilfe von 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üfsystemen verwendet). Tatsächlich planten wir, die Implementierung von mypy von Python in eine statisch typisierte Sprache zu übersetzen, die genau so aussehen (und größtenteils so funktionieren) würde wie Python. (Diese Art der intersprachlichen Migration wurde zu einer Art Tradition des mypy-Projekts. Die ursprüngliche Implementierung von mypy war in Alore geschrieben, dann gab es eine syntaktische Hybride aus Java und Python.)
Die Ausrichtung auf die API der CPython-Erweiterungen war der Schlüssel, um die Möglichkeiten des Projektmanagements nicht zu verlieren. Wir mussten keine virtuelle Maschine oder irgendwelche Bibliotheken implementieren, die mypy benötigte. Darüber hinaus hätten wir weiterhin auf das gesamte Ökosystem von Python zugreifen können, alle Werkzeuge (wie pytest) wären verfügbar gewesen. Das bedeutete, dass wir weiterhin interpretierbaren Python-Code während der Entwicklung verwenden konnten, was uns ermöglichte, sehr schnell Änderungen im Code vorzunehmen und ihn zu testen, anstatt auf die Kompilierung des Codes zu warten. Es sah so aus, als würde es uns hervorragend gelingen, um es so zu sagen, auf zwei Stühlen zu sitzen, und wir mochten es sehr.
Der Compiler, den wir mypyc genannt haben (da er als Frontend mypy zur Typanalyse verwendet), erwies sich als ein sehr erfolgreiches Projekt. Insgesamt haben wir eine etwa vierfache Beschleunigung der häufigen mypy-Starts ohne Einsatz von Caching erreicht. Die Entwicklung des Kernprojekts mypyc dauerte für ein kleines Team, zu dem Michael Sullivan, Ivan Levkovsky, Hugh Han und ich gehörten, etwa vier Kalendermonate. Dieses Maß an Arbeit war weit weniger umfangreich als das, das für eine Umschreibung von mypy, beispielsweise in C++ oder Go, erforderlich gewesen wäre. Auch mussten wir deutlich weniger Änderungen am Projekt vornehmen als bei einer Umschreibung in einer anderen Sprache. Darüber hinaus hofften wir, mypyc auf ein Niveau zu bringen, auf dem es auch andere Entwickler von Dropbox zur Kompilierung und Beschleunigung ihres Codes nutzen könnten.
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 Verwendung schneller, niederleveliger C-Konstrukte beschleunigen. Zum Beispiel wird der Aufruf einer kompilierten Funktion in einen C-Funktionsaufruf übersetzt. Dieser Aufruf wird deutlich schneller ausgeführt als ein Aufruf einer interpretierten Funktion. Einige Operationen, wie das Suchen in Wörterbüchern, beruhten jedoch weiterhin auf den gewöhnlichen C-API-Aufrufen aus CPython, die nach der Kompilierung nur geringfügig schneller waren. Wir konnten die zusätzliche Belastung des Systems, die durch die Interpretation verursacht wurde, beseitigen, aber in diesem Fall brachte das nur einen kleinen Gewinn in Bezug auf die Leistung.
Um die am häufigsten auftretenden "langsamen" Operationen zu identifizieren, führten wir eine Code-Profilierung durch. Ausgestattet mit den gewonnenen Daten versuchten wir entweder, mypyc so „zu optimieren“, dass es schnelleren C-Code für solche Operationen generiert, oder den entsprechenden Python-Code so umzuschreiben, dass schnellere Operationen verwendet werden (manchmal hatten wir schlichtweg nicht die passende einfache Lösung für ein bestimmtes Problem). Das Umschreiben des Python-Codes stellte sich oft als die einfachere Lösung des Problems heraus, als die automatisierte Durchführung derselben Transformation im Compiler zu implementieren. Langfristig wollten wir viele dieser Transformationen automatisieren, aber zu diesem Zeitpunkt konzentrierten wir uns darauf, mypy mit minimalem Aufwand zu beschleunigen. Und auf diesem Weg schnitten wir ein paar Ecken ab.
Fortsetzung folgt…
Liebe Leser! Welche Eindrücke hatte das Projekt mypy auf Sie, als Sie von dessen Existenz erfuhren?
Quelle: habr.com
