【问题标题】:Clojure thunks: stack overflow with [0] but not '(0)?Clojure thunks:堆栈溢出 [0] 但不是 '(0)?
【发布时间】:2014-12-11 08:45:12
【问题描述】:

这是一个给我StackOverflowError的sn-p代码(从我的代码库中的一个实际示例中总结出来):

( ->> (range 3000)
      (mapcat #(concat [0] (take 100 (repeat %))))
      (reduce (constantly nil))
      (count))

(注意:此代码的设计目的是除了演示问题或返回零以外的任何事情。)

我可以通过以下任何步骤“拯救”它:

  1. 删除reduce
  2. [0] 更改为'(0)
  3. mapcatcount 之间的任意点添加(take 100000000)(或任何整数)。

我基本上对这种行为感到困惑(尤其是#2)。我会很感激任何意见。

(我感觉这可能与 Why does reduce give a StackOverflowError in Clojure? 有关,但我不能完全确定如何 - 所以如果它是相关的,我会很感激解释原因。)

【问题讨论】:

  • 假设返回 0 吗?有趣的是,如果我将其更改为这样,我不会得到堆栈溢出: ( ->> (range) (mapcat (fn [p] (concat [0] (take 100 (repeat nil))))) (take 3000) (减少(始终为零))(计数))
  • 是的,它“应该”返回 0。(我的意思是,除了以我能找到的最短方式演示此行为之外,代码实际上不应该做任何事情。)count只是为了证明使用序列本身不会导致错误。在您的示例中,@firthh,如果您愿意,您可以将 (take 3000) 更改为 (take 3000000),它仍然有效。您也可以在reduce步骤之后放置(取3000)...
  • reduce 是严格的,并且使用恒定的堆栈空间 - concat / mapcat 调用更有可能在被强制之前堆叠未评估导致问题
  • @noisesmith 不过,这并不是真正的解释。为什么删除reducecount 会成功?为什么'(0)的行为不同?为什么不包装 concat(或 mapcat)导致(doall)帮助?

标签: clojure stack-overflow lazy-evaluation thunk


【解决方案1】:

在正常情况下,reduce 使用loop/recur 构造运行并使用常量堆栈空间。但是,您遇到了一个令人讨厌的极端情况,这是由于减少了通过为concat 提供交替的分块和非分块序列而产生的序列(向量[0] 是分块的;(take 100 (repeat %)) 产生的序列是非分块的)。

concat 的第一个参数是分块序列时,它将返回一个惰性序列,该序列将使用chunk-cons 生成另一个分块序列。否则,它将使用cons 生成非分块序列。

同时,reduce 的实现使用了InternalReduce 协议(在clojure.core.protocols 中定义),该协议为结构提供了一个internal-reduce 函数,可以比默认的第一个/下一个递归更有效地减少自身。分块序列的internal-reduce 实现使用分块函数在循环中消耗分块项,直到它留下一个非分块序列,然后在剩余部分上调用internal-reduce。默认的internal-reduce 实现类似地使用first/next 在循环中使用项目,直到底层seq 类型发生变化,然后在新的seq 类型上调用internal-reduce 以调度到适当的优化版本。当您处理concat 生成的序列时,在分块和非分块子序列之间交替,internal-reduce 调用会堆积在堆栈上并最终破坏它。

这个案例的一个更简单的例子是:

;; All chunked sub-seqs is OK
user> (reduce + (apply concat (take 10000 (repeat [1]))))
10000

;; All non-chunked sub-seqs is OK
user> (reduce + (apply concat (take 10000 (repeat '(1)))))
10000

;; Interleaved chunked and non-chunked sub-seqs blows the stack
user> (reduce + (apply concat (take 10000 (interleave (repeat [1]) (repeat '(1))))))
StackOverflowError   clojure.lang.LazySeq.seq (LazySeq.java:60)

检查堆栈跟踪:

StackOverflowError 
    clojure.core/seq (core.clj:133)
    clojure.core/interleave/fn--4525 (core.clj:3901)
    clojure.lang.LazySeq.sval (LazySeq.java:42)
    clojure.lang.LazySeq.seq (LazySeq.java:60)
    clojure.lang.RT.seq (RT.java:484)
    clojure.core/seq (core.clj:133)
    clojure.core/take/fn--4232 (core.clj:2554)
    clojure.lang.LazySeq.sval (LazySeq.java:42)
    clojure.lang.LazySeq.seq (LazySeq.java:60)
    clojure.lang.Cons.next (Cons.java:39)
    clojure.lang.RT.next (RT.java:598)
    clojure.core/next (core.clj:64)
    clojure.core/concat/cat--3925/fn--3926 (core.clj:694)
    clojure.lang.LazySeq.sval (LazySeq.java:42)
    clojure.lang.LazySeq.seq (LazySeq.java:60)
    clojure.lang.ChunkedCons.chunkedNext (ChunkedCons.java:59)
    clojure.core/chunk-next (core.clj:660)
    clojure.core.protocols/fn--6041 (protocols.clj:101)
    clojure.core.protocols/fn--6005/G--6000--6014 (protocols.clj:19)
    clojure.core.protocols/fn--6034 (protocols.clj:147)
    clojure.core.protocols/fn--6005/G--6000--6014 (protocols.clj:19)
    clojure.core.protocols/fn--6041 (protocols.clj:104)
    clojure.core.protocols/fn--6005/G--6000--6014 (protocols.clj:19)
    clojure.core.protocols/fn--6034 (protocols.clj:147)
    clojure.core.protocols/fn--6005/G--6000--6014 (protocols.clj:19)
    clojure.core.protocols/fn--6041 (protocols.clj:104)
    clojure.core.protocols/fn--6005/G--6000--6014 (protocols.clj:19)
    clojure.core.protocols/fn--6034 (protocols.clj:147)
    clojure.core.protocols/fn--6005/G--6000--6014 (protocols.clj:19)
    clojure.core.protocols/fn--6041 (protocols.clj:104)

至于您的解决方法:

  • 显然,避免 reduce 可以完全避免问题。
  • [0] 更改为'(0) 将分块子序列替换为非分块子序列,绕过internal-reduce 中分块序列的优化,并允许在具有恒定堆栈空间的单个循环中进行缩减。李>
  • 插入 take 会创建一个新的非分块序列,完全由 cons 单元组成。

【讨论】:

  • 是的,就是这样。
【解决方案2】:

我认为问题出在mapcat,它调用concat,它使用cons。向量上的cons 很昂贵(并且可能会消耗堆栈),而列表则很便宜。这就是为什么从向量更改为列表“修复”问题的原因。

【讨论】:

  • 您能解释一下为什么失败只发生在reduce 步骤中吗?如果我删除reduce,为什么count 仍然有效(这会毫无问题地消耗整个序列)
猜你喜欢
  • 2010-09-18
  • 2015-12-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-18
  • 2015-02-04
  • 1970-01-01
  • 2014-12-15
相关资源
最近更新 更多