Skip to content
cruftex.net
Go back

Java Caching Benchmarks 2016 - Part 1

Looking around, my benchmarks comparing several Java caches, like Guava, EHCache, Infinispan and cache2k are still the most comprehensive ones you can find focusing on Java heap only cache performance. But it’s two years since I published them. There are new products and better ways to do benchmarks now. So, it’s time for an update!

This post is about some fundamental topics and starts with a first set of benchmarks that compare fast in heap caches with Java’s ConcurrentHashMap. Why this? Well, read on…

The Flaw

Of course, there will never be a benchmark without any flaws. But my previous benchmarks have a major flaw: Most of them are only single threaded. This has two effects: The real world throughput of concurrent applications may be different. Furthermore, implementations can “cheat” the benchmark by utilising more threads to do more work in less time.

A second thing: The benchmark is only using integer keys and values. Although this is a flaw, too, for now we continue with this. Testing with different types will be more interesting in case serialisation comes into play. We will start looking into this when starting benchmarks for off heap or persistence features.

Switching to JMH

JMH has become the de facto standard to do (micro) benchmarks on the JVM. The previous benchmarks had loops to repeat the operations, let’s say, 100000 times. With JMH, that is all gone. We can just say: ‘Dear JMH, measure the throughput in operations per second and use a benchmark duration of 10 seconds.’ JMH also makes sure that the operations are not just optimised away by the Java compiler, which is a typical source of error.

Comparing Apples and Bananas

But now, back to the topic, the benchmarks themselves. For the first set of benchmarks I decided to do only scenarios without the need for eviction. That means the whole key space of the benchmark always fits into the cache. What? “No eviction, but isn’t that the essence of caching?”, you might say. That is true, but for the first batch of benchmarks I would like to be able to compare against the performance of ConcurrentHashMap.

Why compare against the performance of a map? First of all, the performance of ConcurrentHashMap is a benchmark in the real sense of the word. Smart guys, knowing lots about concurrency, the JVM and modern computer architecture, spent some effort to get it fast. So why not learn from it by comparing against it? The second reason is typical software engineer thinking: “Do I need a full fledged cache or can I go faster and just use a HashMap or a ConcurrentHashMap?”

So, it is not like comparing apples and bananas, but like comparing a Swiss army knife with a knife. For some scenarios the simple knife will do the job. Sometimes you need a few features that you get from a Swiss army knife, but you fear it is too heavy. Knowing the performance difference is critical information.

Test Setup

For reproduction here is the setup. For the moment I just use my notebook for benchmarking. The hardware may change in the future to a more stable and server grade setup.

All benchmarks run with integers as keys and values.

Compared Caches and Versions

For our first benchmark run we use these “cache” implementations:

EHCache, which is a very popular Java (in heap) cache, is missing at the moment. We will add it in a later update.

Environment

Note: The asymmetrical RAM configuration of the test machine is not desirable for benchmarking, we need to use other hardware in the future.

JMH Configuration

Benchmark 1: Populate Parallel Once

The first benchmark is about inserting entries into the cache. The run variants are: The number of entries inserted and the number of parallel threads used for insertion. The run mode is “one shot”, which means each run is doing the task once. The result is the runtime in seconds.

CHM = ConcurrentHashMap, no eviction, as reference.CHMcache2kCaffeineGuava1M entries2M entries4M entries8M entries01 s2 s3 s4 s5 s6 s7 s8 sruntime in secondscache2k · 1M entries: 0.148 sCaffeine · 1M entries: 0.211 sGuava · 1M entries: 0.274 scache2k · 2M entries: 0.274 sCaffeine · 2M entries: 0.541 sGuava · 2M entries: 0.575 scache2k · 4M entries: 0.699 sCaffeine · 4M entries: 1.214 sGuava · 4M entries: 1.814 scache2k · 8M entries: 4.265 sCaffeine · 8M entries: 5.27 sGuava · 8M entries: 7.446 sCHM · 1M entries: 0.095 sCHM · 2M entries: 0.178 sCHM · 4M entries: 0.531 sCHM · 8M entries: 1.28 s

