2009-05-13 8 views
50

Dopo un po 'di esperienza con i linguaggi funzionali, sto iniziando a utilizzare la ricorsione in Java - Ma il linguaggio sembra avere una quantità di chiamate relativamente bassa di circa 1000.Lo stack trabocca dalla profonda ricorsione in Java?

C'è un modo per aumentare lo stack delle chiamate? Come posso creare funzioni che sono milioni di chiamate profonde, come in Erlang?

Sto notando questo sempre di più quando faccio problemi di Project Euler.

Grazie.

risposta

40

Credo che si potrebbe utilizzare questi parametri

-ss stacksize per aumentare la dimensione di stack nativo o

-oss stacksize per aumentare la dimensione dello stack Java ,

Il nativo di default la dimensione dello stack è 128k, con un valore minimo di 1000 byte. La dimensione java stack predefinita è 400k, con un valore minimo di 1000 byte.

http://edocs.bea.com/wls/docs61/faq/java.html#251197

EDIT:

Dopo aver letto il primo commento (Chuck's), così come ri lettura della domanda e la lettura di un altro risposte, i'd teniamo a precisare che ho interpretato il domanda come "aumentare le dimensioni dello stack". Non intendevo dire che puoi avere stack infiniti, come nella programmazione funzionale (un paradigma di programmazione che ho solo graffiato la sua superficie).

+6

Questo può darti più livelli, ma le dimensioni dello stack sono ancora limitate. Non sarai in grado di recitare all'infinito come in un linguaggio funzionale con l'eliminazione chiamata alta. – Chuck

+0

non proprio una risposta ... soluzione di aiuto per banda –

+1

Il collegamento è interrotto. – free6om

8

La maggior parte delle lingue funzionali supporta la ricorsione della coda. Tuttavia, la maggior parte dei compilatori Java non supporta questo. Invece fa un'altra chiamata di funzione. Ciò significa che ci sarà sempre un limite superiore al numero di chiamate ricorsive che puoi effettuare (dato che alla fine esaurirai lo spazio di stack).

Con ricorsione di coda riutilizzate il frame di stack della funzione che si sta verificando, in modo da non avere gli stessi vincoli sullo stack.

23

Spetta alla JVM decidere se ricorrere o meno alla ricorsione in coda - Non so se qualcuno di loro lo sa, ma non dovresti fare affidamento su di esso. In particolare, la modifica delle dimensioni dello stack sarebbe molto raramente la cosa giusta da fare, a meno che non si abbia un limite rigido di quanti livelli di ricorsione si utilizzeranno effettivamente e si sapeva esattamente quanto spazio di stack occuperebbe ciascuno. Molto fragile

Fondamentalmente, non si dovrebbe ricorrere alla ricorsione illimitata in una lingua che non è costruita per esso. Dovrai invece usare l'iterazione, temo. E sì, che può essere un leggero dolore a volte :(

+0

So che la JVM di Sun non ottimizza la ricorsione della coda, e non credo che nessuna delle altre JVM principali faccia altrettanto. Potrebbero esserci uno o due tentativi sperimentali. –

+1

I ** pensa ** Potenziale di IBM. Ho sentito quella seconda o terza mano, quindi, quindi non citatemi sopra: P –

+2

L'ottimizzazione del lavoro su coda è in corso, ma non è attualmente supportata in Java perché rompe alcune aspettative su come appare lo stack, che sono importanti per il modello di sicurezza di Java (e cose meno importanti, come le tracce dello stack). http://blogs.sun.com/jrose/entry/tail_calls_in_the_vm – erickson

9

If you have to ask, you're probably doing something wrong.

Ora, mentre si può probabilmente trovare un modo per aumentare la pila di default in java, vorrei solo aggiungere i miei 2 centesimi in che davvero è necessario trovare un altro modo per fare ciò che si vuole fare, invece di fare affidamento su uno stack maggiore.

Poiché le specifiche java non rendono obbligatorio per JVM implementare tecniche di ottimizzazione della ricorsione in coda, l'unico modo per aggirare il problema è ridurre la pressione dello stack, riducendo il numero di variabili/parametri locali che deve essere tenuto traccia di, o idealmente, semplicemente riducendo il livello di ricorsione in modo significativo, o semplicemente riscrivendo senza ricorsione.

+1

Perché è il controllo per vedere se una lingua supporta l'eliminazione di tail-call "errata"? – Arafangion

+1

Non lo è, ma Java non lo impone, quindi non puoi fare affidamento su di esso. La situazione sarebbe diversa se Java ottimizzasse l'ottimizzazione della ricorsione in coda, quindi direi semplicemente che dovresti provare a ristrutturare la ricorsione per trarne sempre vantaggio. Dal momento che non lo fa, non l'ho fatto. –

+0

Attenzione a sottolineare come è sbagliato? Nota che ho detto che erano i miei 2 centesimi, in altre parole, un'opinione. Sei libero di non essere d'accordo, ma per dire che è sbagliato, allora devi davvero dare maggiori dettagli sul motivo per cui pensi che sia sbagliato. Altri commenti qui hanno fornito maggiori informazioni sul perché JVM non implementa la ricorsione tail-call. –

7

È possibile impostare questo sulla riga di comando:

java -Xss8M classe

6

Clojure, che gira su Java VM, sarebbe molto simile a implementare l'ottimizzazione chiamata coda, ma non può a causa di una restrizione nel bytecode JVM (non conosco i dettagli). Di conseguenza, può solo aiutare se stesso con una speciale forma "recidiva", che implementa alcune caratteristiche di base che ti aspetteresti da una corretta ricorsione della coda.

In ogni caso, ciò significa che la JVM al momento non può l'ottimizzazione di chiamata coda di supporto. Suggerisco vivamente di non utilizzare la ricorsione come costrutto di loop generale sulla JVM. La mia opinione personale è che Java non è un linguaggio sufficientemente avanzato.

85

L'aumento delle dimensioni dello stack servirà solo come benda temporanea. Come altri hanno sottolineato, ciò che si vuole veramente è l'eliminazione della coda, e Java non ha questo per vari motivi. Tuttavia, puoi imbrogliare se vuoi.

Pillola rossa in mano? OK, in questo modo per favore.

Ci sono modi in cui è possibile scambiare lo stack per l'heap. Ad esempio, invece di effettuare una chiamata ricorsiva all'interno di una funzione, è necessario restituire una infrastruttura lazy che effettua la chiamata quando viene valutata. È quindi possibile scaricare lo "stack" con for-construct di Java. Dimostrerò con un esempio. Considera questo codice Haskell:

map :: (a -> b) -> [a] -> [b] 
map _ [] = [] 
map f (x:xs) = (f x) : map f xs 

Si noti che questa funzione non valuta mai la coda dell'elenco. Quindi la funzione in realtà non ha bisogno di effettuare una chiamata ricorsiva. In Haskell, in realtà restituisce un thunk per la coda, che viene chiamato se è mai necessario. Siamo in grado di fare la stessa cosa in Java (questo utilizza le classi da Functional Java):

public <B> Stream<B> map(final F<A, B> f, final Stream<A> as) 
    {return as.isEmpty() 
    ? nil() 
    : cons(f.f(as.head()), new P1<Stream<A>>() 
     {public Stream<A> _1() 
      {return map(f, as.tail);}});} 

noti che Stream<A> è costituito da un valore di tipo A e un valore di tipo P1 che è come un tonfo che restituisce il resto della stream quando viene chiamato _1(). Sebbene sembri certamente una ricorsione, la chiamata ricorsiva alla mappa non viene creata, ma diventa parte della struttura del flusso.

Questo può quindi essere svolto con un normale costrutto.

for (Stream<B> b = bs; b.isNotEmpty(); b = b.tail()._1()) 
    {System.out.println(b.head());} 

Ecco un altro esempio, dal momento che si stava parlando di Project Euler. Questo programma utilizza le funzioni ricorsive reciprocamente e non suona lo stack, anche per milioni di chiamate:

import fj.*; import fj.data.Natural; 
import static fj.data.Enumerator.naturalEnumerator; 
import static fj.data.Natural.*; import static fj.pre.Ord.naturalOrd; 
import fj.data.Stream; import fj.data.vector.V2; 
import static fj.data.Stream.*; import static fj.pre.Show.*; 

public class Primes 
    {public static Stream<Natural> primes() 
    {return cons(natural(2).some(), new P1<Stream<Natural>>() 
     {public Stream<Natural> _1() 
     {return forever(naturalEnumerator, natural(3).some(), 2) 
       .filter(new F<Natural, Boolean>() 
        {public Boolean f(final Natural n) 
         {return primeFactors(n).length() == 1;}});}});} 

    public static Stream<Natural> primeFactors(final Natural n) 
    {return factor(n, natural(2).some(), primes().tail());} 

    public static Stream<Natural> factor(final Natural n, final Natural p, 
             final P1<Stream<Natural>> ps) 
    {for (Stream<Natural> ns = cons(p, ps); true; ns = ns.tail()._1()) 
      {final Natural h = ns.head(); 
      final P1<Stream<Natural>> t = ns.tail(); 
      if (naturalOrd.isGreaterThan(h.multiply(h), n)) 
       return single(n); 
      else {final V2<Natural> dm = n.divmod(h); 
       if (naturalOrd.eq(dm._2(), ZERO)) 
        return cons(h, new P1<Stream<Natural>>() 
         {public Stream<Natural> _1() 
         {return factor(dm._1(), h, t);}});}}} 

    public static void main(final String[] a) 
    {streamShow(naturalShow).println(primes().takeWhile 
     (naturalOrd.isLessThan(natural(Long.valueOf(a[0])).some())));}} 

Un'altra cosa che potete fare per lo scambio di stack per heap è quello di utilizzare più thread . L'idea è che invece di fare una chiamata ricorsiva, si crea un thunk che effettua la chiamata, si passa questo thunk a un nuovo thread e si lascia che il thread corrente esca dalla funzione. Questa è l'idea alla base di cose come Stackless Python.

Di seguito è riportato un esempio di ciò in Java. Scuse che è un po 'opaco a guardare senza i import static clausole:

public static <A, B> Promise<B> foldRight(final Strategy<Unit> s, 
              final F<A, F<B, B>> f, 
              final B b, 
              final List<A> as) 
    {return as.isEmpty() 
    ? promise(s, P.p(b)) 
    : liftM2(f).f 
     (promise(s, P.p(as.head()))).f 
     (join(s, new P1<Promise<B>>>() 
      {public Promise<B> _1() 
       {return foldRight(s, f, b, as.tail());}}));} 

Strategy<Unit> s è sostenuta da un pool di thread, e la funzione promise mani un tonfo al pool di thread, restituendo un Promise, che è molto simile java.util.concurrent.Future, solo meglio. See here. Il punto è che il metodo sopra piega una infrastruttura destra ricorsiva a destra nello stack O (1), che di solito richiede l'eliminazione di coda. Quindi abbiamo effettivamente ottenuto TCE, in cambio di una certa complessità. Si potrebbe chiamare questa funzione come segue:

Strategy<Unit> s = Strategy.simpleThreadStrategy(); 
int x = foldRight(s, Integers.add, List.nil(), range(1, 10000)).claim(); 
System.out.println(x); // 49995000 

Si noti che quest'ultima tecnica funziona perfettamente bene per non lineare ricorsione. Cioè, verrà eseguito in algoritmi di stack costanti che non hanno chiamate tail.

Un'altra cosa che puoi fare è impiegare una tecnica chiamata trampolino. Un trampolino è un calcolo, reificato come una struttura di dati, che può essere attraversato. Lo Functional Java library include un tipo di dati Trampoline che ho scritto, che consente in effetti di trasformare qualsiasi chiamata di funzione in una coda. Come esempio here is a trampolined foldRightC that folds to the right in constant stack:

public final <B> Trampoline<B> foldRightC(final F2<A, B, B> f, final B b) 
    {return Trampoline.suspend(new P1<Trampoline<B>>() 
    {public Trampoline<B> _1() 
     {return isEmpty() 
     ? Trampoline.pure(b) 
     : tail().foldRightC(f, b).map(f.f(head()));}});} 

È lo stesso principio utilizzando più thread, salvo che invece di invocare ogni passo nel proprio thread, si costruisce ogni passaggio sul mucchio, molto simile utilizzando un Stream, e poi eseguire tutti i passaggi in un unico ciclo con Trampoline.run.

+20

+1 per portarmi giù nella tana del coniglio. –

+18

Questo è il codice Java più pazzo che ho visto scritto, +1 per la spiegazione molto dettagliata. –

+0

OBS di K & R qualcuno? – Xailor

1
public static <A, B> Promise<B> foldRight(final Strategy<Unit> s, 
              final F<A, F<B, B>> f, 
              final B b, 
              final List<A> as) 
{ 
    return as.isEmpty() ? promise(s, P.p(b)) 
    : liftM2(f).f(promise(s, P.p(as.head()))) 
     .f(join(s, new F<List<A>, P1<Promise<B>>>() 
     { 
      public Promise<B> f(List<A> l) 
      { 
       return foldRight(s, f, b, l); 
      } 
     }.f(as.tail()))); 
} 
-1

in Eclipse, se si utilizza, impostare -xss2m come argomenti VM.

o

-xss2m direttamente sulla riga di comando.

java -xss2m classname 
0

Ho incontrato lo stesso problema, e ha finito per riscrivere la ricorsione in un ciclo for e che ha fatto il trucco.

Problemi correlati