Introduction

I'm sure everyone has used, seen, or heard about the mysterious inline keyword in Kotlin. If not, that's not a problem. This article aims to clear the air around the inline , noinline and crossinline modifiers. Also, we will discuss the benefits of reified type parameters.

The cost of using lambdas

Thanks to higher order functions, writing code in Kotlin is much more fun than it was with Java. This feature allows developers to produce concise and more readable code. But is there a downside of using higher order functions extensively in Koltin? How do they perform in terms of memory and bytecode size?

Kotlin in many cases uses compiler tricks to hide the old language constructs of Java. This is also the case here. Behind the scenes, every lambda expression is compiled into an anonymous class. If the lambda captures variables, it also needs to be instantiated on each invocation. If the lambda expression is called many times in a short a duration, the high number of object instantiations could lead to a problem called memory churn.

But don't worry! First, this just means that using lambdas could be less efficient than using regular functions and secondly this is why inline exists. Let's see how we can improve our code by using this magical keyword.

How inline works

Using the inline modifier in a function's signature tells the compiler to insert the function's body to each and every place it's called in the compiled code (every call site). This is called inlining. Let's see a simple example:

inline fun measureRuntime(body: () -> Unit) { val startTimestamp = System.currentTimeMillis() body() println("method runtime ${System.currentTimeMillis() - startTimestamp}") } fun main(args: Array<String>) { measureRuntime { val sumThousand = (1..1000).sum() println("sumThousand: $sumThousand") } }

The measureRuntime function above measures the runtime of the provided lambda expression. We call it from the main function, and when it's done running, it writes a message to the log. By applying the inline keyword to the measuring function, the compiled code is generated like if we would have written this:

fun main(args: Array<String>) { val startTimestamp = System.currentTimeMillis() val sumThousand = (0..1000).sum() println("sumThousand: $sumThousand") println("method runtime ${System.currentTimeMillis() - startTimestamp}") }

There is one more thing I didn't mention intentionally, but I'm sure you already noticed in the example above. Not only the body of the inline function, but also the code of the lambda expression provided as a parameter is inlined.

This feature of the inlining process only works when the developer uses a lambda expression and not when the parameter passed as a variable of a function type:

fun main(args: Array<String>) { val body = { val sumThousand = (0..1000).sum() println("sumThousand: $sumThousand") } measureRuntime(body) }

This restriction is there because the code of the passed function is not available at the call site, and it needs to be stored in an object somewhere. This means the lambda's code is not available at compile time for the compiler to inline it.

From Kotlin 1.1 you can use the inline modifier not just for functions but properties without backing fields.

Individual accessor methods can be marked with it:

var inlineProperty: String inline get() = "inline getter" set(value) {}

Or it can be used before the property declaration:

inline var inlineProperty: String get() = "inline getter" set(value) {}

The magic of non-local control flow

After I started using Kotlin and embraced the simplicity and conciseness of the lambda expressions by using them more and more in my code, I quickly ran into one complication related to the return statement. Let's take the simple example where you want to iterate through a collection and stop when you found a specific element.

fun main(args: Array<String>) { for (letter in listOf("a", "b", "c")) { if (letter == "b") { println("Found: b") return } } println("Not found: b") }

It's dead simple to use the return keyword in the for loop when the condition is fulfilled to stop the iteration, but how does it work with the forEach function? Thanks to inlining it works the same:

fun main(args: Array<String>) { listOf("a", "b", "c").forEach { if (it == "b") { println("Found: b") return } } println("Not found: b") }

When you use a return statement inside a lambda expression body (which is inlined), it will return from the function where it was called and not just from the lambda expression. This is called non-local return.

This feature is not working when the inlining of the lambda is not possible.

Other members of the inline family

Let's check out other special modifiers related to Kotlin's inlining feature which you will eventually bump into if you start adding inline functions to your code!

noinline

If you are considering defining functions with more than one function type parameter you should also consider using the noinline keyword to mark some of those parameters:

inline fun exampleFun(inlined: () -> Unit, noinline notInlined: () -> Unit) { //... inlined() notInlined() //... }

But what does it do? It's not hard to figure it out. When you mark a parameter with noinline , the provided lambda expression will not be substituted at the call site by the compiler. It will be called as mentioned above (static class generated, instantiation if it captures a variable...)

fun main(args: Array<String>) { exampleFun({ println("inlined") }, { println("not inlined") }) }

The code above basically equals with this:

fun main(args: Array<String>) { //... println("inlined") notInlined().invoke() //... }

It's useful when you are expecting that the provided lambda will have a bunch of code (which is not ideal because of the size of the compiled bytecode).

crossinline

My first encounter with this cryptic keyword happened when the compiler warned me about to use it unless my code won't compile. Let's see an example of that mistake:

inline fun exampleFun(body: () -> Unit) { Runnable { body() }.run() }

What's the problem with this code? Because the provided function type is not used directly in the body of the inline function but invoked in another execution context, we have to mark that parameter with the crossinline keyword. That will disable the non-local control flow feature inside the provided lambda, and enable our code to compile again.

inline fun exampleFun(crossinline body: () -> Unit) { Runnable { body() }.run() }

Reified type parameters

There is another benefit of using inline functions: you can use the reified keyword. But what is it good for? To understand it, we have to take a step back and talk about how the JVM handles generics.

Ever heard of the concept of type-erasure? No? Let me explain. Type-erasure is a method how the java compiler works with the type parameters of generic classes/functions. The compiled code for JVM (doesn't matter if it's written in Kotlin or Java) doesn't have the type information provided through generic parameters. This allows the code to run more efficiently and eat less memory because there is no need to store the extra type information. But it comes with a major limitation. Generic types are not available at runtime. It means you cannot check if a List instance was defined in the code with List<String> because the compiler erased the <String> part.

So let's say you are interested in only the elements of a certain type in a collection. Without using the reified keyword, you could write something like this:

fun <T> findType(type: Class<T>, list: List<Any>): Boolean { list.forEach { if (type.isInstance(it)) { System.out.println("A matching element found") return true } } System.out.println("Not found a matching element") return false } fun main(args: Array<String>) { findType(String::class.java, listOf("a", 1, args)) }

It looks okay, but we can improve it a lot more if we are fully aware of the capabilities of the language:

inline fun <reified T> findType(list: List<Any>): Boolean { list.forEach { if (it is T) { System.out.println("A matching element found") return true } } System.out.println("Not found a matching element") return false } fun main(args: Array<String>) { findType<String>(listOf("a", 1, args)) }

The function at the call site looks a lot better than before.

Inline every function? Not really.

I'm sure you are now convinced to use the inline a lot more. You can produce more efficient code with it and it also allows you to use other cool language features like non-local returns and reified type parameters. But there are circumstances you should consider before starting to refactor every function in your code.

The performance gain what inlining offers is usually available for functions with lambdas as arguments. Other cases should be investigated keeping these aspects in mind.

You should know that the JVM also use inlining on the bytecode level when it's translated to machine code, but it's not smart enough to do it everywhere. Helping it with the inline keyword could be beneficial.

But there is the code size problem. Inlining a large function could dramatically increase the size of the bytecode because it's copied to every calls site. In such cases, you can refactor the function and extract code to regular functions.

If you need more examples you can always check the Kotlin Standard Library which uses inline a lot.

Summary