CHM = ConcurrentHashMap, no eviction, as reference.

Populate Parallel Once benchmark result for one thread.
CHM = ConcurrentHashMap, no eviction, as reference.CHMcache2kCaffeineGuava1M entries2M entries4M entries8M entries01 s2 s3 s4 s5 s6 s7 s8 s9 sruntime in secondscache2k · 1M entries: 0.306 sCaffeine · 1M entries: 0.171 sGuava · 1M entries: 0.417 scache2k · 2M entries: 0.734 sCaffeine · 2M entries: 0.481 sGuava · 2M entries: 0.926 scache2k · 4M entries: 1.455 sCaffeine · 4M entries: 1.292 sGuava · 4M entries: 2.26 scache2k · 8M entries: 5.916 sCaffeine · 8M entries: 5.155 sGuava · 8M entries: 8.061 sCHM · 1M entries: 0.055 sCHM · 2M entries: 0.105 sCHM · 4M entries: 0.308 sCHM · 8M entries: 1.074 s

CHM = ConcurrentHashMap, no eviction, as reference.

Populate Parallel Once benchmark result for two threads.
CHM = ConcurrentHashMap, no eviction, as reference.CHMcache2kCaffeineGuava1M entries2M entries4M entries8M entries01 s2 s3 s4 s5 s6 s7 s8 sruntime in secondscache2k · 1M entries: 0.265 sCaffeine · 1M entries: 0.12 sGuava · 1M entries: 0.357 scache2k · 2M entries: 0.513 sCaffeine · 2M entries: 0.335 sGuava · 2M entries: 0.887 scache2k · 4M entries: 1.509 sCaffeine · 4M entries: 1.268 sGuava · 4M entries: 2.17 scache2k · 8M entries: 5.391 sCaffeine · 8M entries: NaN sGuava · 8M entries: 7.93 sCHM · 1M entries: 0.056 sCHM · 2M entries: 0.062 sCHM · 4M entries: 0.356 sCHM · 8M entries: 0.939 s

CHM = ConcurrentHashMap, no eviction, as reference.

Populate Parallel Once benchmark result for four threads.

Reference information

Notes

For the caches, cache2k does best on insert with a single thread. That is a little surprising, since Caffeine actually utilises more threads internally to do its work, while cache2k does not. For two threads the runtime of cache2k doubles, because there is lock contention and I did nothing to optimise for this scenario, but cache2k still performs better than Guava and only a little worse than Caffeine. Only Caffeine profits a little from the additional thread.

The four thread result is missing for Caffeine, since there was a VM crash with an out of memory problem on one test run.

Generally the four thread result isn’t much different from the two thread result, since it is only a 2 core machine. I expected a far better result for the hyperthreaded cores.

Methodology Critique

The idea of the benchmark is to get an isolated performance value for inserting entries into a cache. JMH does multiple iterations and starts with some warmup iterations before doing the iterations that count for the result. After each iteration the cache content is disposed and a new cache is created. That means the GC of the previous cache content will influence the performance result. For better isolation we should force the GC outside the measured time interval.

Benchmark 2: Memory Consumption

We use the benchmark above and measure the consumed Java heap space. After each benchmark iteration, a garbage collection is done and the used memory in the heap is recorded.

CHM = ConcurrentHashMap, no eviction, as reference.CHMcache2kCaffeineGuava1M entries2M entries4M entries8M entries0100 MB200 MB300 MB400 MB500 MB600 MB700 MB800 MB900 MB1000 MBheap memory in MBcache2k · 1M entries: 104.5 MBCaffeine · 1M entries: 119.2 MBGuava · 1M entries: 104.3 MBcache2k · 2M entries: 206.9 MBCaffeine · 2M entries: 234.6 MBGuava · 2M entries: 205.6 MBcache2k · 4M entries: 405.1 MBCaffeine · 4M entries: 463.8 MBGuava · 4M entries: 404.3 MBcache2k · 8M entries: 802.7 MBCaffeine · 8M entries: 924.4 MBGuava · 8M entries: 802.1 MBCHM · 1M entries: 73.8 MBCHM · 2M entries: 145.2 MBCHM · 4M entries: 282.3 MBCHM · 8M entries: 558.5 MB

