Hibernate is faster than I thought it was.


I was playing around with a speed test of Gigaspaces XAP, and got some pretty good numbers out of it – but I couldn’t escape the thought that I was missing something. Then I had it: a comparison point!

If GigaSpaces is X, then… it’s X. But if we compare it to something else – a Y, if you will – then we get a better sense of whether it’s actually fast or not.

Timings capture only the time spent in the DAO, as captured by a dynamic proxy. Therefore, a given test might take four minutes, but yield only 60 seconds’ worth of measurements, because none of the non-DAO stuff was measured. The idea was to isolate any testing code away from the actual focus of the speed test, which was: “How fast do read/write/delete operations run under simple transactions, when connecting to external servers?”

The tests are serial – one operation at a time, over and over again. This is way not real-world. I know it, you should know it, if you take these benchmarks as anything other than a “hmm, that’s interesting” data point, a yeti will hunt you down and eat your toes.

So I created a simple 1:M mapping, an Owner to OwnerAttribute model, and created mappings for Hibernate and GigaSpaces. (Same object, just annotated for both GigaSpaces and Hibernate.) The test ran over four basic operations in bulk, using transactions on every operation, based around a common DAO interface.

When I first ran the tests, I was horrified: Hibernate was so freakin’ bleedin’ farglin’ bleepin’ slow SLOW SLOW! Amazingly slow. Horrifyingly slow. I isolated the cause to two reasons: session acquisition and object model. The session acquisition surprised me: I was acquiring one session and using it for all operations, which I thought would be faster.

After all, it takes time to acquire a session, does it not? Boy, was I wrong.

This is why you test your assumptions, people. Having the DAO get a new session from the factory every call sped things up dramatically. Score one for Hibernate, one point lost for me.

The object model was a stickier problem. The test creates a number of attributes for the Owner object and stores them as well.

When I had OwnerAttribute stored as an external record (@OneToMany, etc.) Hibernate crawled.

Crawled bad.

Crawled “Are you reusing the session, Joe?” bad.

So then I thought about the object model. In the real-world situation this models, I don’t ever search for owners by attributes; the actual attributes are more like function call parameters. So there was no reason to have them stored externally, in either the Hibernate or GigaSpaces model.

Therefore I stored them as a Serializable in the Owner class. This simplified the object model by a lot (the DAO for GigaSpaces would have had to compensate for an Owner being an aggregate) and also, well, sped things up for Hibernate.

The tradeoffs: I couldn’t search by attribute (again, not part of the real application), Hibernate sped up by leaps and bounds, and the GigaSpaces DAO implementation was far simpler than it would have been.

What of those is valuable to you is, well, up to you. Personally, I find the DAO implementation more expensive up front, and the speed benefits are worth it.

The tests themselves: 100000 writes, 100000 serial reads (in order by PK), 100000 random reads (Collections.shuffle() on the PKs), 100000 serial deletes (i.e., destructive reads), 100000 random deletes.

Hibernate did way better than I thought it would. Here’s one view of the data:

GigaSpaces Hibernate
Serial Writes 0.522ms 2.483ms
Serial Reads 0.238 0.668
Serial Deletes 0.613 2.652
Random Reads 0.247 0.854
Random Deletes 0.606 3.641

Now, Hibernate was talking to an external Derby instance; GigaSpaces was talking to an external GigaSpaces container. Some outliers on Hibernate slowed it down a little; the actual perceptive numbers are probably better than you see here.

But this is still really good. I’d expected Hibernate to be in the 6-8 millisecond range, and it was way faster than I thought.

I’m including the source code to the test, if you’re interested. Again, this data isn’t worth making a real conclusion about – you need to test your own object models – but it’s interesting.

Related posts:

  1. Commentary on the datastore benchmark
  2. Rocket Surgery: What is Inversion of Control?
  3. Java schools of thought?
  4. New Article: “Considering Datastores.”
  5. Modules aren’t expensive enough to avoid them.

