1. Overview

Inlining is one of the oldest tricks that many compilers use to optimize our code’s performance. So, naturally, to exhibit better performance and a smaller footprint, Kotlin also takes advantage of this trick.

In this tutorial, we’re going to talk about two consequences of lambda function inlining in Kotlin: noinline and crossinline.

2. Quick Inline Refresher

Inline functions in Kotlin help us to avoid extra memory allocations and unnecessary method invocations for each lambda expression. For instance, in this simple example:

inline fun execute(action: () -> Unit) {
    action()
}

Kotlin will inline both the calls to the execute() function and the lambda functions in the call site. So, if we call this function from another one:

fun main() {
    execute {
        print("Hello ")
        print("World")
    }
}

Then the inline result would be something like:

fun main() {
    print("Hello ")
    print("World")
}

As shown above, there are no signs of the execute() method call and, of course, no signs of the lambda. Therefore, inlining will boost the performance of our applications while using fewer allocations.

3. The noinline Effect

By default, the inline keyword will instruct the compiler to inline the method call and all passed lambda functions on the call site:

inline fun executeAll(action1: () -> Unit, action2: () -> Unit) {
    // omitted
}

In the above example, Kotlin will inline the executeAll() method call along with the two lambda functions (action1 and action2).

Sometimes, we may want to exclude some of the passed lambda functions from inlining for whatever reasons. In that case, we can use the noinline modifier to exclude the marked lambda function from inlining:

inline fun executeAll(action1: () -> Unit, noinline action2: () -> Unit) {
    action1()
    action2()
}

In the above example, Kotlin will still inline the executeAll() method call and action1 lambda. However, it won’t do the same for the action2 lambda function because of the noinline modifier.

Basically, we can expect that Kotlin will compile the following code:

fun main() {
    executeAll({ print("Hello") }, { print(" World") })
}

Into something like:

fun main() {
    print("Hello")
    val action2 = { print(" World") }
    action2()
}

As shown above, there’s no sign of the executeAll() method call. In addition, the first lambda function is obviously inlined. However, the second lambda function is there as-is without being inlined.

3.1. Bytecode Representation

Earlier, we said that Kotlin compiles the inline function call in that main function like:

fun main() { 
    print("Hello") 
    val action2 = { print(" World") } 
    action2() 
}

Even though this mental model helps us to better understand the details, Kotlin does not generate another Kotlin or Java code from the original one. To see the actual details, we should check out how Kotlin generates a bytecode in this case.

In order to do that, let’s compile the Kotlin code and use javap to take a peek at the bytecode:

>> kotlinc Inlines.kt
>> javap -c -p com.baeldung.crossinline.InlinesKt
// omitted
public static final void main();
    Code:
       0: getstatic     #41       // Field com/baeldung/crossinline/InlinesKt$main$2.INSTANCE:LInlinesKt$main$2;
       3: checkcast     #18       // class kotlin/jvm/functions/Function0
       6: astore_0.               // storing the lambda
       7: iconst_0
       8: istore_1
       9: iconst_0
      10: istore_2
      11: ldc           #43       // String Hello
      13: astore_3
      14: iconst_0
      15: istore        4
      17: getstatic     #49       // Field java/lang/System.out:Ljava/io/PrintStream;
      20: aload_3
      21: invokevirtual #55       // Method java/io/PrintStream.print:(Ljava/lang/Object;)V
      24: nop
      25: aload_0                 // the lambda at index 5
      26: invokeinterface #22,  1 // InterfaceMethod kotlin/jvm/functions/Function0.invoke:()LObject;
      31: pop
      32: nop
      33: return
// use -v flag to see the following line
InnerClasses:
static final #37; // class com/baeldung/crossinline/InlinesKt$main$2

From the above bytecode, we can understand a few points:

  • There’s no invokestatic representing a call to the executeAll() method, so Kotlin surely inlined this method call
  • Indices 11 through 21 represent a direct call to System.out.print(“Hello”), so the first lambda function is also inlined
  • At index 5, we’re getting the com/baeldung/crossinline/InlinesKt$main$2 singleton instance, which is a subtype (index 3) of Function0 in Kotlin. Kotlin compiles the lambda function with no arguments and Unit return types as a Function0. At index 26, we’re calling the invoke() method on this Function0, which basically is equivalent to calling the non-inlined lambda function

