Fork me on GitHub
#clojure-dev
<
2021-02-13
>
serioga19:02:19

Is there an issue about that the defrecord's function ->Record does not have :tag meta, so you need to use type hinting to access record fields using dot notation (like (.field ^Record (->Record ...)))?

ghadi19:02:48

not sure, but FYI accesses to a record's fields have an inline optimization, so you can do (:foo r) and it should be just as fast as a type-hinted (.-foo ^Record r)

seancorfield19:02:19

That sounds very familiar -- I swear that's come up recently... gimme a sec...

seancorfield19:02:31

(no Jira ticket -- Alex is looking for a compelling example)

serioga19:02:46

benchmarks give me 2-3× difference :-)

borkdude19:02:24

@ghadi I am measuring something wrong here?

user=> (defrecord Foo [x])
user.Foo
user=> (let [^Foo r (->Foo 1)] (time (dotimes [i 1000000000] (.-x r))))
"Elapsed time: 260.812401 msecs"
nil
user=> (let [^Foo r (->Foo 1)] (time (dotimes [i 1000000000] (:x r))))
"Elapsed time: 2370.017491 msecs"
nil

serioga19:02:14

@borkdude we about ->Foo here 🙂

borkdude19:02:24

does it matter how the record is produced?

borkdude19:02:57

barring reflection issues

borkdude19:02:34

(example updated)

serioga20:02:44

(do
    (let [x (->Foo 1)] (time (dotimes [_ 1000000] (.a x))))
    (let [x (->Foo 1)] (time (dotimes [_ 1000000] (.a ^Foo x))))
    (let [x (Foo. 1)] (time (dotimes [_ 1000000] (.a x))))
    (let [x (Foo. 1)] (time (dotimes [_ 1000000] (:a x)))))
"Elapsed time: 1895.1031 msecs"
"Elapsed time: 3.8925 msecs"
"Elapsed time: 4.4709 msecs"
"Elapsed time: 11.5897 msecs"

borkdude20:02:43

The first usage just shows that reflection in a loop is slow. The other examples need more loops to be able to compare better.

ghadi20:02:16

hmm I'm reproducing your result @borkdude

serioga20:02:46

criterium gives 1-2 ns for field access and 6 ns for keyword

ghadi20:02:50

user=> (defn kwaccess [v] (:x v))
#'user/kwaccess
user=> (defn getaccess [v] (get v :x))
#'user/getaccess
user=> (let [r (->Foo 1)] (time (dotimes [i 1000000000] (kwaccess r))))
"Elapsed time: 2810.895121 msecs"
nil
user=> (let [r (->Foo 1)] (time (dotimes [i 1000000000] (getaccess r))))
"Elapsed time: 8820.251502 msecs"

ghadi20:02:22

this is what I expect if a record has an x field

borkdude20:02:55

@ghadi yes, but direct interop is still x10 faster in my result

borkdude20:02:25

which makes sense probably since invoking a keyword is invoking a function which is another indirection

ghadi20:02:39

no, there is a special case in the compiler

ghadi20:02:50

when keyword appears first

ghadi20:02:23

user=> (let [^Foo r (->Foo 1)] (time (dotimes [i 1000000000] (.-x r))))
"Elapsed time: 565.111442 msecs"
nil
user=> (let [^Foo r (->Foo 1)] (time (dotimes [i 1000000000] (:x r))))
"Elapsed time: 2362.38707 msecs"
nil
user=> (let [^Foo r (->Foo 1)] (time (dotimes [i 1000000000] (get r :x))))
"Elapsed time: 12014.947945 msecs"

bronsa20:02:53

it makes sense that the kw invoke path is still slower than direct invocation

serioga20:02:54

yes, keyword access from map is slower than from record at least in repl benchmarks

bronsa20:02:12

there's quite a lot more bytecode involved and branching

ghadi20:02:59

yeah, I have an invokedynamic variant of this keyword access optimization that has less bytecode

ghadi20:02:16

anyways, it is unlikely going to dominate in any benchmarks, unless of course it reflects