我陷入了处理大量数据(图像数据)的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
以这种方式工作(或者相反,不能以这种方式工作),我很乐意得到启发。
答案 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上,我们也许可以为您提供更好的方法来完成您要执行的操作。