When should defrecord be slower than deftype?

341 views
Skip to first unread message

Mars0i

unread,
Jun 2, 2015, 1:08:13 PM6/2/15
to clo...@googlegroups.com
I have an application using Java interop, in which I can define a class using either deftype or defrecord.  It has one field, containing an atom containing a double, and a few methods.  One of the methods is specified by an interface defined in Java, and that method is called from Java.  This method is called many times in an inner loop.  I don't use any of the extra functionality provided by defrecord, but in otheri, similar programs, I will.

When I use defrecord, the speed of the program is about 25% of its speed when I use deftype.  This is a one-line change.  I'm testing speed simply by running the program for a long time, and timing it.  I've done this test repeatedly, so there's no reason to think the differences have to do with other processes running on my machines.

When I benchmark simple uses of defrecord and deftype using criterium, they're about the same speed.

Is there any obvious reason to think that there are situations--e.g. method calls from Java--in which deftype would be expected to be significantly faster than defrecord? 

I can construct a minimal example based on my program and post it here, but I thought I'd check first whether there is something obvious that I don't get.

[The test I did using defrecord is below.  I also replaced 'defrecord' with 'deftype' and did the same test.  I would think that the JIT compiler wouldn't be smart enough to optimize away whatever differs between defrecord and deftype, but maybe I'm wrong about that.

(defrecord R [x y]
   P
   (incx2y [this] (reset! x (inc @y)))
   (decy2x [this] (reset! y (dec @x))))

(def r (R. (atom 2) (atom 1)))

(bench (def _ (dotimes [_ 50000000] (incx2y r) (decy2x r) r)))
]

Timothy Baldridge

unread,
Jun 2, 2015, 1:11:44 PM6/2/15
to clo...@googlegroups.com
Are you testing this inside of a lein repl (or any repl for that matter)? If so, compile it to a jar and run the jar directly via java -server -jar jarfile.jar. This could change your numbers a bit.

Timothy

--
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clo...@googlegroups.com
Note that posts from new members are moderated - please be patient with your first post.
To unsubscribe from this group, send email to
clojure+u...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en
---
You received this message because you are subscribed to the Google Groups "Clojure" group.
To unsubscribe from this group and stop receiving emails from it, send an email to clojure+u...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.



--
“One of the main causes of the fall of the Roman Empire was that–lacking zero–they had no way to indicate successful termination of their C programs.”
(Robert Firth)

Steven Yi

unread,
Jun 2, 2015, 9:10:04 PM6/2/15
to clo...@googlegroups.com
I'm not aware of anything that would lead to significant differences in regards to the function call.  As a test, I defined an interface and used deftype and defrecord, then used no.disassemble[1] to inspect the results, using the following commands:

