【问题标题】:Why does this code report a stack overflow in Clojure为什么这段代码在 Clojure 中报告堆栈溢出
【发布时间】:2018-08-29 16:30:12
【问题描述】:

这是重现 Ross Ihaka 给出的一些代码的简单尝试,作为 R 性能不佳的示例。我很好奇 Clojure 的持久数据结构是否会提供任何改进。 (https://www.stat.auckland.ac.nz/~ihaka/downloads/JSM-2010.pdf)

但是,我什至没有到达一垒,并且报告了堆栈溢出,并且没有太多其他内容可以参考。有任何想法吗?如果问题有我错过的明显答案,请提前道歉......

; Testing Ross Ihaka's example of poor R performance
; against Clojure, to see if persisntent data structures help

(def dd (repeat 60000 '(0 0 0 0)))

(defn repl-row [d i new-r]
   (concat (take (dec i) d) (list new-r) (drop i d)))

(defn changerows [d new-r]
  (loop [i 10000
         data d]
    (if (zero? i)
      data
      (let [j (rand-int 60000)
            newdata (repl-row data j new-r)]
        (recur (dec i) newdata)))))


user=> (changerows dd '(1 2 3 4))

StackOverflowError   clojure.lang.Numbers.isPos (Numbers.java:96)

此外,如果有人知道如何在上面的示例中充分利用持久性函数数据结构,我会很想听听。据报告使用不可变结构(上面的链接)的加速比约为 500%!

【问题讨论】:

  • 你正在脱离循环。我用较小的数字进行了测试并修改了您的代码,因此很明显if 有两个分支。

标签: clojure stack-overflow


【解决方案1】:

查看 StackOverflowError 的堆栈跟踪,这似乎是一个“爆炸性 thunk”(延迟/暂停计算)问题,与您的示例中的递归没有明显关系:

java.lang.StackOverflowError
    at clojure.lang.RT.seq(RT.java:528)
    at clojure.core$seq__5124.invokeStatic(core.clj:137)
    at clojure.core$concat$cat__5217$fn__5218.invoke(core.clj:726)
    at clojure.lang.LazySeq.sval(LazySeq.java:40)
    at clojure.lang.LazySeq.seq(LazySeq.java:49)
    at clojure.lang.RT.seq(RT.java:528)
    at clojure.core$seq__5124.invokeStatic(core.clj:137)
    at clojure.core$take$fn__5630.invoke(core.clj:2876)

将此行更改为将newdata 实现为向量即可解决此问题:

(recur (dec i) (vec newdata))

这个变通方法是解决concatrepl-row中的使用,通过在每一步中强制实现concat的惰性序列。 concat 返回惰性序列,在您的 loop/recur 中,您将先前 concat 调用的惰性/未评估结果作为输入传递给后续 concat 调用,后者基于先前返回更多惰性序列,未实现的惰性序列。 final concat-produced 的惰性序列直到循环结束才实现,这导致堆栈溢出,因为它依赖于数千个先前的 concat-produced 的惰性序列。

此外,如果有人知道如何在上面的示例中最大限度地利用持久性函数数据结构,我很想听听。

由于这里concat 的用法似乎只是简单地替换集合中的元素,因此我们可以通过使用向量和assoc-将新项目放入向量的正确位置来获得相同的效果:

(def dd (vec (repeat 60000 '(0 0 0 0))))

(defn changerows [d new-r]
  (loop [i 10000
         data d]
    (if (zero? i)
      data
      (let [j (rand-int 60000)
            newdata (assoc data j new-r)]
        (recur (dec i) newdata)))))

注意没有更多的repl-row 函数,我们只是使用索引和新值将assoc 转换为data。在使用time 进行一些基本的基准测试后,这种方法似乎要快很多倍:

"Elapsed time: 75836.412166 msecs" ;; original sample w/fixed overflow
"Elapsed time: 2.984481 msecs"     ;; using vector+assoc instead of concat

还有另一种解决方法,将一系列替换视为替换步骤的无限序列,然后从该序列中采样:

(defn random-replace [replacement coll]
  (assoc coll (rand-int (count coll)) replacement))

(->> (iterate (partial random-replace '(1 2 3 4)) dd)
     (drop 10000) ;; could also use `nth` function
     (first))

【讨论】:

  • 感谢 Taylor,是的,这些是 Ihaka 通过切换到可变代码所获得的改进顺序。我会研究你的例子,我喜欢这种关联数据的方式,这似乎是我所追求的。
猜你喜欢
  • 1970-01-01
  • 2011-12-18
  • 1970-01-01
  • 2012-05-10
  • 1970-01-01
  • 2017-10-07
  • 2014-12-20
  • 2015-07-02
  • 1970-01-01
相关资源
最近更新 更多