article banner (priority)

Effective Kotlin Item 55: Consider Arrays with primitives for performance-critical processing

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

We cannot declare primitives in Kotlin, but they are used as an optimization under the hood. This is a significant optimization, as described already in Item 45: Avoid unnecessary object creation. Primitives are:

  • Lighter, as every object adds additional weight,
  • Faster, as accessing a value through accessors is an additional cost.

Therefore, using primitives for a huge amount of data might be a significant optimization. One problem is that typical Kotlin collections, like List or Set, are generic. Primitives cannot be used as generic types, and so we end up using wrapped types instead. This is a convenient solution that suits most cases as it is easier to do processing over standard collections. Having said that, in performance-critical parts of our code we should instead consider using arrays with primitives, like IntArray or LongArray, as they are lighter in terms of memory and their processing is more efficient.

Kotlin typeJava type
Intint
ListList
ArrayInteger[]
IntArrayint[]

How much lighter are arrays with primitives? Let’s say that in Kotlin/JVM we need to hold 1,000,000 integers, and we can choose to keep them either in IntArray or in List<Int>. If you make measurements, you will find that IntArray allocates 4,000,016 bytes, while List<Int> allocates 20,000,040 bytes, which is 5 times more. If it is possible to optimize for memory use, choose arrays with primitives.

import jdk.nashorn.internal.ir.debug.ObjectSizeCalculator .getObjectSize fun main() { val ints = List(1_000_000) { it } val array: Array<Int> = ints.toTypedArray() val intArray: IntArray = ints.toIntArray() println(getObjectSize(ints)) // 20 000 040 println(getObjectSize(array)) // 20 000 016 println(getObjectSize(intArray)) // 4 000 016 }

There is also a difference in performance. For the same collection of 1,000,000 numbers, calculating an average of those elements is around 25% faster when we use an array with primitives instead of a list with wrapped integers.

open class InlineFilterBenchmark { lateinit var list: List<Int> lateinit var array: IntArray @Setup fun init() { list = List(1_000_000) { it } array = IntArray(1_000_000) { it } } @Benchmark // On average 1 260 593 ns fun averageOnIntList(): Double { return list.average() } @Benchmark // On average 868 509 ns fun averageOnIntArray(): Double { return array.average() } }

As you can see, primitives and arrays with primitives can be used as an optimization in performance-critical parts of your code. They allocate less memory, and their processing is faster. However, the improvement in most cases is not significant enough to use arrays with primitives by default instead of lists. Lists are more intuitive and we use them much more often, so in most cases we should prefer them instead. Just keep this optimization in mind, in case you need to optimize some performance-critical parts of code.

Summary

In a typical case, List or Set should be preferred over Array. However, if you hold big collections of primitives, using Array might significantly improve your performance and memory use. This item is especially important for library creators or developers who write games or do advanced graphic processing.

Item 56: Consider using mutable collections

The biggest advantage of using mutable collections instead of immutable collections is that they are faster in terms of performance. When we add an element to an immutable collection, we need to create a new collection and add all elements to it. Here is how this is currently implemented in the Kotlin stdlib (Kotlin 1.2):

operator fun <T> Iterable<T>.plus(element: T): List<T> { if (this is Collection) return this.plus(element) val result = ArrayList<T>() result.addAll(this) result.add(element) return result }

When we deal with bigger collections, adding multiple elements to another collection can be a costly process. This is why using mutable collections, especially if we often need to add elements, is a performance optimization. On the other hand, Item 1: Limit mutability taught us the advantages of using immutable collections for safety. Notice that these arguments rarely apply to local variables, where synchronization or encapsulation is rarely needed. This is why, for local processing, it generally makes more sense to use mutable collections. This is reflected in the standard library, where all collection processing functions are internally implemented using mutable collections:

inline fun <T, R> Iterable<T>.map( transform: (T) -> R ): List<R> { val size = if (this is Collection<*>) this.size else 10 val destination = ArrayList<R>(size) for (item in this) destination.add(transform(item)) return destination }

Instead of using immutable collections:

// This is not how map is implemented inline fun <T, R> Iterable<T>.map( transform: (T) -> R ): List<R> { var destination = listOf<R>() for (item in this) destination += transform(item) return destination }

Summary

Adding mutable collections to elements is generally faster, but immutable collections give us more control over how they are changed. However,in the local scope we generally do not need this control, so mutable collections should be preferred, especially in utils, where element insertion might happen many times.