Cinder ist Metas interne leistungsorientierte Produktionsversion von CPython 3.10. Es enthält eine Reihe von Leistungsoptimierungen, darunter Bytecode-Inline-Caching, eifrige Auswertung von Coroutinen, einen method-at-a-time JIT und einen experimentellen Bytecode-Compiler, der Typanmerkungen verwendet, um typspezialisierten Bytecode auszugeben, der im JIT eine bessere Leistung erbringt.
Cinder betreibt Instagram, wo es seinen Anfang nahm, und wird zunehmend in immer mehr Python-Anwendungen in Meta verwendet.
Weitere Informationen zu CPython finden Sie unter README.cpython.rst
.
Kurze Antwort: Nein.
Wir haben Cinder öffentlich zugänglich gemacht, um die Diskussion über ein mögliches Upstreaming einiger dieser Arbeiten auf CPython zu erleichtern und Doppelarbeit unter den Leuten zu reduzieren, die an der Leistung von CPython arbeiten.
Cinder wird nicht für die Verwendung durch Dritte poliert oder dokumentiert. Wir haben nicht den Wunsch, dass es eine Alternative zu CPython wird. Unser Ziel bei der Bereitstellung dieses Codes ist ein einheitliches, schnelleres CPython. Während wir Cinder also in der Produktion betreiben, sind Sie auf sich allein gestellt, wenn Sie sich dafür entscheiden. Wir können uns nicht dazu verpflichten, externe Fehlerberichte zu beheben oder Pull-Requests zu überprüfen. Wir stellen sicher, dass Cinder für unsere Produktions-Workloads ausreichend stabil und schnell ist, geben jedoch keine Gewähr für die Stabilität, Korrektheit oder Leistung für externe Workloads oder Anwendungsfälle.
Das heißt, wenn Sie Erfahrung mit dynamischen Sprachlaufzeiten haben und Ideen haben, wie Sie Cinder schneller machen können; oder wenn Sie an CPython arbeiten und Cinder als Inspiration für Verbesserungen in CPython nutzen möchten (oder beim Upstreaming von Teilen von Cinder nach CPython helfen möchten), wenden Sie sich bitte an uns. Wir würden uns gerne unterhalten!
Cinder sollte genau wie CPython erstellt werden. configure
und make -j
. Da die Entwicklung und Nutzung von Cinder jedoch größtenteils im hochspezifischen Kontext von Meta erfolgt, nutzen wir es in anderen Umgebungen nicht häufig. Daher besteht die zuverlässigste Möglichkeit, Cinder zu erstellen und auszuführen, darin, das Docker-basierte Setup aus unserem GitHub CI-Workflow wiederzuverwenden.
Wenn Sie nur einen funktionierenden Cinder erhalten möchten, ohne ihn selbst zu erstellen, ist unser Runtime-Docker-Image am einfachsten (kein Repo-Klon erforderlich!):
docker run -it --rm ghcr.io/facebookincubator/cinder-runtime:cinder-3.10
Wenn Sie es selbst bauen möchten:
git clone https://github.com/facebookincubator/cinder
docker run -v "$PWD/cinder:/vol" -w /vol -it --rm ghcr.io/facebookincubator/cinder/python-build-env:latest bash
./configure && make
Bitte beachten Sie, dass Cinder nur auf Linux x64 erstellt oder getestet wurde; Alles andere (einschließlich macOS) wird wahrscheinlich nicht funktionieren. Das obige Docker-Image basiert auf Fedora Linux und wurde aus einer Docker-Spezifikationsdatei im Cinder-Repo erstellt: .github/workflows/python-build-env/Dockerfile
.
Es gibt einige neue Testziele, die interessant sein könnten. make testcinder
ist im Wesentlichen dasselbe wie make test
außer dass einige Tests übersprungen werden, die in unserer Entwicklungsumgebung problematisch sind. make testcinder_jit
führt die Testsuite mit vollständig aktiviertem JIT aus, sodass alle Funktionen JIT-fähig sind. make testruntime
führt eine Reihe von C++-gtest-Komponententests für die JIT aus. Und make test_strict_module
führt eine Testsuite für strikte Module aus (siehe unten).
Beachten Sie, dass diese Schritte eine Cinder-Python-Binärdatei ohne aktivierte PGO/LTO-Optimierungen erzeugen. Erwarten Sie also nicht, dass Sie diese Anweisungen verwenden, um eine Beschleunigung bei Python-Workloads zu erzielen.
Cinder Explorer ist ein Live-Spielplatz, auf dem Sie sehen können, wie Cinder Python-Code von der Quelle bis zur Assembly kompiliert – Sie können es gerne ausprobieren! Fühlen Sie sich frei, Funktionsanfragen und Fehlerberichte einzureichen. Bedenken Sie, dass der Cinder Explorer, wie auch der Rest davon, nach bestem Wissen und Gewissen „unterstützt“ wird.
Instagram verwendet eine Multiprozess-Webserverarchitektur; Der übergeordnete Prozess startet, führt Initialisierungsarbeiten durch (z. B. Laden von Code) und teilt Dutzende von Arbeitsprozessen auf, um Client-Anfragen zu bearbeiten. Arbeitsprozesse werden aus verschiedenen Gründen (z. B. Speicherlecks, Codebereitstellungen) regelmäßig neu gestartet und haben eine relativ kurze Lebensdauer. In diesem Modell muss das Betriebssystem die gesamte Seite kopieren, die ein Objekt enthält, das im übergeordneten Prozess zugewiesen wurde, wenn der Referenzzähler des Objekts geändert wird. In der Praxis überleben die im übergeordneten Prozess zugewiesenen Objekte die Arbeiter; Die ganze Arbeit, die mit der Referenzzählung verbunden ist, ist unnötig.
Instagram verfügt über eine sehr große Python-Codebasis und der Overhead durch das Copy-on-Write durch die Referenzzählung langlebiger Objekte erwies sich als erheblich. Wir haben eine Lösung namens „Unsterbliche Instanzen“ entwickelt, um eine Möglichkeit zu bieten, Objekte von der Referenzzählung auszuschließen. Weitere Informationen finden Sie unter Include/object.h. Diese Funktion wird durch die Definition von Py_IMMORTAL_INSTANCES gesteuert und ist in Cinder standardmäßig aktiviert. Dies war ein großer Gewinn für uns in der Produktion (~5 %), aber es verlangsamt den linearen Code. Referenzzählvorgänge kommen häufig vor und müssen prüfen, ob ein Objekt an der Referenzzählung teilnimmt, wenn diese Funktion aktiviert ist.
„Shadowcode“ oder „Shadow Bytecode“ ist unsere Implementierung eines spezialisierten Interpreters. Es beobachtet bestimmte optimierbare Fälle bei der Ausführung generischer Python-Opcodes und ersetzt (für Hot-Funktionen) diese Opcodes dynamisch durch spezialisierte Versionen. Der Kern von Shadowcode liegt in Shadowcode/shadowcode.c
, obwohl die Implementierungen für die spezialisierten Bytecodes zusammen mit dem Rest der Auswertungsschleife in Python/ceval.c
liegen. Shadowcode-spezifische Tests befinden sich in Lib/test/test_shadowcode.py
.
Es ähnelt im Geiste dem spezialisierten adaptiven Interpreter (PEP-659), der in CPython 3.11 integriert wird.
Der Instagram-Server ist eine asynchrone Arbeitslast, bei der jede Webanfrage Hunderttausende asynchrone Aufgaben auslösen kann, von denen viele ohne Unterbrechung erledigt werden können (z. B. dank gespeicherter Werte).
Wir haben das Vectorcall-Protokoll erweitert, um ein neues Flag, Ci_Py_AWAITED_CALL_MARKER
, zu übergeben, das angibt, dass der Anrufer sofort auf diesen Anruf wartet.
Bei Verwendung mit asynchronen Funktionsaufrufen, die sofort erwartet werden, können wir die aufgerufene Funktion sofort (eifrig) auswerten, bis zum Abschluss oder bis zu ihrer ersten Unterbrechung. Wenn die Funktion ohne Unterbrechung abgeschlossen wird, können wir den Wert sofort und ohne zusätzliche Heap-Zuweisungen zurückgeben.
Bei Verwendung mit Async Gather können wir den Satz übergebener Waitables sofort (eifrig) auswerten und so möglicherweise die Kosten für die Erstellung und Planung mehrerer Aufgaben für Coroutinen vermeiden, die synchron abgeschlossen werden könnten, abgeschlossene Futures, gespeicherte Werte usw.
Diese Optimierungen führten zu einer erheblichen (~5 %) Verbesserung der CPU-Effizienz.
Dies wird größtenteils in Python/ceval.c
über ein neues Vectorcall-Flag Ci_Py_AWAITED_CALL_MARKER
implementiert, das anzeigt, dass der Anrufer sofort auf diesen Anruf wartet. Suchen Sie nach Verwendungsmöglichkeiten für das IS_AWAITED()
Makro und dieses Vectorcall-Flag.
Das Cinder JIT ist ein benutzerdefiniertes JIT mit jeweils einer Methode, das in C++ implementiert ist. Die Aktivierung erfolgt über das -X jit
oder die Umgebungsvariable PYTHONJIT=1
. Es unterstützt fast alle Python-Opcodes und kann bei vielen Python-Leistungsbenchmarks eine 1,5- bis 4-fache Geschwindigkeitsverbesserung erzielen.
Wenn diese Option aktiviert ist, wird jede Funktion, die jemals aufgerufen wird, per JIT kompiliert, was Ihr Programm aufgrund des Mehraufwands für die JIT-Kompilierung selten aufgerufener Funktionen möglicherweise langsamer und nicht schneller macht. Die Option -X jit-list-file=/path/to/jitlist.txt
oder PYTHONJITLISTFILE=/path/to/jitlist.txt
kann auf eine Textdatei verweisen, die vollständig qualifizierte Funktionsnamen enthält (in der Form path.to.module:funcname
oder path.to.module:ClassName.method_name
), eine pro Zeile, die JIT-kompiliert werden sollte. Wir verwenden diese Option, um nur eine Reihe von Hot-Funktionen zu kompilieren, die aus Produktionsprofilierungsdaten abgeleitet werden. (Ein typischerer Ansatz für eine JIT wäre die dynamische Kompilierung von Funktionen, da beobachtet wird, dass sie häufig aufgerufen werden. Es hat sich für uns noch nicht gelohnt, dies zu implementieren, da unsere Produktionsarchitektur ein Pre-Fork-Webserver ist Aus Gründen der Speicherfreigabe möchten wir die gesamte JIT-Kompilierung im ersten Prozess vorab durchführen, bevor die Worker geforkt werden. Dies bedeutet, dass wir die Arbeitslast im Prozess nicht beobachten können, bevor wir entscheiden, welche Funktionen per JIT kompiliert werden sollen.)
Das JIT befindet sich im Jit/
-Verzeichnis und seine C++-Tests befinden sich in RuntimeTests/
(führen Sie diese mit make testruntime
aus). Es gibt auch einige Python-Tests dafür in Lib/test/test_cinderjit.py
; Diese erheben keinen Anspruch auf Vollständigkeit, da wir die gesamte CPython-Testsuite unter der JIT über make testcinder_jit
ausführen. Sie decken JIT-Edge-Fälle ab, die sonst in der CPython-Testsuite nicht zu finden sind.
Weitere -X
Optionen und Umgebungsvariablen, die das Verhalten von JIT beeinflussen, finden Sie unter Jit/pyjit.cpp
. In dieser Datei ist auch ein cinderjit
Modul definiert, das einige JIT-Dienstprogramme für Python-Code verfügbar macht (z. B. das Kompilieren einer bestimmten Funktion erzwingen, prüfen, ob eine Funktion kompiliert ist, JIT deaktivieren). Beachten Sie, dass cinderjit.disable()
nur die zukünftige Kompilierung deaktiviert; Es kompiliert sofort alle bekannten Funktionen und behält vorhandene JIT-kompilierte Funktionen bei.
Das JIT senkt zunächst den Python-Bytecode auf eine Zwischendarstellung auf hoher Ebene (HIR); Dies ist in Jit/hir/
implementiert. HIR entspricht ziemlich genau dem Python-Bytecode, obwohl es sich um eine Registermaschine und nicht um eine Stapelmaschine handelt, ist es auf einer etwas niedrigeren Ebene, es ist typisiert und einige Details werden vom Python-Bytecode verdeckt, sind aber für die Leistung wichtig (insbesondere die Referenzzählung). explizit in HIR offengelegt. HIR wird in die SSA-Form umgewandelt, einige Optimierungsdurchläufe werden darauf durchgeführt und dann werden automatisch Referenzzähloperationen entsprechend den Metadaten über den Refcount und die Speichereffekte von HIR-Opcodes eingefügt.
HIR wird dann auf eine Low-Level-Intermediate-Repräsentation (LIR) herabgestuft, bei der es sich um eine Abstraktion über Assembler handelt, die in Jit/lir/
implementiert ist. In LIR führen wir eine Registerzuordnung durch, einige zusätzliche Optimierungsdurchläufe und schließlich wird LIR mithilfe der hervorragenden asmjit-Bibliothek auf Assembly (in Jit/codegen/
) herabgesetzt.
Das JIT befindet sich in einem frühen Stadium. Obwohl es den Overhead der Interpreterschleife bereits eliminieren kann und für viele Funktionen erhebliche Leistungsverbesserungen bietet, haben wir erst begonnen, an der Oberfläche möglicher Optimierungen zu kratzen. Viele gängige Compiler-Optimierungen sind noch nicht implementiert. Unsere Priorisierung von Optimierungen wird weitgehend von den Merkmalen der Instagram-Produktionsauslastung bestimmt.
Strikte Module sind ein paar Dinge in einem:
1. Ein statischer Analysator, der validieren kann, dass die Ausführung des Top-Level-Codes eines Moduls keine Nebenwirkungen hat, die außerhalb dieses Moduls sichtbar sind.
2. Ein unveränderlicher StrictModule
-Typ, der anstelle des Standardmodultyps von Python verwendet werden kann.
3. Ein Python-Modullader, der in der Lage ist, Module zu erkennen, die sich für den strikten Modus entschieden haben (über einen import __strict__
oben im Modul), sie zu analysieren, um sicherzustellen, dass keine Nebenwirkungen beim Import auftreten, und sie in sys.modules
als StrictModule
-Objekt aufzufüllen.
Static Python ist ein Bytecode-Compiler, der Typanmerkungen verwendet, um typspezialisierten und typgeprüften Python-Bytecode auszugeben. In Verbindung mit Cinder JIT kann es in vielen Fällen eine ähnliche Leistung wie MyPyC oder Cython liefern und gleichzeitig ein reines Python-Entwicklererlebnis bieten (normale Python-Syntax, kein zusätzlicher Kompilierungsschritt). Static Python plus Cinder JIT erreicht auf einer typisierten Version des Richards-Benchmarks die 18-fache Leistung von Standard-CPython. Bei Instagram haben wir Static Python erfolgreich in der Produktion eingesetzt, um alle Cython-Module in unserer primären Webserver-Codebasis zu ersetzen, ohne Leistungseinbußen.
Der statische Python-Compiler basiert auf dem Python- compiler
Modul, das aus der Standardbibliothek in Python 3 entfernt wurde und seitdem extern gepflegt und aktualisiert wird; Dieser Compiler ist in Cinder in Lib/compiler
integriert. Der statische Python-Compiler ist in Lib/compiler/static/
implementiert und seine Tests befinden sich in Lib/test/test_compiler/test_static.py
.
In statischen Python-Modulen definierte Klassen erhalten automatisch typisierte Slots (basierend auf der Überprüfung ihrer typisierten Klassenattribute und annotierten Zuweisungen in __init__
), und das Laden und Speichern von Attributen für Instanzen dieser Typen verwendet neue STORE_FIELD
und LOAD_FIELD
-Opcodes, die im JIT direkt werden lädt/speichert von/zu einem festen Speicheroffset im Objekt, ohne die Indirektion eines LOAD_ATTR
oder STORE_ATTR
. Klassen erhalten außerdem Vtables ihrer Methoden zur Verwendung durch die unten erwähnten INVOKE_*
Opcodes. Die Laufzeitunterstützung für diese Funktionen befindet sich in StaticPython/classloader.h
und StaticPython/classloader.c
.
Eine statische Python-Funktion beginnt mit einem versteckten Prolog, der prüft, ob die Typen der bereitgestellten Argumente mit den Typanmerkungen übereinstimmen, und andernfalls TypeError
auslöst. Aufrufe einer statischen Python-Funktion an eine andere statische Python-Funktion überspringen diesen Opcode (da die Typen bereits vom Compiler validiert wurden). Statisch-zu-statische Aufrufe können auch einen Großteil des Overheads eines typischen Python-Funktionsaufrufs vermeiden. Wir geben einen INVOKE_FUNCTION
oder INVOKE_METHOD
-Opcode aus, der Metadaten über die aufgerufene Funktion oder Methode enthält; Dies plus optional unveränderliche Module (über StrictModule
) und Typen (über cinder.freeze_type()
, die wir derzeit auf alle Typen in strikten und statischen Modulen in unserem Import-Loader anwenden, aber in Zukunft möglicherweise ein fester Bestandteil von Static Python werden) und kompilieren Dank der Kenntnis der aufgerufenen Signatur können wir (im JIT) viele Python-Funktionsaufrufe mithilfe der x64-Aufrufkonvention in direkte Aufrufe an eine feste Speicheradresse umwandeln, mit kaum mehr Overhead als bei einem C-Funktionsaufruf.
Statisches Python wird immer noch schrittweise typisiert und unterstützt Code, der nur teilweise mit Anmerkungen versehen ist oder unbekannte Typen verwendet, indem es auf das normale dynamische Verhalten von Python zurückgreift. In einigen Fällen (z. B. wenn ein Wert eines statisch unbekannten Typs von einer Funktion mit einer Rückgabeanmerkung zurückgegeben wird) wird ein Laufzeit CAST
Opcode eingefügt, der TypeError
auslöst, wenn der Laufzeittyp nicht mit dem erwarteten Typ übereinstimmt.
Static Python unterstützt auch neue Typen für Maschinen-Ganzzahlen, Bools, Doubles und Vektoren/Arrays. In der JIT werden diese als nicht geschachtelte Werte behandelt, und beispielsweise vermeidet die primitive Ganzzahlarithmetik jeglichen Python-Overhead. Einige Operationen an integrierten Typen (z. B. list oder dictionary subscript oder len()
) werden ebenfalls optimiert.
Cinder unterstützt die schrittweise Einführung statischer Module über einen strikten/statischen Modullader, der statische Module automatisch erkennen und sie mit modulübergreifender Kompilierung als statisch laden kann. Der Loader sucht am Anfang einer Datei nach den Annotationen import __static__
und import __strict__
und kompiliert die Module entsprechend. Um den Loader zu aktivieren, haben Sie eine von drei Möglichkeiten:
1. Installieren Sie den Loader explizit auf der obersten Ebene Ihrer Anwendung über from cinderx.compiler.strict.loader import install; install()
.
PYTHONINSTALLSTRICTLOADER=1
in Ihrer Umgebung fest../python -X install-strict-loader application.py
aus. Alternativ können Sie den gesamten Code statisch kompilieren, indem Sie ./python -m compiler --static some_module.py
verwenden, wodurch das Modul als statisches Python kompiliert und ausgeführt wird.
Ausführlichere Dokumentation finden Sie unter CinderDoc/static_python.rst
.