1. Overview

In Kotlin, functions are first-class citizens, so we can pass functions around or return them just like other normal types. However, the representation of these functions at runtime sometimes may cause a few limitations or performance complications.

In this tutorial, first we’re going to enumerate two seemingly unrelated issues about lambdas and generics and then, after introducing Inline Functions, we’ll see how they can address both of those concerns, so let’s get started!

2. Trouble in Paradise

2.1. The Overhead of Lambdas in Kotlin

One of the perks of functions being first-class citizens in Kotlin is that we can pass a piece of behavior to other functions. Passing functions as lambdas let us express our intentions in a more concise and elegant way but that’s only one part of the story.

To explore the dark side of lambdas, let’s reinvent the wheel by declaring an extension function to filter collections:

fun <T> Collection<T>.filter(predicate: (T) -> Boolean): Collection<T> = // Omitted

Now, let’s see how the function above compiles into Java. Focus on the predicate function that is being passed as a parameter:

public static final <T> Collection<T> filter(Collection<T>, kotlin.jvm.functions.Function1<T, Boolean>);

Notice how the predicate is handled by using the Function1 interface?

Now, if we call this in Kotlin:

sampleCollection.filter { it == 1 }

Something similar to the following will be produced to wrap the lambda code:

filter(sampleCollection, new Function1<Integer, Boolean>() {
  @Override
  public Boolean invoke(Integer param) {
    return param == 1;
  }
});

Every time we declare a higher-order function, at least one instance of those special Function* types will be created.

Why does Kotlin do this instead of, say, using invokedynamic like how Java 8 does with lambdas? Simply put, Kotlin goes for Java 6 compatibility, and invokedynamic isn’t available until Java 7.

But this is not the end of it. As we might guess, just creating an instance of a type isn’t enough.

In order to actually perform the operation encapsulated in a Kotlin lambda, the higher-order function – filter in this case – will need to call the special method named invoke on the new instance. The result is more overhead due to the extra call.

So, just to recap, when we’re passing a lambda to a function, the following happens under the hood:

  1. At least one instance of a special type is created and stored in the heap
  2. An extra method call will always happen

One more instance allocation and one more virtual method call doesn’t seem that bad, right?

2.2. Closures

As we saw earlier, when we pass a lambda to a function, an instance of a function type will be created, similar to anonymous inner classes in Java.

Just like with the latter, a lambda expression can access its closure, that is, variables declared in the outer scope. When a lambda captures a variable from its closure, Kotlin stores the variable along with the capturing lambda code.

The extra memory allocations get even worse when a lambda captures a variable: The JVM creates a function type instance on every invocation. For non-capturing lambdas, there will be only one instance, a singleton, of those function types.

How are we so sure about this? Let’s reinvent another wheel by declaring a function to apply a function on each collection element:

fun <T> Collection<T>.each(block: (T) -> Unit) {
    for (e in this) block(e)
}

As silly as it may sound, here we’re gonna multiply each collection element by a random number:

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)
    val random = random()

    numbers.each { println(random * it) } // capturing the random variable
}

And if we take a peek inside the bytecode using javap:

>> javap -c MainKt
public final class MainKt {
  public static final void main();
    Code:
      // Omitted
      51: new           #29                 // class MainKt$main$1
      54: dup
      55: fload_1
      56: invokespecial #33                 // Method MainKt$main$1."<init>":(F)V
      59: checkcast     #35                 // class kotlin/jvm/functions/Function1
      62: invokestatic  #41                 // Method CollectionsKt.each:(Ljava/util/Collection;Lkotlin/jvm/functions/Function1;)V
      65: return

Then we can spot from index 51 that the JVM creates a new instance of *MainKt$main$*1 inner class for each invocation. Also, index 56 shows how Kotlin captures the random variable. This means that each captured variable will be passed as constructor arguments, thus generating a memory overhead.

2.3. Type Erasure

When it comes to generics on the JVM, it’s never been a paradise, to begin with! Anyway, Kotlin erases the generic type information at runtime. That is, an instance of a generic class doesn’t preserve its type parameters at runtime.

For example, when declaring a few collections like List or List, all we have at runtime are just raw Lists. This seems unrelated to the previous issues, as promised, but we’ll see how inline functions are the common solution for both problems.

3. Inline Functions

3.1. Removing the Overhead of Lambdas

When using lambdas, the extra memory allocations and extra virtual method call introduce some runtime overhead. So, if we were executing the same code directly, instead of using lambdas, our implementation would be more efficient.

Do we have to choose between abstraction and efficiency?

As is turns out, with inline functions in Kotlin we can have both! We can write our nice and elegant lambdas, and the compiler generates the inlined and direct code for us. All we have to do is to put an inline on it:

inline fun <T> Collection<T>.each(block: (T) -> Unit) {
    for (e in this) block(e)
}

When using inline functions, the compiler inlines the function body. That is, it substitutes the body directly into places where the function gets called.  By default, the compiler inlines the code for both the function itself and the lambdas passed to it.

For example, The compiler translates:

val numbers = listOf(1, 2, 3, 4, 5)
numbers.each { println(it) }

To something like:

val numbers = listOf(1, 2, 3, 4, 5)
for (number in numbers)
    println(number)

When using inline functions, there is no extra object allocation and no extra virtual method calls.

However, we should not overuse the inline functions, especially for long functions since the inlining may cause the generated code to grow quite a bit.

3.2. No Inline

By default, all lambdas passed to an inline function would be inlined, too. However, we can mark some of the lambdas with the noinline keyword to exclude them from inlining:

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

3.3. Inline Reification

As we saw earlier, Kotlin erases the generic type information at runtime, but for inline functions, we can avoid this limitation. That is, the compiler can reify generic type information for inline functions.

All we have to do is to mark the type parameter with the reified keyword:

inline fun <reified T> Any.isA(): Boolean = this is T

Without inline and reified, the isA function wouldn’t compile, as we thoroughly explain in our Kotlin Generics article.

3.4. Non-Local Returns

In Kotlin, we can use the return expression (also known as unqualified return) only to exit from a named function or an anonymous one:

fun namedFunction(): Int {
    return 42
}

fun anonymous(): () -> Int {
    // anonymous function
    return fun(): Int {
        return 42
    }
}

In both examples, the return expression is valid because the functions are either named or anonymous.

However, we can’t use unqualified return expressions to exit from a lambda expression. To better understand this, let’s reinvent yet another wheel:

fun <T> List<T>.eachIndexed(f: (Int, T) -> Unit) {
    for (i in indices) {
        f(i, this[i])
    }
}

This function performs the given block of code (function f) on each element, providing the sequential index with the element. Let’s use this function to write another function:

fun <T> List<T>.indexOf(x: T): Int {
    eachIndexed { index, value -> 
        if (value == x) {
            return index
        }
    }
    
    return -1
}

This function is supposed to search the given element on the receiving list and return the index of the found element or -1. However, since we can’t exit from a lambda with unqualified return expressions, the function won’t even compile:

Kotlin: 'return' is not allowed here

As a workaround for this limitation, we can inline the eachIndexed function:

inline fun <T> List<T>.eachIndexed(f: (Int, T) -> Unit) {
    for (i in indices) {
        f(i, this[i])
    }
}

Then we can actually use the indexOf function:

val found = numbers.indexOf(5)

Inline functions are merely artifacts of the source code and don’t manifest themselves at runtime. Therefore, returning from an inlined lambda is equivalent to returning from the enclosing function. 

4. Limitations

Generally, we can inline functions with lambda parameters only if the lambda is either called directly or passed to another inline function. Otherwise, the compiler prevents inlining with a compiler error.

For example, let’s take a look at the replace function in Kotlin standard library:

inline fun CharSequence.replace(regex: Regex, noinline transform: (MatchResult) -> CharSequence): String =
    regex.replace(this, transform) // passing to a normal function

The snippet above passes the lambda, transform, to a normal function, replace, hence the noinline.

5. Conclusion

In this article, we dove into issues with lambda performance and type erasure in Kotlin. Then, after introducing inline functions, we saw how these can address both issues.

However, we should try not to overuse these types of functions, especially when the function body is too large as the generated bytecode size may grow and we may also lose a few JVM optimizations along the way.

As usual, all the examples are available over on GitHub.


« 上一篇: Kotlin中的内联类