Wie kann Lisp sowohl dynamisch als auch kompiliert sein?

8

Okay, also zuerst, um das aus dem Weg zu bekommen: Ich habe die folgende Antwort gelesen:

Wie wird Lisp dynamisch und kompiliert?

aber ich verstehe seine Antwort nicht wirklich.

In einer Sprache wie Python der Ausdruck:

%Vor%

Kann nicht wirklich kompiliert werden, da es für den "Compiler" unmöglich ist, die Typen von a und b zu kennen (da die Typen nur zur Laufzeit bekannt sind) und daher, wie sie hinzugefügt werden.

Dies macht eine Sprache wie Python unmöglich ohne Typdeklarationen zu kompilieren, richtig? Mit Deklarationen weiß der Compiler, dass z.B. a und b sind Ganzzahlen und können daher hinzugefügt und in nativen Code übersetzt werden.

Wie geht es:

%Vor%

arbeiten?

Kompiliert als native vorzeitige Kompilierung definiert.

BEARBEITEN

In Wirklichkeit geht es bei dieser Frage eher darum, ob dynamische Sprachen ohne Typdeklarationen kompiliert werden können, und wenn ja, wie?

BEARBEITEN 2

Nach viel Recherche (d. h. eifrige Wikipedia-Browsing) glaube ich folgendes zu verstehen:

  • dynamisch typisierte Sprachen sind Sprachen, in denen die Typen zur Laufzeit überprüft werden
  • statische typisierte Sprachen sind Sprachen, in denen die Typen überprüft werden, wenn das Programm kompiliert wird
  • Typdeklarationen ermöglichen es dem Compiler, den Code effizienter zu machen, da er nicht mehr ständig API-Aufrufe ausführen muss, sondern mehr native "Funktionen" verwenden kann. Deshalb können Sie Cython-Code Typdeklarationen hinzufügen, um die Geschwindigkeit zu erhöhen muss nicht, weil es immer noch nur die Python-Bibliotheken im C-Code aufrufen kann
  • es gibt keine Datentypen in Lisp; daher keine zu überprüfenden Typen (der Typ sind die Daten selbst)
  • Obj-C hat sowohl statische als auch dynamische Deklarationen; Erstere werden zur Kompilierungszeit typisiert, letztere zur Laufzeit

Korrigiere mich, wenn ich mich in einem der oben genannten Punkte irreführe.

    
Aristides 21.08.2013, 12:16
quelle

2 Antworten

23

Beispielcode:

%Vor%

Ausführung mit einem Lisp-Interpreter

In einem Interpreter-basierten Lisp wären Lisp-Daten und der Interpreter schaut auf jedes Formular und führt den Evaluator aus. Da es die Lisp-Datenstrukturen ausführt, wird es dies jedes Mal tun, wenn es obigen Code sieht

  • Erhalte das erste Formular
  • wir haben einen Ausdruck
  • es ist ein SETQ-Spezialformular
  • werte 60 aus, das Ergebnis ist 60
  • suchen Sie den Platz für die Variable x
  • setze die Variable x auf 60
  • hol dir das nächste Formular ... ...
  • Wir haben einen Funktionsaufruf an +
  • evaluiere x - & gt; 60
  • werte y aus - & gt; 40
  • rufe die Funktion + mit 60 und 40 - & gt; 100 ...

Nun ist + ein Code, der herausfindet, was zu tun ist. Lisp hat normalerweise verschiedene Zahlentypen und (fast) kein Prozessor hat Unterstützung für all diese: fixnums, bignums, ratios, complex, float, ... Also muss die Funktion + herausfinden, welche Typen die Argumente haben und was sie können tun Sie, um sie hinzuzufügen.

Ausführung mit einem Lisp-Compiler

Ein Compiler gibt einfach Maschinencode aus, der die Operationen ausführt. Der Maschinencode erledigt alles, was der Interpreter tut: die Variablen überprüfen, die Typen prüfen, die Anzahl der Argumente überprüfen, Funktionen aufrufen , ...

Wenn Sie den Maschinencode ausführen, ist dies viel schneller, da die Lisp-Ausdrücke nicht betrachtet und interpretiert werden müssen. Der Interpreter müsste jeden Ausdruck dekodieren. Der Compiler hat das schon gemacht.

Es ist immer noch langsamer als irgendein C-Code, da der Compiler die Typen nicht unbedingt kennt und nur den völlig sicheren und flexiblen Code ausgibt.

