【问题标题】:Find out how deep a call stack went in Clojure (or java)找出调用堆栈在 Clojure(或 java)中的深度
【发布时间】:2013-06-02 06:30:14
【问题描述】:

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,所以我真的不知道如何让代码看起来可读。)这段代码只是无限递归,直到它溢出堆栈,然后返回它之前发生的递归次数炸毁了。这只能让我大致了解我可以走多远……这种方法存在很多问题。

我可以做的另一件事是捕获异常并尝试自己展开堆栈...但是我真的看不到从异常中获取我想要的信息的方法。查看http://docs.oracle.com/javase/6/docs/api/java/lang/StackOverflowError.html 的 StackOverflowError 的 javadoc,我看到了一些看起来很有希望的方法,但据我所知,没有任何结果。

我尝试运行 (count (.getStackTrace error)) ,其中错误是我捕获的堆栈溢出,但结果仅为 1024,因为“在某些情况下,可能会省略堆栈帧”。所以这没有用。

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

我也不知道在 java 中有什么方法可以做到这一点,但如果有的话,那么这个答案也可能会有所帮助。

有什么想法吗? 谢谢, --斯科特

【问题讨论】:

    标签: recursion clojure stack-overflow


    【解决方案1】:

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

    在为一个简单的函数炸毁堆栈之前的 1024 递归深度听起来符合我的预期。

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

    【讨论】:

    • 我很怀疑,因为 1024 在计算中是如此完美的整数。我只是假设他们将其截断为 2^10 个插槽(因为他们被允许这样做)。另外,逐行查看堆栈帧,除了一堆交替的“clojure.lang.MultiFn.invoke(MultiFn.java:231)”和“user$eval157$fn__158.invoke(powtest:5 )" 如果它是一个完整的堆栈跟踪就不会出现这种情况——它会有调用它的方法。
    • 另外忘了提一下,我从 max-depth 得到的最大递归深度通常在 9,900 的高位,接近但不完全是 10,000,大约是堆栈的 9 到 10 倍跟踪将表明。
    【解决方案2】:

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

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

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

    user=> (defn pow
      #_=>   ([base exp] (pow base exp base)) ; default the accumulator argument to the base of the power
      #_=>   ([base exp acc] (if (= exp 1)
      #_=>                         acc ; base case - return the accumulated power
      #_=>                         (recur base (dec exp) (* base acc))))) ; recur, decrementing the power
    #'user/pow
    user=> 
    
    user=> (pow 1 10000000) ; stack not blown
    1
    user=> (pow 2 10)
    1024
    

    另请参阅:http://clojure.org/functional_programming#Functional%20Programming--Recursive%20Loopinghttp://en.wikipedia.org/wiki/Tail_call

    当然,这个 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
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-09-04
      • 2017-09-16
      • 2010-10-26
      • 2011-05-03
      • 2021-12-28
      • 1970-01-01
      • 2018-02-11
      相关资源
      最近更新 更多