2009-05-13 10 views
50

Después de un poco de experiencia con los lenguajes funcionales, empiezo a usar recursividad más en Java, pero el lenguaje parece tener una pila de llamadas relativamente poco profunda de aproximadamente 1000.¿Se desborda la pila de recursión profunda en Java?

¿Hay alguna manera de aumentar la cantidad de llamadas? Al igual que puedo hacer funciones que son millones de llamadas profundas, como en Erlang?

Estoy notando esto cada vez más cuando tengo problemas con el Proyecto Euler.

Gracias.

Respuesta

40

Creo que se puede utilizar estos parámetros

-SS STACKSIZE para aumentar el tamaño pila nativa o

-oss STACKSIZE para aumentar el tamaño de pila de Java ,

El nativo por defecto el tamaño de pila es 128k, con un valor mínimo de 1000 bytes. El tamaño predeterminado de la pila Java es 400k, con un valor mínimo de 1000 bytes.

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

EDIT:

Después de leer el primer comentario (de Chuck), así como volver a leer la pregunta y la lectura de otras respuestas, Me gustaría aclarar que yo interpreté la pregunta como solo "aumentar el tamaño de la pila". No pretendo decir que puedes tener montones infinitos, como en la programación funcional (un paradigma de programación que solo he arañado su superficie).

+6

Esto le puede dar más niveles, pero el tamaño de la pila sigue siendo limitado. No podrá recurrir infinitamente como en un lenguaje funcional con la eliminación de llamadas altas. – Chuck

+0

realmente no es una respuesta ... solución de ayuda de banda –

+1

El enlace está roto. – free6om

8

La mayoría de los lenguajes funcionales tienen soporte para recursividad de cola. Sin embargo, la mayoría de los compiladores de Java no son compatibles con esto. En cambio, realiza otra llamada de función. Esto significa que siempre habrá un límite superior en el número de llamadas recursivas que puede realizar (ya que eventualmente se quedará sin espacio en la pila).

Con la recursión de cola reutiliza el marco de pila de la función que se está repuriendo, por lo que no tiene las mismas restricciones en la pila.

23

Depende de la JVM utilizar o no la recursividad de cola: no sé por casualidad si alguno de ellos lo hace, pero no debe confiar en ello. En particular, cambiar el tamaño de la pila muy rara vez es lo correcto, a menos que tenga un límite estricto de cuántos niveles de recursión usaría en realidad, y usted sabía exactamente cuánto espacio de pila ocuparía cada uno. Muy frágil.

Básicamente, no debe usar la recursividad ilimitada en un idioma que no está configurado para ello. Tendrás que usar la iteración en su lugar, me temo. Y sí, eso puede ser un dolor leve a veces :(

+0

Sé que la JVM de Sun no optimiza la recursividad de cola, y tampoco creo que ninguna de las otras JVM importantes lo haga. Puede haber uno experimental o dos que lo hagan. –

+1

Creo que ** el poder de IBM. Escuché eso de segunda o tercera mano, así que no me cité sobre eso: P –

+2

Se está trabajando en la optimización de la cola de llamadas, pero actualmente no es compatible con Java porque rompe algunas expectativas sobre cómo se ve la pila, que son importantes para el modelo de seguridad de Java (y cosas menos importantes, como los rastreos de pila). http://blogs.sun.com/jrose/entry/tail_calls_in_the_vm – erickson

9

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

Ahora, si bien es probable que pueda encontrar una manera de aumentar la pila por defecto en Java, permítanme añadir mis 2 centavos en la que realmente Necesita encontrar otra forma de hacer lo que quiere hacer, en lugar de confiar en un aumento de la pila.

Dado que las especificaciones java no obligan a las JVM a implementar técnicas de optimización de recursividad de cola, la única forma de evitar el problema es reducir la presión de la pila, ya sea reduciendo el número de variables/parámetros locales que debe mantenerse al tanto, o idealmente reduciendo el nivel de recursividad de manera significativa, o simplemente reescribiendo sin recurrencia en absoluto.

+1

¿Por qué se está verificando si un idioma admite la eliminación de la llamada de cola como "incorrecta"? – Arafangion

+1

No lo es, pero Java no lo exige, por lo que no puede confiar en él. La situación sería diferente si Java ordenara la optimización de recursión de cola, entonces solo diría que debería intentar reestructurar la recursión para aprovecharla siempre. Como no es así, no lo hice. –

+0

¿Desea señalar cómo está mal? Tenga en cuenta que dije que eran mis 2 centavos, en otras palabras, una opinión. Usted es libre de estar en desacuerdo, pero decir que está mal, entonces realmente tiene que dar más detalles sobre por qué cree que está mal. Otros comentarios aquí han dado más información acerca de por qué JVM no implementa la recursión de cola de llamada. –

7

Puede configurar esto en la línea de comandos:

java -Xss8M clase

6

Clojure, que se ejecuta en la máquina virtual de Java, les gustaría mucho para implementar la optimización de llamada de cola, pero no puede debido a una restricción en el bytecode de JVM (no sé los detalles). Como consecuencia, solo puede ayudarse a sí mismo con un formulario especial de "recurrencia", que implementa algunas características básicas que usted esperaría de la recursión correcta de la cola.

De todos modos, esto significa que la JVM actualmente no puede apoyar la optimización de llamadas de la cola. Recomiendo encarecidamente no utilizar recursión como una construcción de bucle general en la JVM. Mi punto de vista personal es que Java no es un lenguaje de nivel suficientemente alto.

85

Aumentar el tamaño de la pila solo servirá como un vendaje temporal. Como han señalado otros, lo que realmente quieres es la eliminación de llamadas de cola, y Java no tiene esto por varias razones. Sin embargo, puedes hacer trampa si quieres.

¿Pastilla roja en la mano? OK, de esta manera por favor.

Hay formas en que puede intercambiar pila por pila. Por ejemplo, en lugar de realizar una llamada recursiva dentro de una función, devuelva estructura de datos vagos que realiza la llamada cuando se evalúa. A continuación, puede desenrollar la "pila" con Java for-construct. Lo demostraré con un ejemplo. Considere este código de Haskell:

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

Tenga en cuenta que esta función nunca evalúa el final de la lista. Entonces, la función en realidad no necesita hacer una llamada recursiva. En Haskell, en realidad devuelve un thunk para la cola, que se llama si alguna vez se necesita. Podemos hacer lo mismo en Java (esto usa las clases de 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);}});} 