CHM = ConcurrentHashMap, no eviction, as reference.

Heap memory consumption after inserting.

Reference information

Notes

cache2k and Guava have the lowest measured memory consumption. Of course CHM has a lower memory consumption because it is not a real cache and doesn’t need the extra overhead for it. Please keep in mind that this is the memory consumption for this specific benchmark only, you cannot say anything general yet. Things will look different if expiry is enabled or after some eviction took place.

Benchmark 3: Read Only

This benchmark populates the cache with 100k entries. The workload is accessing entries in a random pattern with different hit rates. The main goal is to check how different hit ratios influence the throughput.

CHM = ConcurrentHashMap, no eviction, as reference.CHMcache2kCaffeineGuava1 thread, 100 %1 thread, 33 %1 thread, 50 %2 threads, 100 %2 threads, 33 %2 threads, 50 %4 threads, 100 %4 threads, 33 %4 threads, 50 %010M20M30M40M50M60M70M80Moperations per secondcache2k · 1 thread, 100 %: 17.71M ops/sCaffeine · 1 thread, 100 %: 5.96M ops/sGuava · 1 thread, 100 %: 4.57M ops/scache2k · 1 thread, 33 %: 20.82M ops/sCaffeine · 1 thread, 33 %: 13.08M ops/sGuava · 1 thread, 33 %: 9.24M ops/scache2k · 1 thread, 50 %: 20.64M ops/sCaffeine · 1 thread, 50 %: 9.7M ops/sGuava · 1 thread, 50 %: 7.01M ops/scache2k · 2 threads, 100 %: 30.4M ops/sCaffeine · 2 threads, 100 %: 12.41M ops/sGuava · 2 threads, 100 %: 7.82M ops/scache2k · 2 threads, 33 %: 13.9M ops/sCaffeine · 2 threads, 33 %: 24.47M ops/sGuava · 2 threads, 33 %: 14.88M ops/scache2k · 2 threads, 50 %: 16.87M ops/sCaffeine · 2 threads, 50 %: 20.5M ops/sGuava · 2 threads, 50 %: 11.57M ops/scache2k · 4 threads, 100 %: 38.31M ops/sCaffeine · 4 threads, 100 %: 20.43M ops/sGuava · 4 threads, 100 %: 9.27M ops/scache2k · 4 threads, 33 %: 11.44M ops/sCaffeine · 4 threads, 33 %: 35.7M ops/sGuava · 4 threads, 33 %: 20.62M ops/scache2k · 4 threads, 50 %: 14.41M ops/sCaffeine · 4 threads, 50 %: 32.67M ops/sGuava · 4 threads, 50 %: 15.75M ops/sCHM · 1 thread, 100 %: 27.03M ops/sCHM · 1 thread, 33 %: 28.62M ops/sCHM · 1 thread, 50 %: 26.75M ops/sCHM · 2 threads, 100 %: 50.43M ops/sCHM · 2 threads, 33 %: 54.65M ops/sCHM · 2 threads, 50 %: 52.97M ops/sCHM · 4 threads, 100 %: 64.21M ops/sCHM · 4 threads, 33 %: 79.78M ops/sCHM · 4 threads, 50 %: 79.03M ops/s

CHM = ConcurrentHashMap, no eviction, as reference.

Read Only benchmark result for 1, 2 and 4 threads and 100, 50 and 33 percent hit ratio.

Reference information

Notes

Honestly, this is a good example of how not to do charts, since I squeezed three different aspects into it: Products, threads and the hit ratio.

Of course, CHM does best. For the caches, the single thread performance of cache2k is superior. For two threads Caffeine is better in case the hit rate is 50% or 33%. For all thread counts the cache2k throughput is better than half of CHM’s throughput for 100% hit rate.

Benchmark 4: Combined Read/Write

