Thursday 26 January 2012

Oh, that performance thing...

Sooo, via this fascinating post and confirmed by my own dirty timing efforts, there's another excellent reason to not define structures that naively implement clojure.lang.IFn.

That being, it's really rather slow.

Using an AABB tree-node I've been messing with, extracting a single argument from a node defined with defrecord 106 times takes roughly:

TimeForm
15 ms(:key node)
135 ms(node :key) (that is, implementing IFn using get)
85 ms(let [{:keys [key]} node] ...)

Fascinating stuff. But that's definitely a large enough improvment that I'll be stripping out the implementation of invoke, and be more aggressive promoting ad-hoc maps to records in future! It's hard to say no to an order of magnitude improvement.

Edit: I don't mean that clojure.lang.IFn is necessarily slow, just that it seems the (:key record) form is faster than (get record :key). I'm assuming this is due to directly mapping the former to member access, whilst the latter has to use some combination of additional reflection and the hash map implementation to get the value.

5 comments:

swannodette said...

What did your implementation of IFn look like? IFn is not inherent slow.

Snut said...

Oh, that's no the impression I wished to give! I'll edit the post shortly to better reflect this.

Basically, my incredibly naive implementation was just:

(defrecord foo [k1 k2]
clojure.lang.IFn
(invoke [this k]
(get this k)))

Looks like the (:k1 a-foo) form maps directly to member access on the underlying object, hence nice and quick.

Interestingly, expanding the invoke method to (condp = k :k1 (:k1 this) etc.) is significantly faster than using get, at least for my small test case. Still means that (a-foo :k1) is half the speed of (:k a-foo), but faster than other implementations I've tried so far.

swannodette said...

Snut I believe what you want to use is case - it will be much faster than condp. The fastest thing is probably to define an accessor protocol.

Snut said...

Excellent point about case.

I only recently started using Clojure 1.3.0, and have a lot of catching up to do. At least that's my excuse for not using case yet! Quick tests do have case taking around a third of the time required to use get, and unlike condp its speed is independant of the number of cases.

It's still slower than using (:key foo) directly, however. I'll have to check out accessor protocols as a possibility too.

swannodette said...

Don't forget you can always access the field directly. But that will require a type hint. Defining a accessor protocol will allow you to grab the field quickly without bothering with the type hint.