user=> (use 'no.disassemble)

user=> (definterface P (testfn []))

user=> (defrecord R1 [x y] P (testfn [this] (reset! x (inc @y)) (reset! y (inc @x))))

user=> (deftype R2 [x y] P (testfn [this] (reset! x (inc @y)) (reset! y (inc @x))))

user=> (println (disassemble R1))

user=> (println (disassemble R2))

Looking at the bytecode for the testfn() function for both R1 and R2, they're pretty much identical (same opcodes). 

My hunch is that your performance issues may lie elsewhere. 

Just a random guess, are you by chance doing anything with the record/type that would require calling the equals() or hashCode() functions?  defrecord would be doing a bit more work there than deftype, which gets the default equals/hashcode.  

Hope that helps!
steven


Mars0i

unread,
Jun 2, 2015, 11:02:03 PM6/2/15
to clo...@googlegroups.com
Thanks Timothy.  I had been testing the simple defrecord and deftype definitions in the repl, but tried it with 'java -server' and still got roughly equal times.  I was testing the full program with 'lein run', but just to be sure tried making a jar and running it with 'java -server'.  The deftype version was still about 3X faster than the defrecord version.

Steven, thanks--that's helpful.  I knew that was possible to disassemble compiled bytecode, but didn't think I would know enough to understand it.  I guess that seeing that the code is almost identical would be informative even without understanding it.  Thanks for checking (and telling me how to do it).

*I'm* not using testing for equality on records/types, or doing anything that I'd expect to use hashCode(), but I'm not sure what the Java library code is doing.   That's possible.  I'll look into it, and I can ask the library designers/maintainers if I have trouble figuring it out.

Thank you!  Great.

Marshall

Mars0i

unread,
Jun 2, 2015, 11:28:24 PM6/2/15
to clo...@googlegroups.com
Steven--Oops, yeah, I am calling Java methods that are probably using hashCode(), and maybe equals().  Interesting. 

Is the idea that records are hashed on the values in their fields, while deftype object are just hashed a pointer (or whatever identical? uses)--something like that?  So if you want fast hashing by raw identity, you should not use defrecord?

Andy Fingerhut

unread,
Jun 2, 2015, 11:55:37 PM6/2/15
to clo...@googlegroups.com
I am not sure if this is the root cause of the performance difference you are seeing, but Clojure records do not cache their hash values: http://dev.clojure.org/jira/browse/CLJ-1224

I don't know what the default .hashCode method is for deftypes that do not specify one -- perhaps it is very fast because it is based upon object identity?

Andy

Steven Yi

unread,
Jun 3, 2015, 12:16:49 AM6/3/15
to clo...@googlegroups.com
Hi Marshall,

Yes, that's the gist of it. If you look at the bytecode for the
defrecord, you should see something like:

// Method descriptor #287 ()I

// Stack: 1, Locals: 1

public int hashCode();
0 aload_0 [this]
1 checkcast clojure.lang.IPersistentMap [18]
4 invokestatic
clojure.lang.APersistentMap.mapHash(clojure.lang.IPersistentMap) : int
[303]
7 ireturn
Line numbers:
[pc: 0, line: 1]
[pc: 4, line: 1]
Local variable table:
[pc: 0, pc: 7] local: this index: 0 type: user.R1

// Method descriptor #305 (Ljava/lang/Object;)Z
// Stack: 3, Locals: 2

public boolean equals(java.lang.Object G__1432);
0 aload_0 [this]
1 checkcast clojure.lang.IPersistentMap [18]
4 aload_1 [G__1432]
5 aconst_null
6 astore_1 [G__1432]
7 invokestatic
clojure.lang.APersistentMap.mapEquals(clojure.lang.IPersistentMap,
java.lang.Object) : boolean [309]
10 ireturn

which calls the code at [1] and [2]. The bytecode I saw for deftype
did not override either, so you should get the default from Object
[3][4], which just does comparison by identity for equals, and the
default hashing method. (Uses a native code call in what I'm seeing
for java.lang.Object's Java source via Netbeans, and javadocs say it
hashes the pointer address as you mentioned).

I'd say your assessment about hashing/identity and performance is
correct in regards to defrecord/deftype. On the other hand, it's good
to have correct value-based hashing out of the box with defrecord,
IMO. I guess the choice to use one or the other should keep the
equals/hashCode stuff in mind, and whether you need things to be
value-based or not.

As a side note, as an experiment I just tried overriding Object.equals
and Object.hashCode with a defrecord, and I got a compiler error:

CompilerException java.lang.ClassFormatError: Duplicate method
name&signature in class file user/R1,
compiling:(/private/var/folders/0k/xj_drd990xxf4q99n2bdknrc0000gn/T/form-init3397614882621384237.clj:1:1)

I'm assuming that defrecord is adding its hashCode and equals
implementations without checking if it's being overridden, but haven't
verified. (That was with 1.7.0-beta3 at least).

Cheers!
steven




[1] - https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/APersistentMap.java#L103-L112
[2] - https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/APersistentMap.java#L52-L71
[3] - http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#hashCode()
[4] - http://docs.oracle.com/javase/7/docs/api/java/lang/Object.html#equals(java.lang.Object)
> --
> You received this message because you are subscribed to the Google
> Groups "Clojure" group.
> To post to this group, send email to clo...@googlegroups.com
> Note that posts from new members are moderated - please be patient with your
> first post.
> To unsubscribe from this group, send email to
> clojure+u...@googlegroups.com
> For more options, visit this group at
> http://groups.google.com/group/clojure?hl=en
> ---
> You received this message because you are subscribed to a topic in the
> Google Groups "Clojure" group.
> To unsubscribe from this topic, visit
> https://groups.google.com/d/topic/clojure/EdjnSxRkOPk/unsubscribe.
> To unsubscribe from this group and all its topics, send an email to

Mars0i

unread,
Jun 3, 2015, 12:51:12 AM6/3/15
to clo...@googlegroups.com
Thanks Steven, Andy.

I feel much better knowing now why deftype and defrecord have different performance.  I felt that I should prefer defrecord because it's the default, and just in case I needed its extra features.  Now I can feel good about using deftype as long as I'm clear about what the tradeoffs are.  In fact, in the applications I'm planning now, I can't imagine that I'll need value-based hashing for these objects.  On the other hand, it might be useful to have the ability to use other defrecord features such as map->record, but now I can make an intelligent choice about it.

Mars0i

unread,
Jun 3, 2015, 11:16:19 PM6/3/15
to clo...@googlegroups.com


On Tuesday, June 2, 2015 at 11:16:49 PM UTC-5, Steven Yi wrote:
As a side note, as an experiment I just tried overriding Object.equals
and Object.hashCode with a defrecord, and I got a compiler error:

CompilerException java.lang.ClassFormatError: Duplicate method
name&signature in class file user/R1,
compiling:(/private/var/folders/0k/xj_drd990xxf4q99n2bdknrc0000gn/T/form-init3397614882621384237.clj:1:1)

I'm assuming that defrecord is adding its hashCode and equals
implementations without checking if it's being overridden, but haven't
verified. (That was with 1.7.0-beta3 at least).

I just understood why you tried this experiment, I think.  I tried it in 1.6.0 and 1.7.0-RC1 and got the same result.  For my purposes, it would be ideal if I could override hashCode so that it was based on identity.  I'd get speed with Java hashmaps along with the other conveniences of records.  Oh well.  Perhaps overriding hash behavior seems too bug-prone to be allowed.

The docstring for defrecord mentions explicitly that it defines hashCode and equals.  It seems as if there's a tiny inconsistency in the documentation given that it says that "You can also define overrides for methods of Object", though it doesn't say that you can do this for all of Object's methods; perhaps strictly speaking it's not inconsistent.

Steven Yi

unread,
Jun 4, 2015, 4:23:26 PM6/4/15
to clo...@googlegroups.com
I gave overriding of equals/hashCode a try with records just as a path
to test against the deftype performance. I guess with records, they're
meant to be treated like immutable value types, so identity based
hashCodes would sort of go against the spirit of that.
Reply all
Reply to author
Forward
0 new messages