deftype/defmethod performance in Clojurescript


Recently I watched a talk about DefRecord DefType in Clojure and ClojureScript given by Michał Marczyk at Clojure/West 2016.

Its a great talk on the details of implement of the two data types.

What I found interesting was that Michał emphasised that a lot of work has gone on to make deftype really performant in Clojurescript.

So, I decided to check how deftype performs in comparison to defrecord and a normal map.

To test the performance, I used the scaffolding I had worked on and described here.

I created Foo1 using deftype and Foo2 using a defrecord. Here is the code:

(defprotocol IAdd
  (add-num [this y] "Add two numbers"))

(deftype Foo1 [x]
  IAdd
  (add-num [this y]
    (+ x y)))

(defrecord Foo2 [x]
  IAdd
  (add-num [this y]
    (+ x y)))

(def f1 (Foo1. 1))

(def f2 (Foo2. 1))

(def f3 {:x 1})

I tested the performance of following three things:

  • (.-x f1)
  • (:x f2)
  • (:x 1)

which are the idiomatic ways of data access for the three data types.

All results are for Chrome 52.0.2734.0 canary (64-bit) on Mac OS X:

Type Mean Timing
deftype access 1.2000012871293235e-8
defmethod access 4.8206608599135286e-8
map access 6.966344634811152e-8

So in this example, field access for a deftype is 4 times faster than that for defmethod and 7 times faster than a normal map.

The speed of deftype method access makes some sense because it generates a javascript class and we are doing a direct field access for it whereas for the two there are different levels of indirection.

Now onto function invocation for the two datatypes.

I tested the performance of following two things:

  • (add-num f1 1)
  • (add-num f2 1)
Type Mean Timing
deftype function call 1.3490893960300394e-8
defmethod function call 1.519495239675906e-8

The results here are mixed.

I looked at the generated javascript code to understand what is happening:

(add-num f1 1)

expands to

add_num(bench.core.f1,(1))

whereas

(add-num f2 1)

expands to

add_num(bench.core.f2,(1))

So we need to look at the definition of add_num.

bench.core.add_num = (function bench$core$add_num(this$,y){
if((!((this$ == null))) && (!((this$.bench$core$IAdd$add_num$arity$2 == null)))){
return this$.bench$core$IAdd$add_num$arity$2(this$,y);
}
...

The key thing is if the first argument to add_num is non-null and it’s prototype has a method called bench$core$IAdd$add_num$arity$2 defined, then it is called with the two argument.

Looking at the code for Foo1 and Foo2, we see that the method is defined.

Foo1.prototype.bench$core$IAdd$add_num$arity$2 = (function (this$,y){
var self__ = this;
var this$__$1 = this;
return (self__.x + y);
});
Foo2.prototype.bench$core$IAdd$add_num$arity$2 = (function (this$,y){
var self__ = this;
var this$__$1 = this;
return (self__.x + y);
});

The function call for the two datatypes looks exactly the same. This explains the similarity in performance of the two.

Hopefully this gives you an insight on reading javascript code generated by clojurescript.