Exploring Kotlin’s hidden costs — Part 3

Delegated properties and ranges

Following the overwhelming positive feedback I received after publishing the first two parts of this series about the Kotlin programming language, including a mention by Jake Wharton himself, I’m happy to continue the investigation. Don’t miss part 1 and part 2.

In this part 3, we’ll reveal more secrets of the Kotlin compiler and provide new tips to write more efficient code.

Image for post
Image for post

Delegated properties

A delegated property is a property whose getter and optional setter implementations are provided by an external object called the delegate. This allows reusable custom property implementations.

The delegate object has to implement an operator getValue() function, as well as a setValue() function for read/write properties. These functions will receive as extra arguments the containing object instance as well as the property metadata (like its name).

When a class declares a delegated property, code matching the following Java representation is generated by the compiler:

Some static property metadata is added to the class. The delegate is initialized in the class constructor and is then invoked every time the property is read or written to.

In the above example, a new delegate instance is created to implement the property. This is required when the delegate implementation is stateful, for example if it caches the computed value of the property locally:

It’s also required to create a new delegate instance if it requires extra parameters, passed through its constructor:

But there are some cases where only a single delegate instance is required to implement any property: when the delegate is stateless and the only variables it needs to do its job are the containing object instance and the property name, which are already provided. In that case, you can make the delegate a singleton by declaring it as object instead of class.

For example, the following singleton delegate retrieves the Fragment whose tag name matches the property name inside an Android Activity:

Similarly, any existing object can be extended to become a delegate. getValue() and setValue() can also be declared as extension functions. Kotlin already provides built-in extension functions to allow using Map and MutableMap instances as delegates, using the property names as keys.

If you choose to reuse the same local delegate instance to implement multiple properties in the same class, you need to initialize this instance in the class constructor.

Note: since Kotlin 1.1, it’s also possible to declare a local variable in a function as a delegated property. In that case, the delegate can be initialized later, up to the point where the variable is declared in the function.

Each delegated property declared in a class also involves the overhead of its associated delegate object, and adds some metadata to the class.
Try to reuse delegates for different properties when it makes sense.
Also consider whether delegated properties are your best option in cases where you would need to declare a large number of them.

The delegate functions can be declared in a generic way, so the same delegate class can be used with various property types.

However, if you use a generic delegate with a primitive type property like in the above example, boxing and unboxing will occur each time the property is read or written to, even if the declared primitive type is non-null.

For delegated properties of non-null primitive types, prefer using specialized delegate classes created for that specific value type rather than a generic delegate, to avoid boxing overhead during each access to the property.

Kotlin provides a few standard delegates to cover common cases, like Delegates.notNull(), Delegates.observable() and lazy().

lazy() is a function returning a delegate for read-only properties that will evaluate the provided lambda to initialize the property value when first read.

This is a neat way to defer an expensive initialization until it’s actually needed, improving performance while keeping the code readable.

It should be noted that lazy() is not an inline function and the lambda passed as argument will be compiled to a separate Function class and will not be inlined inside the returned delegate object.

What is often overlooked is that lazy() has an optional mode argument to determine which of 3 different types of delegates will be returned:

The default mode, LazyThreadSafetyMode.SYNCHRONIZED, will perform a relatively expensive double-checked lock, which is required to guarantee that the initialization block will run safely once when the property can be read from multiple threads.

If you know that a property is only going to be accessed from a single thread (like the main thread), then locking can be avoided entirely by explicitly using LazyThreadSafetyMode.NONE instead:

Use the lazy() delegate to defer expensive initializations, and specify the thread safety mode to avoid locking when it’s not needed.

Ranges

Ranges are special expressions used to represent a finite set of values in Kotlin. The values can be of any Comparable type. These expressions are formed with functions which create objects implementing the ClosedRange interface. The main function used to create a range is the .. operator function.

The main purpose of a range expression is to write inclusion or exclusion tests using the in and !in operators.

