article banner

Effective Kotlin Item 47: Avoid unnecessary object creation

This is a chapter from the book Effective Kotlin. You can find it on LeanPub or Amazon.

Object creation always costs something and can sometimes be expensive. This is why avoiding unnecessary object creation can be an important optimization. It can be done on many levels. For instance, in JVM it is guaranteed that a string object will be reused by other code running in the same virtual machine that happens to contain the same string literal2:

val str1 = "Lorem ipsum dolor sit amet" val str2 = "Lorem ipsum dolor sit amet" print(str1 == str2) // true print(str1 === str2) // true

Boxed primitives (Integer, Long) are also reused in JVM when they are small (by default, the Integer Cache holds numbers in the range from -128 to 127).

val i1: Int? = 1 val i2: Int? = 1 print(i1 == i2) // true print(i1 === i2) // true, because i2 was taken from cache

Reference equality (===) shows that this is the same object. However, if we use a number that is either smaller than -128 or bigger than 127, different objects will be created:

val j1: Int? = 1234 val j2: Int? = 1234 print(j1 == j2) // true print(j1 === j2) // false

Notice that a nullable type is used to force Integer instead of int under the hood. When we use Int, it is generally compiled to the primitive int, but if we make it nullable or when we use it as a type argument, Integer is used instead. This is because a primitive cannot be null and cannot be used as a type argument.

Knowing that such mechanisms are available in Kotlin, you might wonder how significant they are. Is object creation expensive?

Is object creation expensive?

Wrapping something into an object has 3 costs:

  • Objects take additional space. In a modern 64-bit JDK, an object has a 12-byte header that is padded to a multiple of 8 bytes, so the minimum object size is 16 bytes. For 32-bit JVMs, the overhead is 8 bytes. Additionally, object references also take space. Typically, references are 4 bytes on 32-bit or 64-bit platforms up to -Xmx32G, and they are 8 bytes for memory allocation pool set above 32Gb (-Xmx32G). These are relatively small numbers, but they can add up to a significant cost. When we think about small elements like integers, they make a difference. Int as a primitive fit in 4 bytes, but when it is a wrapped type on the 64-bit JDK we mainly use today, it requires 16 bytes (it fits in the 4 bytes after the header), and its reference requires 4 or 8 bytes. In the end, it takes 5 or 6 times more space1. This is why an array of primitive integers (IntArray) takes 5 times less space than an array of wrapped integers (Array<Int>), as explained in the Item 58: Consider Arrays with primitives for performance-critical processing.

  • Access requires an additional function call when elements are encapsulated. Again, this is a small cost as function use is very fast, but it can add up when we need to operate on a huge pool of objects. We will see how this cost can be eliminated in Item 51: Use the inline modifier for functions with parameters of functional types and Item 49: Consider using inline classes.

  • Objects need to be created and allocated in memory, references need to be created, etc. These are small numbers, but they can rapidly accumulate when there are many objects. In the snippet below, you can see the cost of object creation.

class A private val a = A() // Benchmark result: 2.698 ns/op fun accessA(blackhole: Blackhole) { blackhole.consume(a) } // Benchmark result: 3.814 ns/op fun createA(blackhole: Blackhole) { blackhole.consume(A()) } // Benchmark result: 3828.540 ns/op fun createListAccessA(blackhole: Blackhole) { blackhole.consume(List(1000) { a }) } // Benchmark result: 5322.857 ns/op fun createListCreateA(blackhole: Blackhole) { blackhole.consume(List(1000) { A() }) }

By eliminating objects, we can avoid all three of these costs. By reusing objects, we can eliminate the first and the third ones. If we know the costs of objects, we can start considering how we can minimize these costs in our applications by limiting the number of unnecessary objects. In the next few items, we will see different ways to eliminate or reduce the number of objects. In this item, I will only present one technique, that is designing classes to use primitives instead of wrapped types.

Using primitives

In JVM, we have a special built-in type to represent basic elements like numbers or characters. These are called primitives and are used by the Kotlin/JVM compiler under the hood wherever possible. However, there are some cases where a wrapped class (an object instance containing a primitive) needs to be used instead. The two main cases are:

  • When we operate on a nullable type (primitives cannot be null).
  • When we use a type as a generic type argument.

So, in short:

Kotlin typeJava type
Intint
Int?Integer
ListList

Now you know that you can optimize your code to have primitives under the hood instead of wrapped types. Such optimization makes sense mainly on Kotlin/JVM and on some flavors of Kotlin/Native, but it doesn't make any sense on Kotlin/JS. Access to both primitive and wrapped types is relatively fast compared to other operations. The difference manifests itself when we deal with bigger collections (we will discuss this in Item 58: Consider Arrays with primitives for performance-critical processing) or when we operate on an object intensively. Also, remember that forced changes might lead to less-readable code. This is why I suggest this optimization only for performance-critical parts of code and in libraries. You can identify the performance-critical parts of your code using a profiler.

To consider a concrete example, let’s imagine that you implement a financial application in which you need to represent a stock snapshot. A snapshot is a set of values that are updated twice a second. It contains the following information:

class Snapshot( var afterHours: SessionDetails, var preMarket: SessionDetails, var regularHours: SessionDetails, ) data class SessionDetails( val open: Double? = null, val high: Double? = null, val low: Double? = null, val close: Double? = null, val volume: Long? = null, val dollarVolume: Double? = null, val trades: Int? = null, val last: Double? = null, val time: Int? = null, )

Since you are tracking tens of thousands of stocks, and the snapshot for each of them is updated twice a second, your application will create instances of SessionDetails many times per second, which will require a lot of effort from the garbage collector. To avoid this, you can change the SessionDetails class to use primitives instead of wrapped types by eliminating nullability.

data class SessionDetails( val open: Double = Double.NaN, val high: Double = Double.NaN, val low: Double = Double.NaN, val close: Double = Double.NaN, val volume: Long = -1L, val dollarVolume: Double = Double.NaN, val trades: Int = -1, val last: Double = Double.NaN, val time: Int = -1, )

Note that this change harms readability and makes this class harder to use because null is a better way to represent the lack of a value than a special value like NAN or -1. However, in this case we decided to make this change because we are dealing with a performance-critical part of the application. By eliminating nullability, we’ve made our object allocate far fewer objects and much less memory. On a typical machine, the first version of SessionDetails allocates 192 bytes and needs to create 10 objects; in contrast, the second version allocates only 80 bytes and needs to create only one object. This is a significant difference that might be worth the trouble when we are dealing with tens of thousands of objects.

If such interventions are not enough in your application, you can consider using a very powerful but also very dangerous pattern object pool. Its core idea is to make objects mutable and to store and reuse unused objects. This pattern is hard to implement correctly, and it is easy to introduce synchronization issues, which is why I don’t recommend using it unless you’re sure that you need it.

Summary

In this chapter, we learned about the costs of object creation and allocation. We also learned that we can reduce these costs by eliminating objects or reusing them, or by designing our objects to use primitives. The next items present other ways to reduce the number of unnecessary objects in our applications.

1:

To measure the size of concrete fields in JVM objects, use Java Object Layout.

2:

Java Language Specification, Java SE 8 edition, 3.10.5