4. The crossinline Effect

In Kotlin, we can only use a normal, unqualified return to exit a named function, an anonymous function, or an inline function. In order to exit from a lambda, we must use a label (as in return@label). We can’t use a normal return in a lambda because it will exit from the enclosing function:

fun foo() {
    val f = {
        println("Hello")
        return // won't compile
    }
}

Here, the Kotlin compiler won’t allow us to exit from the enclosing function using a return inside the lambda. Such returns are called non-local returns.

We can use non-local control flow in inline functions because the lambda will be inlined in the call site:

inline fun foo(f: () -> Unit) {
    f()
}

fun main() {
    foo { 
        println("Hello World")
        return
    }
}

Even though we’re exiting from a lambda, the lambda itself is inlined in the main function. Therefore, this return statement happens directly in the main function and not in the lambda. That’s the reason we can use normal returns inside inline functions.

Given this, what happens when passing a lambda function from an inline function to a non-inline one? Let’s check it out:

inline fun foo(f: () -> Unit) {
    bar { f() }
}

fun bar(f: () -> Unit) {
    f()
}

Here, we’re passing the f lambda from an inline function to a non-inline function. When the lambda parameter in an inline function is passed to another non-inline function context like this, we can’t use non-local returns. So, the above code won’t even compile in Kotlin.

Let’s see what sort of problems this would cause if Kotlin allowed it.

4.1. The Problem

If Kotlin allowed the above function, we could actually use a non-local return at the call site:

fun main() {
    foo {
        println("Hello World")
        return
    }
}

Since foo is an inline function, Kotlin would inline it in the call site. The same is true for lambda. So, at the end of the day, Kotlin would compile the main function into something like:

fun main() {
    bar {
        println("Hello World")
        return // root cause
    }
}

Even though Kotlin inlined some calls here, the call to the bar function remained as-is. Therefore, if Kotlin allowed the non-local return for the inline function foo, it would eventually violate its rule about not using non-local control flows in lambdas in the function bar.

That’s the reason behind this rule. So, to sum up, we can use non-local control flows in three cases:

  • Normal named functions
  • Anonymous functions
  • Inline functions only when we call the lambda directly or pass it to another inline function

4.2. The Solution

Sometimes, we know that we’re not going to use non-local control flows in lambda functions. At the same time, we may also want to benefit from the advantages of inline functions.

In such situations, we can mark the inline function lambda parameter with the crossinline modifier:

inline fun foo(crossinline f: () -> Unit) {
    bar { f() }
}

fun bar(f: () -> Unit) {
    f()
}

With the help of the crossinline modifier, the above code will compile. However, we still can’t use non-local returns on the call site:

fun main() {
    foo {
        println("Hello World")
        return // won't compile
    }
}

That’s the whole existential purpose for the crossinline: benefiting from the efficiency of inline functions while losing the ability to use non-local control flows in lambdas.

4.3. noinline vs. crossinline

Surprisingly, we can even use the noinline modifier to compile the foo and bar functions:

inline fun foo(noinline f: () -> Unit) {
    bar { f() }
}

fun bar(f: () -> Unit) {
    f()
}

This surely compiles. However, we lose both the efficiency of inline functions and the ability to use non-local returns. Even Kotlin will warn us about this fact during compilation:

>> kotlinc Inlines.kt
Inlines.kt:12:1: warning: expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types
inline fun foo(noinline f: () -> Unit) {
^

Here, the Kotlin compiler explicitly states that we won’t gain that much benefit from inline functions when all lambdas are marked with noinline.

5. Conclusion

In this article, we evaluated the differences between noinline and crossinline modifiers. More specifically, the former allows us to exclude some lambda parameters from inlining.

Moreover, we can use noinline when passing a lambda from an inline function to a non-inline one. However, if we use this modifier for this purpose, we will lose the efficiency of inlining. Similarly, the crossinline modifier is applicable in the same scenario with one big difference: We can still benefit from the superiority of inlining.

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

Comments are closed on this article!