Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Performance concern of defining functions in a `let` form

Tags:

clojure

Is there any performance penalty for defining anonymous functions in a let form and calling the outer function repeatedly? Like this:

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

Compared to defining foo as a separate function, like this:

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

I understood the lexical scope of foo is different in these two cases, I'm just concerned if the foo function would be defined over and over again when calling bar multiple times.

I guess the answer is no, i.e., there is no penalty for this, but how did clojure do this?

Thank you!

like image 921
Davyzhu Avatar asked Mar 16 '23 04:03

Davyzhu


1 Answers

let local approach:

foo is compiled only once (when the top-level form is). The result of this compilation is a class implementing the clojure.lang.IFn interface; the actual body lives in the invoke(Object) method of that class. At runtime, each time control reaches the point in bar where the foo local is introduced, a fresh instance of that class is allocated; the two calls to foo use that instance of that class.

Here is a simple way to prove the "single compilation" property at the REPL:

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

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

NB. Clojure is smart enough to notice that foo is not an "actual closure" (it closes over the parameters of bar, but it doesn't actually use them), so the runtime representation of foo does not carry any extra fields that a closure would, but a fresh instance of foo's class is nevertheless allocated on each call to bar.

Separate defn approach:

There is a single instance of foo, however calling it involves an indirection through a Var, which itself comes with a non-zero cost. That cost is generally not worth worrying about in all but the most performance-sensitive code, but it's there, so factoring out a local function may not necessarily be a performance win. As usual, if it's worth optimizing, it's worth measuring/benchmarking first.

let over lambda

There is also the final option mentioned by Daniel, where the let goes around, and not inside, the defn. With that approach, there is a single instance of (the class of) foo; it is stored in a field inside bar; and it is used for all calls to foo inside bar.

like image 113
Michał Marczyk Avatar answered Mar 25 '23 02:03

Michał Marczyk