Clojure führt die Tail-Call-Optimierung nicht alleine durch: Wenn Sie eine Tail-Recursive-Funktion haben und diese optimieren möchten, müssen Sie das spezielle Formular verwenden recur
. Wenn Sie zwei sich gegenseitig rekursive Funktionen haben, können Sie diese ebenfalls nur mithilfe von optimieren trampoline
.
Der Scala-Compiler kann TCO für eine rekursive Funktion ausführen, jedoch nicht für zwei sich gegenseitig rekursive Funktionen.
Wann immer ich über diese Einschränkungen gelesen habe, wurden sie immer einer Einschränkung zugeschrieben, die dem JVM-Modell eigen ist. Ich weiß so ziemlich nichts über Compiler, aber das verwirrt mich ein bisschen. Lassen Sie mich das Beispiel nehmen Programming Scala
. Hier die Funktion
def approximate(guess: Double): Double =
if (isGoodEnough(guess)) guess
else approximate(improve(guess))
wird übersetzt in
0: aload_0
1: astore_3
2: aload_0
3: dload_1
4: invokevirtual #24; //Method isGoodEnough:(D)Z
7: ifeq
10: dload_1
11: dreturn
12: aload_0
13: dload_1
14: invokevirtual #27; //Method improve:(D)D
17: dstore_1
18: goto 2
Auf Bytecode-Ebene braucht man also nur goto
. In diesem Fall wird die harte Arbeit vom Compiler erledigt.
Welche Einrichtung der zugrunde liegenden virtuellen Maschine würde es dem Compiler ermöglichen, die Gesamtbetriebskosten einfacher zu handhaben?
Als Randnotiz würde ich nicht erwarten, dass tatsächliche Maschinen viel schlauer sind als die JVM. Trotzdem scheinen viele Sprachen, die mit nativem Code kompiliert werden, wie z. B. Haskell, keine Probleme mit der Optimierung von Tail Calls zu haben (Haskell kann manchmal aufgrund von Faulheit Probleme haben, aber das ist ein anderes Problem).
tailcall
Anweisung für JVM wurde bereits 2007 vorgeschlagen: Blog auf sun.com über die Wayback-Maschine . Nach der Übernahme von Oracle ist dieser Link 404's. Ich schätze, es hat es nicht in die Prioritätenliste von JVM 7 geschafft.tailcall
Anweisung würde einen Tail Call nur als Tail Call kennzeichnen. Ob die JVM diesen Tail Call dann tatsächlich optimiert hat, ist eine ganz andere Frage. Die CLI-CIL hat ein.tail
Anweisungspräfix, die Microsoft 64-Bit-CLR hat es jedoch lange Zeit nicht optimiert. OTOH, die IBM J9 JVM erkennt Tail Calls und optimiert sie, ohne dass eine spezielle Anweisung erforderlich ist, um festzustellen, welche Calls Tail Calls sind. Das Kommentieren von Tail Calls und das Optimieren von Tail Calls sind wirklich orthogonal. (Abgesehen von der Tatsache, dass das statische Ableiten, welcher Anruf ein Tail-Anruf ist, möglicherweise nicht entscheidbar ist oder auch nicht. Keine Ahnung.)call something; oreturn
. Die Hauptaufgabe eines JVM-Spezifikationsupdates besteht darin, keine explizite Anweisung für den Endanruf einzuführen, sondern die Optimierung einer solchen Anweisung anzuweisen. Eine solche Anweisung erleichtert nur die Arbeit der Compilerautoren: Der JVM-Autor muss diese Anweisungssequenz nicht unbedingt erkennen, bevor sie unkenntlich gemacht wird, und der X-> Bytecode-Compiler kann sicher sein, dass ihr Bytecode entweder ungültig oder ungültig ist eigentlich optimiert, nie richtig, aber Stapelüberlauf.call something; return;
würde nur dann einem Tail-Aufruf entsprechen, wenn das Ding, das aufgerufen wird, niemals nach einem Stack-Trace fragt. Wenn die betreffende Methode virtuell ist oder eine virtuelle Methode aufruft, kann die JVM nicht wissen, ob sie nach dem Stapel fragen kann.Clojure könnte die automatische Optimierung der Schwanzrekursion in Schleifen durchführen. Wie Scala beweist, ist dies auf der JVM durchaus möglich.
Es war eigentlich eine Designentscheidung, dies nicht zu tun - Sie müssen das
recur
spezielle Formular explizit verwenden , wenn Sie diese Funktion wünschen. Siehe den Mail-Thread Betreff: Warum keine Tail-Call-Optimierung in der Google-Gruppe von Clojure.In der aktuellen JVM ist die Optimierung des Tail Calls zwischen verschiedenen Funktionen (gegenseitige Rekursion) nur unmöglich. Die Implementierung ist nicht besonders kompliziert (andere Sprachen wie Scheme hatten diese Funktion von Anfang an), erfordert jedoch Änderungen an der JVM-Spezifikation. Beispielsweise müssten Sie die Regeln zum Beibehalten des gesamten Funktionsaufrufstapels ändern.
Eine zukünftige Iteration der JVM wird wahrscheinlich diese Funktion erhalten, möglicherweise jedoch als Option, damit das abwärtskompatible Verhalten für alten Code beibehalten wird. Sagen wir , Features Preview bei Geeknizer listet dies für Java 9 auf:
Natürlich können sich zukünftige Roadmaps jederzeit ändern.
Wie sich herausstellt, ist es sowieso keine so große Sache. In über 2 Jahren der Codierung von Clojure bin ich nie auf eine Situation gestoßen, in der der Mangel an TCO ein Problem war. Die Hauptgründe dafür sind:
recur
eine schnelle Schwanzrekursion mit oder einer Schleife erhalten. Der Fall der gegenseitigen Schwanzrekursion ist im normalen Code ziemlich seltenquelle
Es geht nicht darum, schlauer zu sein, sondern anders zu sein. Bis vor kurzem wurde die JVM ausschließlich für eine einzige Sprache (Java, offensichtlich) entwickelt und optimiert, die über sehr strenge Speicher- und Aufrufmodelle verfügt.
Es gab nicht nur keine
goto
oder Zeiger, es gab auch keine Möglichkeit, eine "nackte" Funktion aufzurufen (eine, die keine in einer Klasse definierte Methode war).Wenn ein Compiler auf JVM abzielt, muss er sich konzeptionell die Frage stellen, wie dieses Konzept in Java ausgedrückt werden kann. Und offensichtlich gibt es keine Möglichkeit, die Gesamtbetriebskosten in Java auszudrücken.
Beachten Sie, dass diese nicht als Fehler von JVM angesehen werden, da sie für Java nicht benötigt werden. Sobald Java ein solches Feature benötigt, wird es zu JVM hinzugefügt.
Erst vor kurzem haben die Java-Behörden begonnen, JVM als Plattform für Nicht-Java-Sprachen ernst zu nehmen. Daher wurden bereits einige Funktionen unterstützt, die kein Java-Äquivalent aufweisen. Am bekanntesten ist die dynamische Typisierung, die bereits in JVM, jedoch nicht in Java vorhanden ist.
quelle
Haben Sie bemerkt, dass die Methodenadresse mit 0 beginnt? Dass alle Methoden von Sets mit 0 beginnen? JVM erlaubt es keinem, außerhalb einer Methode zu springen.
Ich habe keine Ahnung, was mit einem Zweig passieren würde, dessen Offset außerhalb der Methode von Java geladen wurde. Vielleicht würde er vom Bytecode-Verifizierer abgefangen, vielleicht würde er eine Ausnahme erzeugen und vielleicht tatsächlich außerhalb der Methode springen.
Das Problem ist natürlich, dass Sie nicht wirklich garantieren können, wo sich andere Methoden derselben Klasse befinden, geschweige denn Methoden anderer Klassen. Ich bezweifle, dass JVM irgendwelche Garantien darüber gibt, wo die Methoden geladen werden, obwohl ich gerne korrigiert werden würde.
quelle