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.