Pourquoi la JVM ne prend toujours pas en charge l'optimisation des appels de fin?

95

Deux ans après les optimisations de do -the-jvm-prevent-tail-call , il semble y avoir une implémentation prototype et MLVM a répertorié la fonctionnalité comme "proto 80%" depuis un certain temps maintenant.

Y a-t-il aucun intérêt actif de la part de Sun / Oracle pour la prise en charge des appels de queue ou est-ce simplement que les appels de queue sont "[...] voués à venir en deuxième place sur chaque liste de priorités de fonctionnalités [...]" comme mentionné à la JVM Sommet de la langue ?

Je serais vraiment intéressé si quelqu'un a testé une version MLVM et pourrait partager quelques impressions sur son fonctionnement (voire pas du tout).

Mise à jour: notez que certaines VM comme Avian prennent en charge les appels de queue appropriés sans aucun problème.

soc
la source
14
Avec l'exode signalé des Sun personnes d'Oracle, je ne m'attendrais pas à ce qu'aucun des projets en cours se poursuive à moins que cela ne soit explicitement dit d'Oracle :(
Thorbjørn Ravn Andersen
16
Notez que votre réponse acceptée est complètement fausse. Il n'y a pas de conflit fondamental entre l'optimisation des appels de queue et la POO et, bien sûr, plusieurs langages comme OCaml et F # ont à la fois la POO et le TCO.
JD
2
Eh bien, appeler les langages OCaml et F # OOP est une mauvaise blague en premier lieu. Mais oui, la POO et le TCO n'ont pas grand-chose en commun, sauf le fait que le runtime doit vérifier que la méthode optimisée n'est pas remplacée / sous-classée ailleurs.
soc
5
+1 Venant d'un fond C, j'ai toujours supposé que le TCO était une donnée dans toute JVM moderne. Il ne m'est jamais venu à l'
esprit de
2
@soc: "sauf le fait que le runtime doit vérifier que la méthode optimisée n'est pas surchargée / sous-classée ailleurs". Votre «fait» est un non-sens complet.
JD

Réponses:

32

Diagnostiquer le code Java: Améliorer les performances de votre code Java ( alt ) explique pourquoi la JVM ne prend pas en charge l'optimisation des appels de fin.

Mais bien qu'il soit bien connu de transformer automatiquement une fonction récursive de queue en une simple boucle, la spécification Java n'exige pas que cette transformation soit effectuée. Vraisemblablement, une des raisons pour lesquelles ce n'est pas une exigence est que, en général, la transformation ne peut pas être effectuée de manière statique dans un langage orienté objet. Au lieu de cela, la transformation de la fonction récursive de queue en boucle simple doit être effectuée dynamiquement par un compilateur JIT.

Il donne ensuite un exemple de code Java qui ne se transformera pas.

Ainsi, comme le montre l'exemple du Listing 3, nous ne pouvons pas nous attendre à ce que des compilateurs statiques effectuent une transformation de la récursion de queue sur le code Java tout en préservant la sémantique du langage. Au lieu de cela, nous devons nous fier à la compilation dynamique par le JIT. En fonction de la JVM, le JIT peut le faire ou non.

Ensuite, il donne un test que vous pouvez utiliser pour déterminer si votre JIT fait cela.

Naturellement, puisqu'il s'agit d'un article IBM, il comprend un plug:

J'ai exécuté ce programme avec quelques SDK Java et les résultats ont été surprenants. L'exécution sur la JVM Hotspot de Sun pour la version 1.3 révèle que Hotspot n'effectue pas la transformation. Aux paramètres par défaut, l'espace de pile est épuisé en moins d'une seconde sur ma machine. D'autre part, la JVM d'IBM pour la version 1.3 ronronne sans problème, indiquant qu'elle transforme le code de cette manière.