Dieser kompilierte Lisp-Code ist also viel schneller als der Interpreter, der den ursprünglichen Lisp-Code ausführt.

Verwenden eines optimierenden Lisp-Compilers

Manchmal ist es nicht schnell genug. Dann brauchen Sie einen besseren Compiler und sagen dem Lisp-Compiler, dass er mehr Arbeit in die Kompilation einbringen und optimierten Code erstellen soll.

Der Lisp-Compiler kennt möglicherweise die Typen der Argumente und Variablen. Sie können dann den Compiler anweisen, Laufzeitprüfungen zu unterlassen. Der Compiler kann auch davon ausgehen, dass + immer dieselbe Operation ist. So kann der notwendige Code inline eingebunden werden. Da er die Typen kennt, kann er nur den Code für diese Typen generieren: Ganzzahladdition.

Aber die Semantik von Lisp unterscheidet sich immer noch von C- oder Maschinenoperationen. A + behandelt nicht nur verschiedene Zahlentypen, sondern schaltet auch automatisch von kleinen ganzen Zahlen (Fixnums) auf große ganze Zahlen (Bignums) oder Signalfehler bei Überläufen für einige Typen um. Sie können dem Compiler auch mitteilen, dies wegzulassen und nur einen nativen Ganzzahlenzusatz zu verwenden. Dann ist Ihr Code schneller - aber nicht so sicher und flexibel wie normaler Code.

Dies ist ein Beispiel für den vollständig optimierten Code, der die 64Bit LispWorks-Implementierung verwendet. Es verwendet Typdeklarationen , Inline-Deklarationen und Optimierungsdirektiven. Sie sehen, dass wir dem Compiler ein bisschen sagen müssen:

%Vor%

Der Code (64bit Intel Maschinencode) ist dann sehr klein und optimiert für das, was wir dem Compiler gesagt haben:

%Vor%

Beachten Sie jedoch, dass der obige Code etwas anderes macht als der Interpreter oder der Sicherheitscode:

  • es berechnet nur fixnums
  • es überläuft lautlos
  • Das Ergebnis ist auch eine Fixnummer
  • es macht keine Fehlerüberprüfung
  • funktioniert nicht für andere numerische Datentypen

Jetzt der nicht optimierte Code:

%Vor%

Sie können sehen, dass es eine Bibliotheksroutine aufruft, um die Addition durchzuführen. Dieser Code würde alle Interpreter tun. Aber es muss den Lisp-Quellcode nicht interpretieren. Es ist bereits zu den entsprechenden Maschinenanweisungen kompiliert.

Warum ist der kompilierte Lisp-Code schnell (er)?

Also, warum ist der kompilierte Lisp-Code schnell? Zwei Situationen:

  • nicht optimierter Lisp-Code: Das Lisp-Laufzeitsystem ist für dynamische Datenstrukturen optimiert und Code muss nicht interpretiert werden

  • optimierter Lisp-Code: Der Lisp-Compiler benötigt Informationen oder leitet sie weiter und macht viel Arbeit, um optimierten Maschinencode zu erzeugen.

Als Lisp-Programmierer möchten Sie die meiste Zeit mit nicht optimiertem, aber kompiliertem Lisp-Code arbeiten. Es ist schnell genug und bietet viel Komfort.

Verschiedene Ausführungsmodi bieten Auswahlmöglichkeiten

Als Lisp-Programmierer haben wir die Wahl:

  • interpretierter Code : langsam, aber am einfachsten zu debuggen
  • kompilierter Code : schnell zur Laufzeit, schnelle Kompilierung, viele Compiler-Prüfungen, etwas schwieriger zu debuggen, vollständig dynamisch
  • optimierter Code : sehr schnell zur Laufzeit, möglicherweise zur Laufzeit unsicher, viel Kompilierungsrauschen über verschiedene Optimierungen, langsame Kompilierung

Normalerweise optimieren wir nur diejenigen Teile des Codes, die die Geschwindigkeit benötigen.

Beachten Sie, dass es viele Situationen gibt, in denen selbst ein guter Lisp-Compiler keine Wunder vollbringen kann. Ein vollständig generisches objektorientiertes Programm (mit dem Common Lisp Object System) wird fast immer einen gewissen Overhead haben (Verteilung basierend auf Laufzeitklassen, ...).

