Hopp til hovedinnhold

On the JVM, calling a function or instantiating a class will always incur an overhead, unless the JVM runtime performs some magic. At least, that's how it used to be before Kotlin introduced the inline keyword. This article will give you a quick introduction to this fantastic keyword, and how it can help you!

Inline function

Say we have an extension function for finding one element in a list of strings which has some specific length.

public fun List<String>.findStringOfLength(targetLength: Int): Boolean {
    if (isEmpty()) return false
    for (element in this) if (element.length == targetLength) return true
    return false
}

val result = listOf("Cake", "Pie", "Pasta").findStringOfLength(4)

Yes, this is a simple and contrived function for this use case, but it illustrates the point.

The function will be compiled into a static function as part of a final class, which will then be called at the call site. Now, say we have some hyper-optimization to do(no, the JVM will not inline for us at runtime for.. reasons 🤷‍♀️ ), so we simply cannot accept the overhead of the function call! Enter, inline.

public inline fun List<String>.findStringOfLength(targetLength: Int): Boolean

By adding the inline keyword to the function signature, the call to findStringOfLength will be inlined at the call site by the compiler. In essence, it copies the code of the findStringOfLength function, and replaces any findStringOfLength call with the code in the function body. Neat! However, if you copy this simple sample code, you'll get this reminder from the compiler;

Expected performance impact of inlining 'function definition' is insignificant. Inlining works best for functions with parameters of functional types

So when might this be usefull? 🤔

Higher-order functions

In the Kotlin standard library, there is a cornucopia of extension functions which make our lifes easier. One example of such a function is the any function, a higher-order function which is callable on anything Iterable. By the help of Intellij, here's a definition and sample call.

public inline fun <T> Iterable<T>.any(predicate: (T) -> Boolean): Boolean {
    if (this is Collection && isEmpty()) return false
    for (element in this) if (predicate(element)) return true
    return false
}

listOf("Cake", "Pie", "Pasta").any { it.length == 4 }

any acceptes a function argument called predicate and calls it on each element in the list. If the predicate returns true for any element, the any function will also return true. Now, if any didn't have the inline modifier, this would mainly have two consequences;

  • The any function would be called as a static function
  • Any Lambdas passed as arguments would be compiled into a generic function object at each call site

So, if we're only passing regular functions to any, there isn't that much overhead. But we pass Lambdas, don't we?

This reveales another effect of the inline modifier - Lambdas which are passed as arguments are also inlined! 🤯 So by passing Lambdas to an inline function, we avoid both the runtime costs of the static version of the inline function, as well as the generic function object of the Lambda! This is one of the many reasons that Kotlin can maintain performance on par with Java, while extending the Java library with modern concepts.

noinline

While inlining of lambdas is great in most circumstances, there are times where that's not desired. In that case, just annotate the function argument with noinline.

public inline fun <T> Iterable<T>.any(noinline predicate: (T) -> Boolean): Boolean

Non-local returns

Lambdas are allowed to use the return keyword with a label, e.g. return@forEach. When the lambda is used as an argument to an inline function however, the return keyword will act as if it was a return used in the function body of the enclosing function. This is a small but neat trick that makes an ordinary extension function seem as if it's part of the language!

fun foo() {
    listOf(1, 2).forEach {
        if(it % 2 == 0) return //Acts the same as the return in the for loop below
    }
    for (i in listOf(1, 2)) {
        if(i % 2 == 0) return
    }
    println("No even number found!")
}

crossinline

Non-local returns can be great, and makes the Lambdas feel like a natural part of the language. However, there might be times when this kind of functionality is not desired, such as when passing the lambda to another function. To deny this behavior, just add the crossinline keyword to the argument.

public inline fun <T> Iterable<T>.any(crossinline predicate: (T) -> Boolean): Boolean

Generic functions

Another usecase is generic functions where you need access to the actual type of the generic parameter. Normally, generic functions does not have access to the actual type of a generic parameter, as the generic type is erased at runtime. With an inlined function however, the function will be executed in place where it still has access to the type. Just add the keyword reified to the generic type!

public inline fun <reified T> Iterable<T>.any(predicate: (T) -> Boolean): Boolean

For more information, see day 15 of the Kotlin Christmas calendar.

Inline class ⚠️

An experimental feature of Kotlin is inline classes.

inline class Salary(val salary: Int)
inline class Age(val age: Int)

These classes can only have one actual property(as declared in the constructor), and no init block. While this sounds like a somewhat amputated class, the power comes when the code is compiled. The compilator will prefer to erase the inline class, so source code which deals with the Name class, will (mostly) see the String class after compilation. This has some key benefits;

  • The overhead incured when using the inline class is comparable to other primitive wrappers such as Integer and Boolean, so minimal
  • The optimizations related to primitives(e.g. Long or Floats) are used
  • The ability to have stronger type safety on properties

As an example, see the following two data classes;

data class Employee(val name: String, val salary: Int, val age: Int)

data class EmployeeWithInlineClass(val name: Name, val salary: Salary, val age: Age)

When you use the Employee class, it's easy to accidently swap the salary and age arguments. With EmployeeWithInlineClass however, you have to instantiate the arguments with the correct type, forcing you to send in the right value to the right argument. This is of course nothing new, as one could use a regular old class before, but that would guarantee an overhead! When an inline class is compiled on the other hand, the compiler will prefer to erase the inline class as much as possible and stick to the primitive wrapped. This will yield better performance when compared to using the tradition class, while providing the same type safety, which again can lead to fewer bugs. For more on inline classes, see this post on the Kotlin Expertise Blog.

Did you like the post?

Feel free to share it with friends and colleagues