, , , , ,

  1. #1 by i2matrix on 6 March, 2010 - 12:00 am

    good one :-)
    the session observation which you made is also surprising to me

  2. #2 by Artur Biesiadowski on 6 March, 2010 - 3:19 am

    When I looked at gigaspaces results I thought – not bad, not bad at all. Seems that they have improved something a lot. Only after a moment I realized it has to be per-item time, not per 100k time…

    I wonder what would be amortized time for batches of say 100-1000 items – we could estimate overhead of serial access in such case.

  3. #3 by None on 6 March, 2010 - 4:00 am

    I think you ran into the same problem I ran into at work with hibernate (was on a fairly old version of hibernate but anyway…). If you are doing a lot of inserts on the same hibernate session it actually starts off very fast and becomes slower and slower as it gets further into the insertion. This is because it’s caching what it inserts and on subsequent inserts it refreshes its cache. As you can imagine that really slows down insertion rate, so the trick is to either disable the cacheing entirely or call session.clear() after each insert.

  4. #4 by dreamreal on 6 March, 2010 - 5:51 am

    Well, this really isn’t the right way to do a few things:

    1) We’re using GigaSpaces as an external datastore. They have a mechanism by which they embed processing into the GigaSpaces container itself, which increases speed by a lot. Here, we’re seeing “wire time” except everything is on localhost; inter-process communication on localhost is fast, but not transparent. When I embedded the Gigaspace, it went faster. (Hmm, good idea…)
    2) Serialization is definitely factoring in. However, I don’t want to get rid of it because I am not trying to tune the serialization mechanism, but measure the actual time taken by the datastores.

    I’m adding to the speed test just because I’m interested; I’ll publish new (and improved) measurements as soon as I have them.

    [WORDPRESS HASHCASH] The poster sent us ’0 which is not a hashcash value.

  5. #5 by dreamreal on 6 March, 2010 - 9:26 am

    Artur Biesiadowski :

    When I looked at gigaspaces results I thought – not bad, not bad at all. Seems that they have improved something a lot. Only after a moment I realized it has to be per-item time, not per 100k time…

    I wonder what would be amortized time for batches of say 100-1000 items – we could estimate overhead of serial access in such case.

    Well, the serialization definitely factors in – I wasn’t testing raw throughput as much as i was testing throughput with this topology (all servers on localhost, etc) and this object design, and trying to get a sense of relative speed of the different mechanisms.

    Serialization is pretty easy to test – and it’s also fairly easy to eliminate in the storage mechanism. I just didn’t want to bother, and still don’t. Yet. :)

    I’ll have another round of the results soon (with another datastore, and with far better statistics gathering).

  6. #6 by Nicolas on 6 March, 2010 - 6:23 pm

    And using traditionnal RDBMS using JDBC for instance ?

    Or better performing the computations on a stored procedure in PLSQL ?

    I think your benchmark are interesting, but analysing another alternatives would be nice !

  7. #7 by Shay Hassidim on 6 March, 2010 - 7:48 pm

    Joe,
    The following will provide better numbers with GigaSpaces:
    - GigaspaceBaseDAO.read() and GigaspaceBaseDAO.delete() should use readbyId/takeById
    - read without using a transaction (optimistic locking)
    - Use local cache/view
    - Use Task Executors
    - Test with multi threaded client
    - Use batch operations when relevant
    - Index when relevant

    Shay

  8. #8 by dreamreal on 7 March, 2010 - 7:44 am

    Nicolas :

    And using traditionnal RDBMS using JDBC for instance ?

    Or better performing the computations on a stored procedure in PLSQL ?

    I think your benchmark are interesting, but analysing another alternatives would be nice !

    Sure, and I’m working on some others. I’m not likely to do PLSQL because I don’t feel like writing a bunch of stored procedures for simple CRUD operations. But who knows?

  9. #9 by dreamreal on 7 March, 2010 - 7:46 am

    Shay Hassidim :

    Joe,
    The following will provide better numbers with GigaSpaces:
    - GigaspaceBaseDAO.read() and GigaspaceBaseDAO.delete() should use readbyId/takeById
    - read without using a transaction (optimistic locking)
    - Use local cache/view
    - Use Task Executors
    - Test with multi threaded client
    - Use batch operations when relevant
    - Index when relevant

    Shay

    Shay, transactions are DEFINITELY part of what I wanted to include; not using them is not even a basis for consideration. Plus, I’m adding other bits to the test that don’t use the ID for searching.

    This is not a task-oriented thing; no executors. Also, the test specifically works sans multithreads. I’m not trying to see how fast I can run 10000 things; I’m trying to sample 10000 times for ONE operation.

    More is coming.

  10. #10 by Ashish on 8 March, 2010 - 3:40 am

    Interesting benchmarks. Worth investigating further.

  11. #11 by Steve Ebersole on 8 March, 2010 - 10:20 am

    The Session bit is not so surprising given the test code. The “pattern” used here initially was analogous to what we call session-per-application. Essentially the session cache (persistence context) kept growing and growing and growing..); the main issue with this is that flushing become cumbersome for the session because it needs to dirty check all those (now irrelevant) entities.

  12. #12 by Steve Ebersole on 8 March, 2010 - 10:24 am

    None :
    I think you ran into the same problem I ran into at work with hibernate (was on a fairly old version of hibernate but anyway…). If you are doing a lot of inserts on the same hibernate session it actually starts off very fast and becomes slower and slower as it gets further into the insertion. This is because it’s caching what it inserts and on subsequent inserts it refreshes its cache. As you can imagine that really slows down insertion rate, so the trick is to either disable the cacheing entirely or call session.clear() after each insert.

    This is actually not true, unless you are flushing after each insert. As I mentioned in previous comment it is the process of flushing where the number of things in the persistence context begins to adversely effect time to do the flush. But that is an anti-pattern for usage also.

  13. #13 by dreamreal on 9 March, 2010 - 10:46 am

    Steve Ebersole :

    The Session bit is not so surprising given the test code. The “pattern” used here initially was analogous to what we call session-per-application. Essentially the session cache (persistence context) kept growing and growing and growing..); the main issue with this is that flushing become cumbersome for the session because it needs to dirty check all those (now irrelevant) entities.

    Yeah, that initial run disappeared pretty quickly – I was sure SOMETHING was wrong. The next version won’t have the problem either.

  14. #14 by None on 10 March, 2010 - 6:11 pm

    Steve:
    “This is actually not true, unless you are flushing after each insert. As I mentioned in previous comment it is the process of flushing where the number of things in the persistence context begins to adversely effect time to do the flush. But that is an anti-pattern for usage also.”

    Yes we were flushing after each insert but that was necessary as hibernate had been plugged into an existing framework (in a large body of code) in such a way that we needed to catch insertion errors immediately so a decision could be made to continue with the rest or not based on business rules. Having a batch of 10 fail on a flush when only 1 would have failed and should be ignored anyway caused problems with existing application logic. Hence the flush on single inserts.

  15. #15 by Steve Ebersole on 12 March, 2010 - 4:48 pm

    None :
    Steve:
    “This is actually not true, unless you are flushing after each insert. As I mentioned in previous comment it is the process of flushing where the number of things in the persistence context begins to adversely effect time to do the flush. But that is an anti-pattern for usage also.”
    Yes we were flushing after each insert but that was necessary as hibernate had been plugged into an existing framework (in a large body of code) in such a way that we needed to catch insertion errors immediately so a decision could be made to continue with the rest or not based on business rules. Having a batch of 10 fail on a flush when only 1 would have failed and should be ignored anyway caused problems with existing application logic. Hence the flush on single inserts.

    My point is that you are misusing a tool and then blaming the tool when it does not work/perform well. Sounds more like you want a org.hibernate.StatelessSession or what the documentation terms “batch processing” http://docs.jboss.org/hibernate/core/3.3/reference/en-US/html/batch.html.

  16. #16 by artykuly z linkiem on 3 April, 2010 - 6:35 pm

    Interesting article i totally agree with the comments above. Keep us posting

(will not be published)

Powered by WP Hashcash


Rss Feed Tweeter button Linkedin button Digg button Stumbleupon button Youtube button