Perché la JVM continua a non supportare l'ottimizzazione della chiamata finale?


95

Due anni dopo le ottimizzazioni-jvm-prevent-tail-call , sembra esserci un'implementazione del prototipo e MLVM ha elencato la funzionalità come "proto 80%" da qualche tempo.

Non c'è alcun interesse attivo da parte di Sun / Oracle nel supportare le tail call o è solo che le tail call sono "[...] destinate a venire al secondo posto in ogni lista di priorità delle funzionalità [...]" come menzionato alla JVM Summit linguistico ?

Sarei davvero interessato se qualcuno avesse testato una build MLVM e potesse condividere alcune impressioni su come funziona (se non del tutto).

Aggiornamento: si noti che alcune VM come Avian supportano chiamate di coda appropriate senza problemi.


14
Con l'esodo riferito di persone del Sole da Oracle, non mi aspetterei che nessuno dei progetti in corso continui a meno che non lo dicesse esplicitamente da Oracle :(
Thorbjørn Ravn Andersen

16
Nota che la tua risposta accettata è completamente sbagliata. Non vi è alcun conflitto fondamentale tra l'ottimizzazione della chiamata di coda e l'OOP e, naturalmente, diversi linguaggi come OCaml e F # hanno sia OOP che TCO.
JD

2
Beh, chiamare i linguaggi OCaml e F # OOP è un brutto scherzo in primo luogo. Ma sì, OOP e TCO non hanno molto in comune, tranne il fatto che il runtime deve controllare che il metodo da ottimizzare non sia sovrascritto / sottoclasse da qualche altra parte.
soc

5
+1 Provenendo da un background C, ho sempre pensato che il TCO fosse un dato di fatto in qualsiasi JVM moderna. Non mi è mai venuto in mente di controllare effettivamente e quando l'ho fatto i risultati sono stati sorprendenti ...
thkala

2
@soc: "tranne il fatto che il runtime deve controllare che il metodo da ottimizzare non sia sovrascritto / sottoclasse da qualche altra parte". Il tuo "fatto" è una totale assurdità.
JD

Risposte:


32

Diagnosi del codice Java: Migliorare le prestazioni del codice Java ( alt ) spiega perché JVM non supporta l'ottimizzazione della chiamata finale.

Ma sebbene sia ben noto come trasformare automaticamente una funzione ricorsiva di coda in un semplice ciclo, la specifica Java non richiede che questa trasformazione venga eseguita. Presumibilmente, una delle ragioni per cui non è un requisito è che, in generale, la trasformazione non può essere eseguita staticamente in un linguaggio orientato agli oggetti. Invece, la trasformazione da funzione ricorsiva di coda a ciclo semplice deve essere eseguita dinamicamente da un compilatore JIT.

Quindi fornisce un esempio di codice Java che non si trasformerà.

Quindi, come mostra l'esempio nel Listato 3, non possiamo aspettarci che i compilatori statici eseguano la trasformazione della ricorsione in coda sul codice Java preservando la semantica del linguaggio. Invece, dobbiamo fare affidamento sulla compilazione dinamica da parte del JIT. A seconda della JVM, il JIT può farlo o meno.

Quindi fornisce un test che puoi usare per capire se il tuo JIT lo fa.

Naturalmente, trattandosi di un documento IBM, include un plug:

Ho eseguito questo programma con un paio di Java SDK ei risultati sono stati sorprendenti. L'esecuzione su Sun's Hotspot JVM per la versione 1.3 rivela che Hotspot non esegue la trasformazione. Con le impostazioni predefinite, lo spazio dello stack si esaurisce in meno di un secondo sulla mia macchina. D'altra parte, la JVM di IBM per la versione 1.3 fa le fusa senza problemi, indicando che trasforma il codice in questo modo.


62
FWIW, le chiamate di coda non riguardano solo funzioni auto-ricorsive come implica. Le chiamate di coda sono tutte le chiamate di funzione che appaiono in posizione di coda. Non devono essere chiamate a se stessi e non devono essere chiamate a posizioni note staticamente (ad esempio, possono essere chiamate a metodi virtuali). Il problema che descrive è un non-problema se l'ottimizzazione della chiamata di coda viene eseguita correttamente nel caso generale e, di conseguenza, il suo esempio funziona perfettamente nei linguaggi orientati agli oggetti che supportano le chiamate di coda (ad esempio OCaml e F #).
JD

3
"deve essere fatto dinamicamente da un compilatore JIT", il che significa che deve essere fatto dalla stessa JVM piuttosto che dal compilatore Java. Ma l'OP chiede della JVM.
Raedwald

11
"in generale, la trasformazione non può essere eseguita staticamente in un linguaggio orientato agli oggetti." Questa è una citazione ovviamente, ma ogni volta che vedo una scusa del genere vorrei chiedere dei numeri - perché non sarei sorpreso se in pratica nella maggior parte dei casi potesse essere stabilito in fase di compilazione.
greenoldman

