Have you ever wondered what’s the performance impact of using lazy keyword in Scala? Recently I had a dispute with my colleagues about it. It resulted in a set of microbenchmarks with quite interesting results which I share in this article.

But before going to the benchmark results let’s try to understand what can cause any performance penalty. For my JMH benchmark I created a very simple Scala class with a single lazy value in it:

Now let’s take a look at what is hidden under the hood of the lazy keyword. At first, we need to compile given code with scalac , and then it can be decompiled to correspondent Java code. I used JD decompiler and here is the output (I skip @ScalaSignature’s content for the sake of readability):

As it’s seen, the lazy keyword is translated to a classical double-checked locking idiom for delayed initialization. Thus, most of the time the only performance penalty may come from a single volatile read per lazy val read (except for the time it takes to initialize lazy val instance since its very first interaction). Let’s finally measure its impact in numbers.

My very simple JMH-based microbenchmark is shown below:

In a baseline method I measure thoughput of accessing a final counter object and incrementing an integer value by 1. And as we’ve just found out, the main benchmark method – lazyValCounter – also does one volatile read in addition to what baseline method does.
Note: all measurements are performed on MBA with Core i5 1.7GHz CPU.

Benchmark results

Benchmark results

All results are obtained with JMH running in throughput mode measuring ops/s . Each JMH session contained 10 samples and lasted for 50 seconds. I performed 6 measurements with the following options:

Exp #JVM ModeThreads #BenchmarkScoreScore error
1Client VM1baseline412277751.6198116731.382
1Client VM1lazyValCounter352209296.4856695318.185
2Client VM2baseline542605885.93215340285.497
2Client VM2lazyValCounter383013643.71053639006.105
3Client VM4baseline551105008.7675085834.663
3Client VM4lazyValCounter394175424.8983890422.327
4Server VM1baseline407010942.1399004641.910
4Server VM1lazyValCounter341478430.11518183144.277
5Server VM2baseline531472448.57822779859.685
5Server VM2lazyValCounter428898429.12424720626.198
6Server VM4baseline549568334.97012690164.639
6Server VM4lazyValCounter374460712.01717742852.788

The benchmark results show that lazy values performance penalty is quite small and can be ignored in most cases. Roughly speaking, the overhead is comparable with compound assignment operation time.

For further reading about the subject I would recommend SIP 20 – Improved Lazy Vals Initialization. It contains very interesting in-depth analysis of existing issues with lazy initialization implementation in Scala.

Did you like the post? Please spread the word 🙂