Clojure comp不尾调用优化(可以创建StackOverflow异常)

时间:2019-04-24 02:49:57

标签: performance clojure

我陷入了处理大量数据(图像数据)的Clojure程序中。当图像大于128x128时,程序将崩溃并出现StackOverflow异常。因为它适用于较小的图像,所以我知道这不是无限循环。

有很多可能导致内存占用率很高的原因,因此我花了一些时间在周围研究。确保我正确地使用了惰性序列,确保适当地使用recur,依此类推。当我意识到这一点时,转折点就来了:

at clojure.core$comp$fn__5792.invoke(core.clj:2569)
at clojure.core$comp$fn__5792.invoke(core.clj:2569)
at clojure.core$comp$fn__5792.invoke(core.clj:2569)

指的是comp函数。

所以我看了看我在哪里使用它:

(defn pipe [val funcs]
  ((apply comp funcs) val))

(pipe the-image-vec
  (map
    (fn [index] (fn [image-vec] ( ... )))
    (range image-size)))

我正在按像素进行操作,将一个函数映射到每个像素进行处理。有趣的是,comp似乎没有从尾调用优化中受益,也没有从函数中进行任何形式的顺序应用。看来这只是以基本方式组成的东西,当有65k函数时,可以理解的是,它会使堆栈溢出。这是固定版本:

(defn pipe [val funcs]
  (cond
    (= (count funcs) 0) val
    :else               (recur ((first funcs) val) (rest funcs))))

recur确保递归优化了尾调用,从而防止了堆栈堆积。

如果任何人都可以解释为什么comp以这种方式工作(或者相反,不能以这种方式工作),我很乐意得到启发。

1 个答案:

答案 0 :(得分:3)

首先,一个更简单的MCVE:

(def fs (repeat 1e6 identity))
((apply comp fs)  99)

#<StackOverflowError...

但是为什么会这样呢?如果您查看(摘要)的comp来源:

(defn comp
  ([f g] 
     (fn 
       ([x] (f (g x)))
  ([f g & fs]
     (reduce1 comp (list* f g fs))))

您可以看到整个过程基本上只有两部分:

  • 第一个参数重载,其主要工作是将每个组合的函数调用包装在另一个函数中。。

  • 使用comp简化功能。

我相信第一点就是问题。 comp的工作方式是获取函数列表,并将每组调用连续包装在函数中。最终,如果您尝试编写过多的函数,这将耗尽堆栈空间,因为这最终会创建一个包装了许多其他函数的庞大函数。

那么,为什么TCO在这里无济于事?因为与大多数StackOverflowErrors不同,所以递归不是问题。在底部的可变情况下,递归调用只能到达一帧深度。问题是要建立一个庞大的功能,而不能简单地对其进行优化。

为什么您可以“修复”它?因为您可以访问val,所以您可以随时评估函数,而无需构建一个函数以供以后调用。 comp是使用一种简单的实现编写的,该实现在大多数情况下都可以正常工作,但在极端情况下(如您介绍的情况)却无法使用。编写专门的版本处理大量馆藏是相当琐碎的:

(defn safe-comp [& fs]
  (fn [value]
    (reduce (fn [acc f]
              (f acc))
            value
            (reverse fs))))

当然,请注意,它不能像核心版本那样处理多种Arities。

老实说,在使用Clojure的3年多的时间里,我从未写过(apply comp ...)。尽管您肯定有可能遇到了我从来不需要处理的案例,但更有可能您在此工作中使用了错误的工具。这段代码完成后,将其发布在Code Review上,我们也许可以为您提供更好的方法来完成您要执行的操作。