Tenga en cuenta que Stream<A> consiste en un valor de tipo A y un valor de tipo P1 que es como un golpe seco que devuelve el resto de la transmitir cuando se llama _1(). Aunque ciertamente parece una recursión, la llamada recursiva al mapa no se hace, sino que se convierte en parte de la estructura de datos de Stream.

Esto se puede desenrollar con un for-build normal.

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

Aquí hay otro ejemplo, ya que hablabas Proyecto Euler. Este programa utiliza funciones mutuamente recursivas y no toca la pila, incluso para millones de llamadas:

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

Otra cosa que puede hacer para cambiar la pila de pila es el uso de múltiples hilos . La idea es que en lugar de hacer una llamada recursiva, crees un thunk que realiza la llamada, transfiere este thunk a un nuevo hilo y deja que el hilo actual salga de la función. Esta es la idea detrás de cosas como Stackless Python.

El siguiente es un ejemplo de eso en Java. Disculpas que es un poco opaco a mirar sin los import static cláusulas:

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 está respaldado por un grupo de subprocesos, y la función promise manos de un golpe seco a la agrupación de hebras, devolviendo un Promise, que es muy parecido java.util.concurrent.Future, solo que mejor. See here. El punto es que el método anterior dobla una estructura de datos recursiva a la derecha en O (1) pila, que ordinariamente requiere la eliminación de la llamada final. Así que efectivamente hemos logrado TCE, a cambio de cierta complejidad. Se podría llamar a esta función de la siguiente manera:

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

Tenga en cuenta que esta última técnica funciona perfectamente bien para la recursividad no lineal. Es decir, se ejecutará en algoritmos de pila constante que no tienen llamadas de cola.

Otra cosa que puede hacer es emplear una técnica llamada trampolín. Un trampolín es un cálculo, reificado como una estructura de datos, que se puede atravesar. El Functional Java library incluye un tipo de datos Trampoline que escribí, que efectivamente le permite convertir cualquier llamada de función en una llamada final. Como un ejemplo 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()));}});} 

Es el mismo principio que el uso de múltiples hilos, excepto que en lugar de invocar cada paso en su propio hilo, construimos cada paso en el montón, muy parecido usando un Stream, y luego ejecutar todos los pasos en un solo bucle con Trampoline.run.

+20

+1 por llevarme por el agujero del conejo. –

+18

Ese es uno de los códigos Java más loco que he visto escrito, +1 para la explicación muy detallada. –

+0

OTBS de K & R a alguien? – 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

en Eclipse si está utilizando, establezca -xss2m como argumentos vm.

o

-xss2m directamente en la línea de comandos.

java -xss2m classname 
0

me encontré con el mismo problema, y ​​terminó volver a escribir la recursividad en un ciclo for y que hizo el truco.

Cuestiones relacionadas