Dynamisch typisiert und Dynamisch sind nicht identisch

Beachten Sie auch, dass dynamisch typisierte und dynamische verschiedene Eigenschaften einer Programmiersprache sind:

  • Lisp wird dynamisch typisiert , weil Typprüfungen zur Laufzeit ausgeführt werden und Variablen standardmäßig auf alle Arten von Objekten gesetzt werden können. Dazu benötigt Lisp auch Typen, die an die Datenobjekte selbst angehängt sind.

  • Lisp ist dynamisch , weil sowohl die Programmiersprache Lisp als auch das Programm selbst zur Laufzeit geändert werden können: Wir können Funktionen hinzufügen, ändern und entfernen, wir können syntaktische Konstrukte hinzufügen, ändern oder entfernen , wir können Datentypen (Datensätze, Klassen, ...) hinzufügen, ändern oder entfernen, wir können die Oberflächensyntax von Lisp auf verschiedene Arten ändern, usw. Es hilft, dass Lisp auch dynamisch typisiert wird, um einige dieser Funktionen bereitzustellen / p>

Benutzeroberfläche: Kompilieren und Disassemblieren

ANSI Common Lisp bietet

Rainer Joswig 21.08.2013, 12:50
quelle
7

Compilation ist eine einfache Übersetzung von einer Sprache in eine andere. Wenn Sie dasselbe in der Sprache A und Sprache B ausdrücken können, können Sie dieses in der Sprache A ausgedrückte Ding in die gleiche Sache in der Sprache B übersetzen.

Sobald Sie Ihre Absicht in einer Sprache ausgedrückt haben, wird sie ausgeführt, indem Sie interpretiert werden. Selbst wenn Sie C oder eine andere kompilierte Sprache verwenden, lautet Ihre Aussage:

  1. Übersetzt von C - & gt; Assemblersprache
  2. Übersetzt von Assembly - & gt; Maschinencode
  3. Von der Maschine interpretiert.

Ein Computer ist eigentlich ein Interpreter für eine sehr Grundsprache. Da es so einfach und so schwierig ist, mit ihnen zu arbeiten, haben die Leute andere Sprachen entwickelt, mit denen man leichter arbeiten kann, und sie können leicht in äquivalente Anweisungen im Maschinencode (z.B. C) übersetzt werden. Dann können Sie die Kompilierungsphase übernehmen, indem Sie die Übersetzung "on-the-fly" als JIT-Compiler ausführen oder indem Sie Ihren eigenen Interpreter schreiben, der Anweisungen direkt in Ihrer Hochsprache (zB LISP oder Python) ausführt.

Beachten Sie jedoch, dass Interpreter nur eine Abkürzung ist, um Ihren Code direkt auszuführen! Wenn der Interpreter, statt den Code auszuführen, den Aufruf ausgibt, würde er den Code ausführen, hätte er ... einen Compiler. Natürlich wäre das ein sehr dummer Compiler und würde die meisten Informationen nicht nutzen.

Tatsächliche Compiler werden versuchen, so viele Informationen wie möglich aus dem ganzen Programm zu sammeln, bevor sie den Code generieren. Zum Beispiel der folgende Code:

%Vor%

Generiert theoretisch den gesamten Code innerhalb des if -Verzweigs. Aber ein cleverer Compiler wird es wahrscheinlich als unerreichbar betrachten und es einfach ignorieren, indem es die Tatsache nutzt, dass es alles im Programm kennt und weiß, dass dowork immer false ist.