The implementation is optimized for non-null primitive type ranges (bounded byInt, Long, Byte, Short, Float, Double or Char values), so that the above example effectively gets compiled to this:

There is zero overhead and no extra object allocation. Ranges can also be used with any other non-primitive Comparable type.

Before Kotlin 1.1.50, a temporary ClosedRange object was always created when compiling the above example. But since 1.1.50, the implementation has been optimized to avoid allocations for Comparable types as well:

As a bonus, range tests can be included in a when expression.

This makes the code more readable than a series of if{...} else if{...} statements and is just as efficient.

However, ranges have a small cost when there is at least one level of indirection between their declaration and their usage in an inclusion test. Consider the following Kotlin code:

It involves the extra creation of an IntRange object after compilation:

Declaring the property getter as an inline function does not prevent the creation of this object either. This is a case where the Kotlin 1.1 compiler could be improved. At least, no boxing is involved to compare primitive types thanks to these specialized range classes.

Try to declare ranges directly in the tests where they are used with no indirection, to avoid the extra allocation of range objects.
Alternatively, you can declare them as constants and reuse them.

Integral type ranges (ranges of any primitive type but Float or Double) are also progressions: they can be iterated over. This allows to replace the classic Java for loop with a shorter syntax.

This gets compiled to comparable optimized code with zero overhead:

To iterate backwards, you use the downTo() infix function instead of ..

Again, there is zero overhead after compilation with this construct:

There is also the useful until() infix function to iterate up to, but excluding the upper value.

When the original version of this article was published, calling this function used to generate suboptimal code. Things have greatly improved since Kotlin 1.1.4 and the compiler now generates the equivalent of an efficient Java for loop:

However, other iteration variants are not as well optimized.

Here is another way to iterate backwards and produce the exact same result as downto(), using the reversed() function combined with a range.

The resulting compiled code is unfortunately less pretty:

A temporary IntRange object is created to represent the range, then a second IntProgression object is created to reverse the values of the first one.

In fact, any combination of more than one function to create a progression generates similar code involving the small overhead of creating at least two lightweight progression objects.

This rule also applies to using the step() infix function to modify the step of a progression, even for a step of 1:

As a side note, when the generated code reads the last property of the IntProgression, this will perform a small computation to determine the exact last value of the range by taking the bounds and the step into consideration. In the above example, that last value would be 9.

To iterate in a for loop, prefer using a range expression involving a single function call to .. or downTo() or until() to avoid the overhead of creating temporary progression objects.

Instead of using a for loop, it could be tempting to use the forEach() inline extension function on a range to obtain similar results.

But if you take a closer look at the signature of the forEach() function used here, you’ll notice that it’s not optimized for ranges but only for Iterable, so it requires the creation of an iterator. Here is the Java representation of the compiled code:

This code is even less efficient than the previous examples, because in addition to the creation of an IntRange object, you also have to pay the cost of an IntIterator. At least, this one generates primitive values.

To iterate on a range, prefer using a simple for loop rather than calling the forEach() function on it, to avoid the overhead of an iterator object.

The Kotlin standard library provides a built-in indices extension property to generate ranges for array indices and Collection indices.

Surprisingly, iterating over this indices gets compiled to optimized code as well:

Here we can see no IntRange object being created at all and the list iteration is as efficient as it could be.

This works well for arrays and classes implementing Collection, so you could be tempted to roll your own indices extension property for custom classes while expecting the same iteration performance.

After compilation however, we can see that this is not as efficient because the compiler can not smartly avoid the creation of a range object:

Instead, I would recommend that you simply use until() in the for loop:

When iterating over custom collections not implementing the Collection interface, prefer writing your own range of indices directly in the for loop rather than relying on a function or property to generate the range, to avoid the allocation of a range object.

I hope this was as interesting for you to read as it was for me to write. You may expect more at a later date, but these 3 first parts cover everything I planned to write about initially. Please share if you like. Thank you!

Written by

Android developer from Belgium, blogging about advanced programming topics.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store