Recursion 了解Clojure(或java)中调用堆栈的深度

Recursion 了解Clojure(或java)中调用堆栈的深度,recursion,clojure,stack-overflow,Recursion,Clojure,Stack Overflow,Clojure是我第一次进入lisp世界。我决定尝试一个简单的递归函数,将数字提升为整数的幂。很简单 (defmulti pow #(compare %2 0)) (defmethod pow 0 [a n] 1) (defmethod pow 1 [a n] (* a (pow a (dec n)))) (defmethod pow -1 [a n] (/ 1 (pow a (* -1 n)))) 如果只传递整数次幂,则效果很好。但当我使用它提升到足够大的功率时,我会得到堆栈溢出(可以预见)

Clojure是我第一次进入lisp世界。我决定尝试一个简单的递归函数,将数字提升为整数的幂。很简单

(defmulti pow #(compare %2 0))
(defmethod pow 0 [a n] 1)
(defmethod pow 1 [a n] (* a (pow a (dec n))))
(defmethod pow -1 [a n] (/ 1 (pow a (* -1 n))))
如果只传递整数次幂,则效果很好。但当我使用它提升到足够大的功率时,我会得到堆栈溢出(可以预见)

问题是,我怎样才能在函数死之前准确地找出它递归的深度呢?一种大致了解我能递归多深的方法是:

(defn max-depth [n] 
    (try
        (max-depth (inc n))
    (catch StackOverflowError err (do
        (printf "%s at %d\n" err n)
        n)
)))
(Clojure是我第一次尝试lisp,所以我真的不知道如何使代码看起来可读。)这段代码只是无限递归,直到它溢出堆栈,然后返回在它崩溃之前发生的递归次数。这只给了我一个大概的数字,我可以走多深。。。这种方法有很多问题

我可以做的另一件事是捕获异常并尝试自己解开堆栈。。。但我真的看不到从异常中获取所需信息的方法。在查看StackOverflowerr的javadoc时,我看到了一些看起来很有前途的方法,但就我所能看到的而言,还没有什么具体的方法

我尝试运行(count(.getStackTrace error)),其中error是我捕获的stackoverflow,但结果仅为1024,因为“在某些情况下,堆栈帧可能会被复制”。所以那没用

我唯一能想到的另一件事是在连续较大的指数上运行pow,但这并没有真正告诉我调用堆栈有多深,它只告诉我可以提升到指数的多大(因为函数调用其他函数,两个答案不一样)

我也不知道用java做这件事的任何方法,但如果有,那么这个答案可能也会有帮助

有什么想法吗? 谢谢
--Scott

我本以为
(.getStackTrace error)
是您的最佳选择-为什么您认为它不起作用

在为一个简单的函数打开堆栈之前,递归深度为1024,这听起来与我所期望的差不多


请记住,单个Clojure函数通常会转换为几个JVM方法调用,因此实际的JVM堆栈深度可能比递归有效
n
要深得多。如果您想从堆栈跟踪转换回
n
,您可能需要计算对multimethod的递归调用在堆栈跟踪中出现的次数。

因此,我实际上不打算回答您的问题,因为我认为尝试调试调用堆栈深度是解决此问题的错误方法。相反,我将建议在Clojure中使用一种更有效的递归方法,这种方法不会破坏调用堆栈

重要的概念是“尾部调用消除”——这基本上意味着,如果函数F是递归函数,它所做的最后一件事就是调用自身,则可以对其进行优化。对于F的第n次递归调用,它所做的最后一件事是调用F的n+1次递归调用并将该值返回到F的n-1次调用-因此您可以覆盖与F的第n次调用相关的所有堆栈数据,并让n+1次调用直接将其值返回到n-1次调用。因此,这基本上意味着您只需要堆栈上的一个位置,而不是数百或数千个位置

JVM本身不支持函数调用的这种优化,否则您的示例可能会起作用。相反,Clojure提供了recur操作符,它重用当前循环或函数调用,而不是增加堆栈。我会用它像这样重写你的函数(为了简单起见,我省略了你对负指数的支持):

另见:和

当然,pow的这个实现只是一个递归示例——任何阅读本文的人都应该在实际工作中使用Math/pow

user=> (time (Math/pow 1 10000000))
"Elapsed time: 0.166723 msecs"
1.0
user=> (time (pow 1 10000000))
"Elapsed time: 2991.321703 msecs"
1
user=> (time (pow 2 1000))

ArithmeticException integer overflow  clojure.lang.Numbers.throwIntOverflow (Numbers.java:1388)
user=> (time (Math/pow 2 1000))
"Elapsed time: 0.069109 msecs"
1.0715086071862673E301

我很怀疑,因为1024在计算中是一个完美的整数。我只是假设他们将其截断为2^10个插槽(因为他们可以这样做)。另外,逐行查看堆栈帧,我只看到了一堆交替的“clojure.lang.MultiFn.invoke(MultiFn.java:231)”和“user$eval157$fn_uu158.invoke(powtest:5)”,如果它是完整的堆栈跟踪,就不会出现这种情况——它会有调用它的方法。还忘了提到,我从max-depth得到的最大递归深度通常在高9900,接近10000,但不完全是10000,大约是堆栈跟踪显示的9到10倍。
user=> (time (Math/pow 1 10000000))
"Elapsed time: 0.166723 msecs"
1.0
user=> (time (pow 1 10000000))
"Elapsed time: 2991.321703 msecs"
1
user=> (time (pow 2 1000))

ArithmeticException integer overflow  clojure.lang.Numbers.throwIntOverflow (Numbers.java:1388)
user=> (time (Math/pow 2 1000))
"Elapsed time: 0.069109 msecs"
1.0715086071862673E301