Zusätzlich dazu gibt es in einigen Sprachen Typen , die beim Senden eines Funktionsaufrufs helfen können, einige Dinge zur Kompilierzeit sicherstellen und die Übersetzung in den Maschinencode unterstützen. Einige Sprachen wie C erfordern den Programmierer, den Typ ihrer Variablen zu deklarieren. Andere wie LISP und Python leiten nur den Typ der Variablen ab, wenn sie gesetzt ist, und panisch zur Laufzeit, wenn Sie versuchen, einen Wert eines bestimmten Typs zu verwenden, wenn ein anderer Typ erforderlich ist (zB wenn Sie (car 2) in die meisten lisp-Interpreter schreiben), es wird ein Fehler auftauchen, der Ihnen sagt, dass ein Paar erwartet wird). Typen können verwendet werden, um den Speicher zur Kompilierzeit zuzuweisen (zB wird ein C-Compiler genau 10 * sizeof(int) Speicherbytes zuweisen, wenn es erforderlich ist, int[10] zuzuweisen), aber dies ist nicht genau erforderlich . Tatsächlich verwenden die meisten C-Programme Zeiger zum Speichern von Arrays, die im Grunde dynamisch sind. Wenn ein Pointer mit einem Pointer arbeitet, wird ein Compiler einen Code generieren / verlinken, der zur Laufzeit die notwendigen Überprüfungen, Neuzuweisungen usw. durchführt. Aber unter dem Strich ist zu verstehen, dass dynamic und compiled nicht entgegengesetzt sind. Die Python- oder Lisp-Interpreter sind kompilierte Programme, können jedoch weiterhin dynamische Werte verarbeiten. Tatsächlich ist die Assemblersprache selbst nicht wirklich getippt, da der Computer irgendeine Operation an irgendeinem Objekt ausführen kann, da alles, was er sieht, Bitströme und Operationen auf Bits sind. Höhere Sprachen führen willkürliche Typen und Grenzen ein, um die Dinge lesbarer zu machen und zu verhindern, dass man völlig verrückte Sachen macht. Aber das ist nur um Ihnen zu helfen , keine absolute Notwendigkeit.

Nun, da die philosophische Tirade vorbei ist, schauen wir uns Ihr Beispiel an:

%Vor%

Und versuchen wir, das zu einem gültigen C-Programm zu kompilieren. Sobald das erledigt ist, gibt es C-Compiler im Überfluss, so dass wir LISP übersetzen können - & gt; C - & gt; Maschinensprache oder so ziemlich alles andere. Denken Sie daran, dass die Kompilierung nur eine Übersetzung ist (Optimierungen sind auch cool, aber optional).

%Vor%

Dies weist einen Wert zu. Aber wir wissen nicht, was was zugeteilt ist. Lass uns weitermachen

%Vor%

Ok, wir ordnen 60 zu x zu. 60 ist ein Integer-Literal, also ist der C-Typ int . Da es keinen Grund gibt anzunehmen, dass x von einem anderen Typ ist, entspricht dies dem C:

%Vor%

Ähnliches gilt für (setq y 40) :

%Vor%

Jetzt haben wir:

%Vor%

+ ist eine Funktion, die je nach Implementierung mehrere Arten von Argumenten annehmen kann, aber wir wissen, dass x und y ganze Zahlen sind. Unsere Compiler wissen, dass es eine äquivalente C-Anweisung gibt, nämlich:

%Vor%

Also übersetzen wir es einfach. Unser abschließendes C-Programm:

%Vor%

Welches ist ein perfekt gültiges C-Programm? Es kann schwieriger werden als das. Zum Beispiel, wenn x und y sehr groß sind, lassen die meisten LISP sie nicht überlaufen, während C wird, also könnten Sie Ihren Compiler so programmieren, dass er seinen eigenen Integertyp als Array von Ints hat (oder was auch immer Sie für relevant halten) .Wenn Sie in der Lage sind, allgemeine Operationen (wie + ) für diese Typen zu definieren, wird Ihr neuer Compiler möglicherweise den vorherigen Code stattdessen in diesen übersetzen:

%Vor%

Mit Ihren Funktionen newbigint und addbigints an anderer Stelle definiert oder vom Compiler generiert. Es ist immer noch gültig C, also wird es kompilieren. Tatsächlich ist Ihr eigener Interpreter wahrscheinlich in einer untergeordneten Sprache implementiert und hat bereits Repräsentationen für LISP-Objekte in einer eigenen Implementierung, so dass er diese direkt verwenden kann.

Das ist übrigens genau das, was der Cython -Compiler für Python-Code macht:)

Sie können Typen in Cython statisch definieren, um zusätzliche Geschwindigkeit / Optimierungen zu erhalten. Dies ist jedoch nicht erforderlich. Cython kann Ihren Python-Code direkt in C und dann in Maschinencode übersetzen.

Ich hoffe, es macht es klarer! Denken Sie daran:

  1. ALL-Code wird interpretiert, eventuell
  2. Compiler übersetzen Code in etwas, das einfacher / schneller zu interpretieren ist. Sie führen oft Optimierungen durch, aber dies ist nicht Teil der Definition
val 21.08.2013 13:09
quelle