Scala: The Cost of Laziness
Considering using ''lazy'' in Scala? Unsure if doing so will cost you efficiency? If so, it's important to look at what's under the hood of the ''lazy'' keyword.
Join the DZone community and get the full member experience.
Join For FreeHave you ever wondered about the performance impact of using a 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.
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:
@State(Scope.Benchmark)
classLazyValCounterProvider {
lazy val counter=SlowInitializer.createCounter()
}
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 Scala 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):
@State(Scope.Benchmark)
@ScalaSignature(bytes="...")
publicclassLazyValCounterProvider {
private SlowInitializer.Counter counter;
private volatile boolean bitmap$0;
privateSlowInitializer.Counter counter$lzycompute() {
synchronized(this) {
if(!this.bitmap$0) {
this.counter = SlowInitializer.createCounter();
this.bitmap$0 = true;
return this.counter;
}
}
}
public SlowInitializer.Counter counter() {
return this.bitmap$0 ? this.counter : counter$lzycompute();
}
}
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:
public class LazyValsBenchmarks {
@Benchmark
public long baseline(ValCounterProvider eagerProvider) {
return eagerProvider.counter().incrementAndGet();
}
@Benchmark
public long lazyValCounter(LazyValCounterProvider lazyProvider) {
return lazyProvider.counter().incrementAndGet();
}
}
In a baseline method, I measure the thoughput of accessing a final counter object and incrementing an integer value by 1. As we’ve just found out, the main benchmark method – lazyValCounter – also does one volatile read in addition to what the baseline method does.
Note: All measurements are performed on MBA with Core i5 1.7GHz CPU.

Benchmark results
All results are obtained with JMH running in throughput mode measuring op(s). Each JMH session contained 10 samples and lasted for 50 seconds. I performed six measurements with the following options:
Exp # | JVM Mode | Threads # | Benchmark | Score | Score error |
---|---|---|---|---|---|
1 | Client VM | 1 | baseline | 412277751.619 | 8116731.382 |
1 | Client VM | 1 | lazyValCounter | 352209296.485 | 6695318.185 |
2 | Client VM | 2 | baseline | 542605885.932 | 15340285.497 |
2 | Client VM | 2 | lazyValCounter | 383013643.710 | 53639006.105 |
3 | Client VM | 4 | baseline | 551105008.767 | 5085834.663 |
3 | Client VM | 4 | lazyValCounter | 394175424.898 | 3890422.327 |
4 | Server VM | 1 | baseline | 407010942.139 | 9004641.910 |
4 | Server VM | 1 | lazyValCounter | 341478430.115 | 18183144.277 |
5 | Server VM | 2 | baseline | 531472448.578 | 22779859.685 |
5 | Server VM | 2 | lazyValCounter | 428898429.124 | 24720626.198 |
6 | Server VM | 4 | baseline | 549568334.970 | 12690164.639 |
6 | Server VM | 4 | lazyValCounter | 374460712.017 | 17742852.788 |
The benchmark results show that the lazy 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 on the subject, I recommend SIP 20 – Improved Lazy Vals Initialization. It contains an interesting in-depth analysis of existing issues with lazy initialization implementation in Scala.
Published at DZone with permission of Roman Gorodyshcher, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments