Inline Functions

Alex Woods

Alex Woods

January 27, 2020


Do you remember when you first started programming, and instead of breaking repeated code into a function, you copied and pasted it anywhere you needed? That's what the compiler does with an inline function.

The compiler inlines the function and any lambdas passed to it, meaning it takes the function's bytecode and pastes it directly at the call site.

inline fun <T> log(name: String, action: () -> T) {
    println("Before $name")
    action.invoke()
    println("After $name")
}

log("foo") { println("foo") }

// compiler generates
println("Before foo")
println("foo")
println("After foo")

When should you use an inline function?

Performance

We use inline functions every time we use a collection operation. I'm talking about map, filter, etc. This is great news since it allows us to use higher order functions at no cost—enabling clean and fast functional programming.

val newList = listOf(1, 2, 3, 4).map { it * 2 }

// compiles to
val temp: MutableList<Int> = mutableListOf()
for (x in listOf(1, 2, 3, 4)) {
	temp.add(x * 2)
}

val newList = temp.toList()

Why does inlining gain performance?

To understand why inlining functions can increase performance, we have to understand what Kotlin does when we don't inline a function:

  • It jumps to a different place in memory, to get the function's code.
  • If the function contains a lambda argument, an extra object is created to hold all of its information.

Therefore when we inline a function, we avoid a memory jump, and if there's a lambda argument, we avoid extra object creation. In general this is not game changing, but for functions like map, forEach, and filter, which are heavily used and take lambda function arguments, inlining is a valuable feature.

Non-local returns

Speaking of the collection operations: have you noticed you can return from them?

fun findIndex(value: String, list: List<String>): Int? {
    list.forEachIndexed { index, item ->
        if (item == value) return index
    }

    return null
}

Try that from a normal function, the compiler won't allow it.

fun bar(): Int {
    normalFunction {
        return 5   // `return` is not allowed here!
    }
}

In the first example, because the compiler is inlining the code, in terms of return, it's as if the function call wasn't there. return goes back to the nearest fun declaration, and with inline functions, the compiler has stripped that away.

Reified Type Parameters

Sometimes, functions using generics need type information. For example, the collection function filterIsInstance.

abstract class Animal(name: String)

data class Dog(val name: String): Animal(name)
data class Cat(val name: String): Animal(name)

val pets: List<Animal> = listOf(Cat("Cleatus"), Cat("Jim"), Dog("Morgana"))

pets.filterIsInstance<Dog>()  // [Dog(name=Morgana)]

Because the function is inlined, we don't have to do any reflection. We can use the type operators is and as. The filterIsInstance function is a simple extension function on Iterable that does just that.

inline fun <reified T> kotlin.collections.Iterable<*>.filterIsInstance(): List<T> {
    val newList = mutableListOf<T>()

    this.forEach { if (it is T) newList.add(it) }

    return newList.toList()
}

Why doesn't type erasure apply to inlined functions?

Because the compiler is inlining the function's bytecode, it knows the actual type used as an argument. When the compiler pastes the code, there is no notion of generics. It's using a real type—the type is used at that particular call site. A type we can access and get information about.

This is significantly different behaviour than a normal function. It's so different that it allows us to gather type information, even in the presence of type erasure.

Limitations of inline functions

You might ask—why not make every function inline?

It's easy to abuse inline functions. If we create a large inline function, or an inline function that calls an inline function, our code can quickly balloon in size. When in doubt, think about what the compiler is doing–are you comfortable with it pasting the code at every call site? Are you content losing the stack-trace if you need to debug? If not, use a regular function.

Furthermore, inline functions cannot use elements with a more restrictive visibility [4]. Your inline function might be pasted to a class in another module, where that private class it's using won't be accessible.

Lastly, inline functions can't be recursive. Think about what the compiler would do if it encountered one.

Inline functions are a simple tool to increase performance, but they should only be used at the right time.

Sources

  1. Kotlin documentation
  2. How does reified keyword in Kotlin work?
  3. Inlining Generated Code Explanation
  4. Effective Kotlin

Want to know when I write a new article?

Get new posts in your inbox