émory
la source
62
FWIW, les appels de queue ne sont pas seulement des fonctions auto-récursives comme il l'implique. Les appels de queue sont tous les appels de fonction qui apparaissent en position de queue. Ils ne doivent pas nécessairement être des appels à soi et ils ne doivent pas nécessairement être des appels à des emplacements statiquement connus (par exemple, ils peuvent être des appels de méthode virtuelle). Le problème qu'il décrit n'est pas un problème si l'optimisation des appels de queue est effectuée correctement dans le cas général et, par conséquent, son exemple fonctionne parfaitement dans les langages orientés objet qui prennent en charge les appels de queue (par exemple OCaml et F #).
JD
3
"doit être fait dynamiquement par un compilateur JIT" ce qui signifie qu'il doit être fait par la JVM elle-même plutôt que par le compilateur Java. Mais l'OP pose des questions sur la JVM.
Raedwald
11
"en général, la transformation ne peut pas être effectuée de manière statique dans un langage orienté objet." Ceci est une citation bien sûr, mais chaque fois que je vois une telle excuse, je voudrais poser des questions sur les chiffres - car je ne serais pas surpris si dans la pratique, dans la majorité des cas, il pouvait être établi au moment de la compilation.
greenoldman
5
Le lien vers l'article cité est maintenant rompu, bien que Google l'ait mis en cache. Plus important encore, le raisonnement de l'auteur est erroné. L'exemple donné pourrait être optimisé pour les appels de fin, en utilisant une compilation statique et pas seulement dynamique, si seul le compilateur insérait une instanceofvérification pour voir si thisest un Exampleobjet (plutôt qu'une sous-classe de Example).
Alex D
4
juste un lien vers webarchive web.archive.org/web/20120506085636/http://www.ibm.com/…
Suvitruf - Andrei Apanasik
30

Une raison que j'ai vue dans le passé pour ne pas implémenter le TCO (et cela est considéré comme difficile) en Java est que le modèle d'autorisation dans la JVM est sensible à la pile et que les appels finaux doivent donc gérer les aspects de sécurité.

Je crois que cela a été montré comme n'étant pas un obstacle par Clements et Felleisen [1] [2] et je suis à peu près sûr que le patch MLVM mentionné dans la question le traite également.

Je me rends compte que cela ne répond pas à votre question; juste en ajoutant des informations intéressantes.

  1. http://www.ccs.neu.edu/scheme/pubs/esop2003-cf.pdf
  2. http://www.ccs.neu.edu/scheme/pubs/cf-toplas04.pdf
Alex Miller
la source
1
+1. Écoutez les questions / réponses à la fin de cette présentation de Brian Goetz youtube.com/watch?v=2y5Pv4yN0b0&t=3739
mcoolive
15

Vous le savez peut-être déjà, mais la fonctionnalité n'est pas aussi triviale que cela puisse paraître puisque le langage Java expose en fait la trace de la pile au programmeur.

Considérez le programme suivant:

public class Test {

    public static String f() {
        String s = Math.random() > .5 ? f() : g();
        return s;
    }

    public static String g() {
        if (Math.random() > .9) {
            StackTraceElement[] ste = new Throwable().getStackTrace();
            return ste[ste.length / 2].getMethodName();
        }
        return f();
    }

    public static void main(String[] args) {
        System.out.println(f());
    }
}

Même si cela a un "appel de queue", il peut ne pas être optimisé. (S'il est optimisé, il nécessite toujours la comptabilité de l'ensemble de la pile d'appels puisque la sémantique du programme en dépend.)

Fondamentalement, cela signifie qu'il est difficile de prendre en charge cela tout en étant compatible avec les versions antérieures.

aioobe
la source
17
Vous avez trouvé l'erreur dans votre pensée: "nécessite la comptabilité de toute la pile d'appels puisque la sémantique du programme en dépend". :-) C'est comme les nouvelles "exceptions supprimées". Les programmes reposant sur de telles choses sont voués à l'échec. À mon avis, le comportement du programme est tout à fait correct: jeter les cadres de pile est la raison d'être des appels de queue.
soc
4
@Marco, mais à peu près n'importe quelle méthode pourrait lever une exception, à partir de laquelle toute la pile d'appels est forcément disponible, non? De plus, vous ne pouvez pas décider à l'avance quelles méthodes appelleront indirectement gdans ce cas ... pensez au polymorphisme et à la réflexion par exemple.
aioobe
2
C'est un effet secondaire causé par l'ajout d'ARM dans Java 7. C'est un exemple que vous ne pouvez pas vous fier aux choses que vous avez montrées ci-dessus.
soc
6
"le fait que le langage expose la pile d'appels rend difficile l'implémentation de ceci": le langage exige-t-il que la trace de pile retournée par getStackTrace()une méthode x()que le code source montre est appelée à partir d'une méthode y()montre aussi qu'elle a x()été appelée y()? Parce que s'il y a une certaine liberté, il n'y a pas de véritable problème.
Raedwald
8
Il s'agit simplement de formuler la spécification d'une méthode unique, de "vous donne tous les cadres de pile" à "vous donne tous les cadres de pile actifs, en laissant de côté ceux qui sont obsolètes par les appels de queue". De plus, on pourrait en faire un commutateur de ligne de commande ou une propriété système si l'appel final est respecté.
Ingo
12

Java est le langage le moins fonctionnel que vous puissiez imaginer (enfin, d'accord, peut - être pas !) Mais ce serait un grand avantage pour les langages JVM, comme Scala , qui le sont.

Mes observations sont que faire de la JVM une plate-forme pour d'autres langues n'a jamais semblé être en haut de la liste des priorités pour Sun et je suppose, maintenant pour Oracle.

oxbow_lakes
la source
16
@ Thorbjørn - J'ai écrit un programme pour prédire si un programme donné s'arrêterait dans un laps de temps limité. Cela m'a pris des siècles !
oxbow_lakes
3
Les premiers BASIC que j'ai utilisés n'avaient pas de fonctions, mais plutôt GOSUB et RETURN. Je ne pense pas non plus que LOLCODE soit très fonctionnel (et vous pouvez prendre cela dans deux sens).
David Thornley
1
@David, fonctionnel! = A des fonctions.
Thorbjørn Ravn Andersen
2
@ Thorbjørn Ravn Andersen: Non, mais c'est une sorte de condition préalable, n'est-ce pas?
David Thornley
3
"faire de la JVM une plate-forme pour d'autres langues n'a jamais semblé être en tête de liste des priorités pour Sun". Ils ont déployé beaucoup plus d'efforts pour faire de la JVM une plate-forme pour les langages dynamiques que pour les langages fonctionnels.
JD