Three benchmarks: “ro” doing reads only in 8 threads, “rw” doing reads and writes in 6 and 2 threads, “wo” doing writes in 8 threads. The cache is populated in advance with the test data set. No eviction and no inserts happen during the benchmark time. The test data size is 11k, the cache size 32k. This benchmark is almost identical to the one in Caffeine.

CHM = ConcurrentHashMap, no eviction, as reference.CHMcache2kCaffeineGuavaread onlyread and writewrite only020M40M60M80M100M120M140M160Moperations per secondcache2k · read only: 112.02M ops/sCaffeine · read only: 93.6M ops/sGuava · read only: 12.97M ops/scache2k · read and write: 92.32M ops/sCaffeine · read and write: 81.71M ops/sGuava · read and write: 12.21M ops/scache2k · write only: 8.2M ops/sCaffeine · write only: 36.94M ops/sGuava · write only: 9.99M ops/sCHM · read only: 143.23M ops/sCHM · read and write: 121.68M ops/sCHM · write only: 36.1M ops/s

CHM = ConcurrentHashMap, no eviction, as reference.

Combined Read/Write.

Reference information

Notes

For read only and mostly read scenarios cache2k has the best performance. There is a big drop for concurrent writes to cache2k.

How to Reproduce

If you want to reproduce the values or do your own benchmarks on your favourite server hardware: Do it! You’ll find the current stuff at the cache2k-benchmark GitHub page

git clone https://github.com/headissue/cache2k-benchmark.git
git checkout 27634762258f2d4dd2abca81f5807ec9ecd73239
cd cache2k-benchmark

mvn -DskipTests package

# this runs the benchmark suite (takes at least 6 hours, for only a quick check use --quick)
bash jmh-run.sh

# plot the graphs
bash processJmhResult.sh process

The results will be written to: target/jmh-result.

The raw JSON data of this benchmark is here: data.json.gz.

Lessons Learned

Some final thoughts on what we already learned from the first batch of benchmarks.

Simple Benchmarks, but some Interesting Results

The benchmarks are not sufficient to get an idea of how real world applications will behave. But concentrating only on these few benchmarks led to some interesting insights. Learned: Concentrate on a few things, do it correctly and be surprised what you find with a “little” effort.

Lost in Variations

With all the variables we have now (cache vendors, cache sizes and threads) it is a total of 119 benchmark runs. Doing 5 warmup and 5 measurement iterations of 20 seconds each leads to a total runtime of 6 hours. Well, but this is only the beginning of some thorough cache benchmarks. We want to check more cache implementations, more benchmark scenarios, different VMs and VM parameters.

So we need to be very careful about what we want to benchmark. We will probably end up with benchmark times of several days.

Don’t do Benchmarks on Your Notebook

For a serious evaluation of how well a cache behaves in multi threaded contexts we need more cores. For stable benchmark hardware we should have at least 4 cores. Doing benchmarks about which cache will work better with hyperthreading is not really a worthy goal.

Threads, Cores and CPU Utilisation

I am still not happy with the measurement of different thread counts. Doing the benchmarks with different threads gives us an idea of how well a cache behaves for concurrent requests. But this can be misinterpreted. Since a cache may use additional threads to do its work, benchmarking with one thread doesn’t mean that only a single CPU core was utilised.

If we expect to know from a benchmark how fast something will be for a fixed set of resources, the benchmarks with varying threads are all wrong.

cache2k does well

Since I am the author of cache2k and the benchmarks, this won’t be a surprise for the reader. But it is a surprise to me. I didn’t select nice benchmarks for cache2k on purpose, nor did I tune cache2k for these benchmarks.

Next on this Channel

Stand by for some more updates in the next weeks, which will feature:

Join the Fun!

My own primary target is to see where cache2k can improve. But, OTOH, the benchmarks are useful for other caching vendors and caching users, too.

If you would like to contribute, you are welcome! There are a lot of tiny things that can be done:

Before starting work on a topic, please open and/or check the issue tracker at cache2k-benchmark.

Note

Update 2026: Typo and wording fixes, switch to UK English.



Previous Post
Jenkins: How to Analyse Your Plugin Usage
Next Post
cache2k - the high performance Java caching library is now Apache licensed!