以“let”形式定义函数的性能问题

时间:2015-08-05 08:17:44

标签: clojure

let形式定义匿名函数并重复调用外部函数是否存在性能损失?像这样:

(defn bar [x y]
  (let [foo (fn [x] (* x x))]
    (+ (foo x) (foo y))))

与将foo定义为单独的函数相比,如下所示:

(defn foo [x] (* x x))
(defn bar [x y] (+ (foo x) (foo y)))

我理解foo的词汇范围在这两种情况下是不同的,我只关心foo函数是否会定义一遍又一遍多次致电bar时。

我猜答案是 no ,即没有惩罚,但clojure是如何做到的?

谢谢!

2 个答案:

答案 0 :(得分:6)

let本地方法:

foo只编译一次(当顶级表单时)。这个编译的结果是一个实现clojure.lang.IFn接口的类;实际的身体存在于该类的invoke(Object)方法中。在运行时,每次控制到达引入bar local的foo中的点时,都会分配该类的新实例;对foo的两次调用都使用该类的实例。

这是一种简单的方法来证明"单个编译" REPL的财产:

(defn bar [x y]
  (let [foo (fn [x] (* x x))]
    foo))

(identical? (class (bar 1 2)) (class (bar 1 2)))
;= true

NB。 Clojure很聪明,可以注意到foo不是"实际关闭" (它关闭了bar的参数,但它实际上并没有使用它们),因此foo的运行时表示不会带有闭包所需的任何额外字段,而是一个新的实例然而,在foo的每次调用中都会分配bar个班级。

单独的defn方法:

foo的单个实例,但是调用它涉及通过Var的间接,它本身带有非零成本。除了最具性能敏感性的代码之外,这个成本通常不值得担心,但它就在那里,因此分解本地功能可能不一定是性能上的胜利。像往常一样,如果它值得优化,那么首先值得测量/基准测试。

let超过lambda

还有丹尼尔提到的最终选项,其中let绕过defn,而不是foo。使用这种方法,有一个(类)bar的单个实例;它存储在foo内的字段中;它用于bardocument.write([1,2,3]); 的所有来电。

答案 1 :(得分:1)

答案是肯定的,这将会受到惩罚,但迈克尔在let local approach部分指出,这是最小的。

如果你想要你所描述的词法范围,那么将定义foo的let块包围在defn周围吧。这将做你要求的。然而,这是Clojure代码中不常见的模式。