5
Il collegamento all'articolo citato è ora interrotto, sebbene Google lo abbia memorizzato nella cache. Ancora più importante, il ragionamento dell'autore è difettoso. L'esempio fornito potrebbe essere ottimizzato per le chiamate di coda, utilizzando la compilazione statica e non solo dinamica, se solo il compilatore inserisse un instanceofcontrollo per vedere se thisè un Exampleoggetto (piuttosto che una sottoclasse di Example).
Alex D


30

Uno dei motivi che ho visto in passato per non implementare il TCO (ed è considerato difficile) in Java è che il modello di autorizzazione nella JVM è sensibile allo stack e quindi le chiamate in coda devono gestire gli aspetti di sicurezza.

Credo che Clements e Felleisen [1] [2] abbiano dimostrato che questo non è un ostacolo e sono abbastanza sicuro che anche la patch MLVM menzionata nella domanda lo affronti.

Mi rendo conto che questo non risponde alla tua domanda; aggiungendo solo informazioni interessanti.

  1. http://www.ccs.neu.edu/scheme/pubs/esop2003-cf.pdf
  2. http://www.ccs.neu.edu/scheme/pubs/cf-toplas04.pdf

1
+1. Ascolta le domande / risposte alla fine di questa presentazione di Brian Goetz youtube.com/watch?v=2y5Pv4yN0b0&t=3739
mcoolive

15

Forse lo sai già, ma la funzionalità non è così banale come potrebbe sembrare poiché il linguaggio Java espone effettivamente la traccia dello stack al programmatore.

Considera il seguente programma:

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());
    }
}

Anche se ha un "richiamo di coda", potrebbe non essere ottimizzato. (Se è ottimizzato, richiede comunque la contabilità dell'intero stack di chiamate poiché la semantica del programma si basa su di esso.)

Fondamentalmente, questo significa che è difficile supportarlo pur essendo compatibile con le versioni precedenti.


17
Hai trovato l'errore nel tuo pensiero: "richiede la contabilità dell'intero stack di chiamate poiché la semantica del programma si basa su di esso". :-) È come le nuove "eccezioni soppresse". I programmi che si basano su queste cose sono destinati a fallire. A mio parere il comportamento del programma è assolutamente corretto: buttare via gli stack frame è l'essenza delle chiamate tail.
soc

4
@ Marco, ma quasi tutti i metodi potrebbero generare un'eccezione, dalla quale è destinato a essere disponibile l'intero stack di chiamate, giusto? Inoltre, non puoi decidere in anticipo quali metodi chiameranno indirettamente gin questo caso ... pensa ad esempio al polimorfismo e alla riflessione.
aioobe

2
È un effetto collaterale causato dall'aggiunta di ARM in Java 7. È un esempio che non puoi fare affidamento su queste cose che hai mostrato sopra.
soc

6
"il fatto che il linguaggio esponga lo stack di chiamate rende difficile implementarlo": il linguaggio richiede che lo stack-trace restituito da getStackTrace()un metodo x()che il codice sorgente mostra sia chiamato da un metodo y()mostra anche che è x()stato chiamato da y()? Perché se c'è un po 'di libertà non c'è un vero problema.
Raedwald

8
Questa è semplicemente una questione di formulare le specifiche di un singolo metodo, da "ti dà tutti gli stack frame" a "ti dà tutti gli stackframe attivi, tralasciando quelli obsoleti dalle chiamate tail". Inoltre, si potrebbe renderlo un interruttore della riga di comando o una proprietà di sistema se la chiamata di coda è rispettata.
Ingo

12

Java è il linguaggio meno funzionale che potresti immaginare (beh, OK, forse no !) Ma questo sarebbe un grande vantaggio per i linguaggi JVM, come Scala , che lo sono.

Le mie osservazioni sono che rendere la JVM una piattaforma per altri linguaggi non è mai sembrato essere in cima alla lista delle priorità per Sun e immagino, ora per Oracle.


16
@ Thorbjørn - Ho scritto un programma per prevedere se un determinato programma si sarebbe fermato in un periodo di tempo limitato. Mi ci sono voluti anni !
oxbow_lakes

3
I primi BASIC che ho usato non avevano funzioni, ma piuttosto GOSUB e RETURN. Non penso nemmeno che LOLCODE sia molto funzionale (e puoi prenderlo in due sensi).
David Thornley

1
@David, funzionale! = Ha funzioni.
Thorbjørn Ravn Andersen

2
@ Thorbjørn Ravn Andersen: No, ma è una specie di prerequisito, non credi?
David Thornley

3
"rendere la JVM una piattaforma per altri linguaggi non è mai sembrato essere in cima alla lista delle priorità per Sun". Hanno fatto molti più sforzi per rendere la JVM una piattaforma per linguaggi dinamici rispetto a quanto hanno fatto per i linguaggi funzionali.
JD
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.