Chapter 7: Make it cheap
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Item 47: Avoid unnecessary object creation
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 literal1:
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
Integerinstead ofintunder the hood. When we useInt, it is generally compiled to the primitiveint, but if we make it nullable or when we use it as a type argument,Integeris used instead. This is because a primitive cannot benulland 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.
Intas 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 space2. 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 type | Java type |
|---|---|
| Int | int |
| Int? | Integer |
| List<Int> | List<Integer> |
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(
val afterHours: SessionDetails,
val preMarket: SessionDetails,
val 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.
Item 48: Consider using object declarations
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Summary
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Item 49: Use caching when possible
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Summary
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Item 50: Extract objects that can be reused
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Lazy initialization
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Summary
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Item 51: Use the inline modifier for functions with parameters of functional types
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
A type argument can be reified
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Functions with functional parameters are faster when they are inlined
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Non-local return is allowed
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Costs of inline modifiers
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Crossinline and noinline
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Summary
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Item 52: Consider using inline value classes
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Indicate unit of measure
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Protect us from value misuse
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Optimize for memory usage
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Inline value classes and interfaces
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Typealias
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Summary
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.
Item 53: Eliminate obsolete object references
This content is not available in the sample book. The book can be purchased on Leanpub at http://leanpub.com/effectivekotlin.