Was sind die Herausforderungen beim Schreiben eines Compilers für eine dynamisch typisierte Sprache?

9

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?

Syntagma
quelle
In diesem Fall ist die dynamische Eingabe bei weitem nicht das größte Problem. Für Python ist es ein dynamisches Scoping.
SK-Logik
Es ist erwähnenswert, dass andere Leute argumentiert haben, dass das Einbauen von dynamischer Eingabe in die Plattform hier die richtige Antwort ist. Microsoft hat genau aus diesem Grund viel Geld in das DLR gesteckt - und NeXT / Apple ist seit Jahrzehnten auf halbem Weg. Das hilft CPython nicht, aber IronPython beweist, dass Sie Python effektiv statisch kompilieren können, und PyPy beweist, dass Sie dies nicht müssen.
Abarnert
2
@ SK-Logik Dynamisches Scoping in Python? Zuletzt habe ich überprüft, dass alle Konstrukte in der Sprache lexikalisches Scoping verwenden.
1
@ SK-Logik Sie können dynamisch Code erstellen und ausführen, aber dieser Code wird auch lexikalisch ausgeführt. Für jede einzelne Variable in einem Python-Programm können Sie einfach durch Überprüfen des AST bestimmen, zu welchem ​​Bereich eine Variable gehört. Sie denken vielleicht an die execAussage , 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).
1
@ SK-Logik Das einzige, was für mich ein Implementierungsdetail ist, sind Änderungen am Rückgabewert des locals()Fortbestehens über Aufrufe von locals. Was dokumentiert ist und definitiv kein Implementierungsdetail ist, ist, dass nicht einmal localsoder globalsin 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, evalund execsind definitiv auch keine Implementierungsdetails - siehe meine Antwort!)

Antworten:

16

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
4

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:

i = 2
x = 3 + i

def prn(s):
    print(s)

i = prn
i(x)

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 fooeine Ganzzahl ist. Dann kann PyPy den Code optimieren, der foobei jedem Durchlauf durch die Schleife nach dem Typ sucht , und häufig sogar das Python-Objekt entfernen , das eine Ganzzahl darstellt, und fookann 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.

steveha
quelle
Bei Unladen Swallow ging es nicht um statische Kompilierung oder statische Typen. Das letztendliche Ziel bestand darin, den CPython-Interpreter mit all seiner Dynamik mit einer ausgefallenen neuen JIT (ähnlich wie Parrot oder DLR für .NET… oder PyPy) auf LLVM zu portieren, obwohl das, was sie tatsächlich waren Dabei wurden viele lokale Optimierungen in CPython gefunden (von denen einige in Mainline 3.x aufgenommen wurden). Shedskin ist wahrscheinlich das Projekt, an das Sie denken und das statische Typinferenz zum statischen Kompilieren von Python verwendet hat (obwohl in C ++, nicht direkt in nativem Code).
Abarnert
Einer der Autoren von Unladen Swallow, Reid Kleckner, veröffentlichte eine Retrospektive von Unladen Swallow , die in diesem Zusammenhang möglicherweise lesenswert ist, obwohl es in Wirklichkeit mehr um Management- und Sponsoring-Herausforderungen als um technische geht.
Abarnert
0

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.

Ira Baxter
quelle