In diesem Vortrag spricht Guido van Rossum (27:30) über Versuche, einen Compiler für Python-Code zu schreiben, und kommentiert ihn mit den Worten:
Es stellt sich heraus, dass es nicht so einfach ist, einen Compiler zu schreiben, der alle netten dynamischen Typisierungseigenschaften und die semantische Korrektheit Ihres Programms beibehält, so dass er tatsächlich dasselbe tut, egal welche Art von Verrücktheit Sie irgendwo unter der Decke tun und tatsächlich ausführen schneller
Was sind die (möglichen) Herausforderungen beim Schreiben eines Compilers für eine dynamisch typisierte Sprache wie Python?
exec
Aussage , die seit 3.0 weg ist und daher außerhalb meiner Überlegungen liegt (und wahrscheinlich an Guidos, da der Vortrag aus dem Jahr 2012 stammt). Könnten Sie ein Beispiel geben? Und Ihre Definition von "dynamischem Scoping", wenn es [anders als meins] ist (en.wikipedia.org/wiki/Dynamic_scoping).locals()
Fortbestehens über Aufrufe vonlocals
. Was dokumentiert ist und definitiv kein Implementierungsdetail ist, ist, dass nicht einmallocals
oderglobals
in welchem Bereich jede Variable nachgeschlagen werden kann. Für jede einzelne Verwendung einer Variablen wird der Bereich, auf den verwiesen wird, statisch bestimmt. Das macht es entschieden lexikalisch. (Und übrigens,eval
undexec
sind definitiv auch keine Implementierungsdetails - siehe meine Antwort!)Antworten:
Sie haben Guidos Aussage bei der Formulierung Ihrer Frage zu stark vereinfacht. Das Problem besteht nicht darin, einen Compiler für eine dynamisch typisierte Sprache zu schreiben. Das Problem besteht darin, eine zu schreiben, die (Kriterium 1) immer korrekt ist, (Kriterium 2) die dynamische Typisierung beibehält und (Kriterium 3) für eine erhebliche Menge Code spürbar schneller ist.
Es ist einfach, 90% (fehlgeschlagene Kriterien 1) von Python zu implementieren und konstant schnell zu sein. Ebenso ist es einfach, eine schnellere Python-Variante mit statischer Typisierung zu erstellen (fehlgeschlagene Kriterien 2). Die Implementierung von 100% ist ebenfalls einfach (sofern die Implementierung einer so komplexen Sprache einfach ist), aber bisher ist jede einfache Implementierung relativ langsam (fehlgeschlagene Kriterien 3).
Die Implementierung eines Interpreters plus JIT , der korrekt ist, die gesamte Sprache implementiert und für einige Codes schneller ist, erweist sich als machbar, wenn auch erheblich schwieriger (vgl. PyPy) und nur dann, wenn Sie die Erstellung des JIT-Compilers automatisieren (Psyco hat darauf verzichtet) , war aber sehr begrenzt in welchem Code es beschleunigen konnte). Beachten Sie jedoch, dass dies ausdrücklich außerhalb des Geltungsbereichs liegt, da es sich um statische Aufladung handelt(auch bekannt als vorzeitig) Compiler. Ich erwähne dies nur, um zu erklären, warum sein Ansatz für statische Compiler nicht funktioniert (oder zumindest kein Gegenbeispiel vorhanden ist): Er muss zuerst das Programm interpretieren und beobachten und dann Code für eine bestimmte Iteration einer Schleife (oder eines anderen linearen Codes) generieren Pfad), dann optimieren Sie die Hölle daraus basierend auf Annahmen, die nur für diese bestimmte Iteration gelten (oder zumindest nicht für alle möglichen Iterationen). Die Erwartung ist, dass viele spätere Ausführungen dieses Codes ebenfalls der Erwartung entsprechen und somit von den Optimierungen profitieren. Einige (relativ billige) Prüfungen werden hinzugefügt, um die Richtigkeit sicherzustellen. Um all dies zu tun, benötigen Sie eine Vorstellung davon, worauf Sie sich spezialisieren müssen, und eine langsame, aber allgemeine Implementierung, auf die Sie zurückgreifen können. AOT-Compiler haben keine. Sie können sich überhaupt nicht spezialisierenbasierend auf Code, den sie nicht sehen können (z. B. dynamisch geladener Code), und nachlässige Spezialisierung bedeutet, mehr Code zu generieren, was eine Reihe von Problemen mit sich bringt (Icache-Nutzung, Binärgröße, Kompilierungszeit, zusätzliche Verzweigungen).
Die Implementierung eines AOT-Compilers, der die gesamte Sprache korrekt implementiert, ist ebenfalls relativ einfach: Generieren Sie Code, der zur Laufzeit aufgerufen wird, um das zu tun, was der Interpreter tun würde, wenn er mit diesem Code gespeist würde. Nuitka macht das (meistens). Dies bringt jedoch keinen großen Leistungsvorteil (Fehlerkriterium 3), da Sie immer noch genauso viel unnötige Arbeit wie ein Interpreter leisten müssen, außer den Bytecode an den C-Code-Block zu senden, der das tut, was Sie kompiliert haben Das sind nur relativ geringe Kosten - signifikant genug, um in einem vorhandenen Interpreter optimiert zu werden, aber nicht signifikant genug, um eine völlig neue Implementierung mit eigenen Problemen zu rechtfertigen.
Was wäre erforderlich, um alle drei Kriterien zu erfüllen? Wir haben keine Ahnung. Es gibt einige statische Analyseschemata, mit denen Informationen zu konkreten Typen, Kontrollabläufen usw. aus Python-Programmen extrahiert werden können. Diejenigen, die genaue Daten liefern, die über den Rahmen eines einzelnen Basisblocks hinausgehen, sind extrem langsam und müssen das gesamte Programm oder zumindest den größten Teil davon sehen. Trotzdem können Sie mit diesen Informationen nicht viel anfangen, außer vielleicht ein paar Operationen für eingebaute Typen zu optimieren.
Warum ist das? Um es ganz klar auszudrücken: Ein Compiler entfernt entweder die Möglichkeit, zur Laufzeit geladenen Python-Code auszuführen (fehlgeschlagenes Kriterium 1), oder er trifft keine Annahmen, die von einem Python-Code überhaupt ungültig gemacht werden können. Leider beinhaltet dies so ziemlich alles, was für die Optimierung von Programmen nützlich ist: Globale einschließlich Funktionen können zurückgebunden werden, Klassen können mutiert oder vollständig ersetzt werden, Module können ebenfalls willkürlich geändert werden, der Import kann auf verschiedene Arten entführt werden usw. Eine einzelne Zeichenfolge, die an übergeben wird
eval
,exec
,__import__
oder zahlreiche andere Funktionen können dies tun. In der Tat bedeutet dies, dass fast keine großen Optimierungen angewendet werden können, was nur einen geringen Leistungsvorteil bringt (fehlgeschlagene Kriterien 3). Zurück zum obigen Absatz.quelle
Das schwierigste Problem besteht darin, herauszufinden, welchen Typ alles zu einem bestimmten Zeitpunkt hat.
In einer statischen Sprache wie C oder Java wissen Sie, sobald Sie die Typdeklaration gesehen haben, was dieses Objekt ist und was es tun kann. Wenn eine Variable deklariert ist
int
, ist sie eine Ganzzahl. Es ist beispielsweise keine aufrufbare Funktionsreferenz.In Python kann es sein. Das ist schreckliches Python, aber legal:
Nun, dieses Beispiel ist ziemlich dumm, aber es veranschaulicht die allgemeine Idee.
Realistischer können Sie eine integrierte Funktion durch eine benutzerdefinierte Funktion ersetzen, die etwas anderes ausführt (z. B. eine Version, die ihre Argumente protokolliert, wenn Sie sie aufrufen).
PyPy verwendet die Just-In-Time-Kompilierung, nachdem beobachtet wurde, was der Code tatsächlich tut. Dadurch kann PyPy die Dinge erheblich beschleunigen. PyPy kann eine Schleife überwachen und überprüfen, ob die Variable bei jeder Ausführung der Schleife
foo
eine Ganzzahl ist. Dann kann PyPy den Code optimieren, derfoo
bei jedem Durchlauf durch die Schleife nach dem Typ sucht , und häufig sogar das Python-Objekt entfernen , das eine Ganzzahl darstellt, undfoo
kann einfach zu einer Zahl werden, die in einem Register auf der CPU sitzt. So kann PyPy schneller sein als CPython. CPython führt die Typensuche so schnell wie möglich durch, aber nicht einmal die Suche ist noch schneller.Ich kenne die Details nicht, aber ich erinnere mich, dass es ein Projekt namens Unladen Swallow gab, das versuchte, statische Compilertechnologie anzuwenden, um Python zu beschleunigen (mit LLVM). Vielleicht möchten Sie bei Google nach Unladen Swallow suchen und herausfinden, warum es nicht so funktioniert hat, wie sie es sich erhofft hatten.
quelle
Wie die andere Antwort sagt, besteht das Hauptproblem darin, Typinformationen herauszufinden. Soweit Sie dies statisch tun können, können Sie direkt guten Code generieren.
Aber selbst wenn Sie dies nicht statisch tun können, können Sie zur Laufzeit angemessenen Code generieren, wenn Sie tatsächliche Typinformationen erhalten. Diese Informationen erweisen sich häufig als stabil oder haben höchstens einige unterschiedliche Werte für eine bestimmte Entität an einem bestimmten Codepunkt. Die Programmiersprache SELF war Vorreiter bei vielen Ideen zur aggressiven Erfassung von Laufzeitarten und zur Generierung von Laufzeitcode. Seine Ideen sind in modernen JIT-basierten Compilern wie Java und C # weit verbreitet.
quelle