【发布时间】:2010-10-26 00:33:33
【问题描述】:
在对函数式语言有一些经验后,我开始在 Java 中更多地使用递归 - 但该语言的调用堆栈似乎相对较浅,约为 1000。
有没有办法让调用堆栈变大?就像在 Erlang 中那样,我可以制作数百万次调用的函数吗?
我在做 Project Euler 问题时越来越多地注意到这一点。
谢谢。
【问题讨论】:
标签: java functional-programming stack overflow
在对函数式语言有一些经验后,我开始在 Java 中更多地使用递归 - 但该语言的调用堆栈似乎相对较浅,约为 1000。
有没有办法让调用堆栈变大?就像在 Erlang 中那样,我可以制作数百万次调用的函数吗?
我在做 Project Euler 问题时越来越多地注意到这一点。
谢谢。
【问题讨论】:
标签: java functional-programming stack overflow
增加堆栈大小只会作为临时绷带。正如其他人所指出的那样,您真正想要的是消除尾调用,而Java由于各种原因没有这个。但是,您可以根据需要作弊。
手里拿着红色药丸?好的,请这边走。
您可以通过多种方式将堆栈换成堆。例如,不要在函数中进行递归调用,而是让它返回一个 惰性数据结构,在评估时进行调用。然后,您可以使用 Java 的 for-construct 展开“堆栈”。我将用一个例子来演示。考虑这个 Haskell 代码:
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = (f x) : map f xs
请注意,此函数从不计算列表的尾部。所以该函数实际上并不需要进行递归调用。在 Haskell 中,它实际上为尾部返回一个 thunk,如果需要,它就会被调用。我们可以在 Java 中做同样的事情(这使用来自 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);}});}
注意Stream<A> 由A 类型的值和P1 类型的值组成,这就像一个thunk,它在调用_1() 时返回流的其余部分。虽然它看起来确实像递归,但不会对 map 进行递归调用,而是成为 Stream 数据结构的一部分。
然后可以使用常规的 for-construct 展开。
for (Stream<B> b = bs; b.isNotEmpty(); b = b.tail()._1())
{System.out.println(b.head());}
这是另一个例子,因为你在谈论 Project Euler。这个程序使用相互递归的函数并且不会爆栈,即使是数百万次调用:
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())));}}
您可以用栈交换堆的另一件事是使用多线程。这个想法是,不是进行递归调用,您创建一个进行调用的 thunk,将这个 thunk 交给一个新线程并让当前线程退出函数。 这就是事情背后的想法像 Stackless Python。
以下是 Java 中的示例。抱歉,如果没有 import static 子句,看起来有点不透明:
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 由一个线程池支持,promise 函数将一个 thunk 传递给线程池,返回一个 Promise,这很像 java.util.concurrent.Future,只是更好。 See here. 重点是上面的方法在 O(1) 堆栈中向右折叠一个右递归数据结构,这通常需要消除尾调用。因此,我们有效地实现了 TCE,以换取一些复杂性。你可以这样调用这个函数:
Strategy<Unit> s = Strategy.simpleThreadStrategy();
int x = foldRight(s, Integers.add, List.nil(), range(1, 10000)).claim();
System.out.println(x); // 49995000
请注意,后一种技术非常适用于非线性递归。也就是说,即使没有尾调用的算法,它也会在常量堆栈中运行。
您可以做的另一件事是采用一种称为蹦床的技术。蹦床是一种计算,具体化为一种数据结构,可以单步执行。 Functional Java library 包含我编写的 Trampoline 数据类型,它可以有效地将任何函数调用转换为尾调用。例如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()));}});}
这和使用多线程的原理是一样的,只是我们不是在自己的线程中调用每个步骤,而是在堆上构造每个步骤,很像使用Stream,然后我们在一个带有Trampoline.run 的单循环。
【讨论】:
我遇到了同样的问题,最终将递归重写为一个 for 循环,结果成功了。
【讨论】:
如果你在使用 eclipse,请将 -xss2m 设置为 vm 参数。
或
-xss2m 直接在命令行上。
java -xss2m classname
【讨论】:
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())));
}
【讨论】:
我猜你可以使用这些参数
-ss Stacksize 增加原生 堆栈大小或
-oss Stacksize 增加Java 堆栈大小,
默认的本机堆栈大小为 128k, 最小值为 1000 字节。 默认的java堆栈大小是400k, 最小值为 1000 字节。
http://edocs.bea.com/wls/docs61/faq/java.html#251197
编辑:
在阅读第一条评论(Chuck 的)以及重新阅读问题并阅读其他答案后,我想澄清一下,我将问题解释为“增加堆栈大小”。我并不是要说你可以拥有无限的堆栈,例如在函数式编程中(我只是触及其表面的一种编程范式)。
【讨论】:
运行在 Java VM 上的 Clojure 非常想实现尾调用优化,但由于 JVM 字节码的限制(我不知道细节),它不能实现。因此,它只能通过一种特殊的“递归”形式来帮助自己,该形式实现了一些您期望从正确的尾递归中获得的基本功能。
无论如何,这意味着JVM目前不能支持尾调用优化。我强烈建议不要将递归用作 JVM 上的一般循环结构。我个人的看法是,Java 不是一种足够高级的语言。
【讨论】:
您可以在命令行上设置:
java -Xss8M 类
【讨论】:
If you have to ask, you're probably doing something wrong.
现在,虽然您可能可以找到一种方法来增加 java 中的默认堆栈,但让我添加我的 2 美分,因为您确实需要找到另一种方法来做您想做的事情,而不是依赖于增加的堆栈。
由于 java 规范没有强制要求 JVM 实现尾递归优化技术,解决该问题的唯一方法是减少堆栈压力,或者通过减少需要的局部变量/参数的数量被跟踪,或者理想情况下只是显着降低递归级别,或者只是重写而不使用递归。
【讨论】:
是否使用尾递归取决于 JVM - 我不知道它们中的任何一个是否使用,但你不应该依赖它。特别是,更改堆栈大小非常很少是正确的做法,除非您对实际使用的递归级别有一些硬性限制,并且您确切知道每个级别有多少堆栈空间会占用。非常脆弱。
基本上,您不应该在不是为它构建的语言中使用无限递归。恐怕您将不得不使用迭代。是的,有时这可能会有点痛苦:(
【讨论】:
大多数函数式语言都支持尾递归。但是,大多数 Java 编译器不支持这一点。相反,它会进行另一个函数调用。这意味着您可以进行的递归调用的数量总是有一个上限(因为您最终会用完堆栈空间)。
使用尾递归,您可以重用正在递归的函数的堆栈帧,因此您对堆栈没有相同的约束。
【讨论】: