inline, noinline and crossinline modifier in Kotlin

Rakibul Huda

25 February, 2025

Kotlin, as a modern language, provides numerous features for developers to write expressive, concise, and optimized code. One such powerful feature is inline functions, accompanied by two additional modifiers: crossinline and noinline. These modifiers help improve performance and control how lambdas behave in higher-order functions. In this blog, we will explore the background, the problems these features solve, and provide detailed, step-by-step explanations with examples.

In Kotlin, a higher-order function is a function that either takes another function as a parameter or returns a function. This leads to a very expressive and functional style of programming. Lambdas (or function literals) are often passed as arguments in higher-order functions.

				
					

fun main() {
   for (i in 1..10) {
       doAction {
           println("Action called -> $i")
       }
       println("End action called")
   }
   println("Done")
}


fun doAction(action: () -> Unit) {
   action()
}

				
			

In this case, the function operateOnNumbers takes another function, operation, as a parameter.

However, lambdas in Kotlin are objects under the hood. When a lambda is passed to a higher-order function, Kotlin creates a new object to represent that lambda, which comes with two main costs:

  1. Memory overhead: A new object is created every time a lambda is passed.
  2. Function call overhead: Each lambda has an invoke() method, and calling it adds an extra method call, reducing performance in certain scenarios.

Decompiling the bytecode, we get the following java code

				
					public static final void main() {
  for(final int i = 1; i < 11; ++i) {
     doAction((Function0)(new Function0() {//new Function object is being created
        public final void invoke() {
           String var1 = "Action called -> " + i;
           System.out.println(var1);
        }


        // $FF: synthetic method
        // $FF: bridge method
        public Object invoke() {
           this.invoke();
           return Unit.INSTANCE;
        }
     }));
     String var1 = "End action called";
     System.out.println(var1);
  }


  String var2 = "Done";
  System.out.println(var2);
}

				
			

When lambdas are passed around frequently, such as in loops or performance-sensitive code, this overhead can become significant. Kotlin solves this problem using inline functions.

inline

By marking a function as inline, the Kotlin compiler is instructed to copy the function body and the lambda directly into the call site. This avoids the creation of lambda objects and their corresponding invoke() method calls, improving performance. Essentially, inlining makes higher-order functions behave as if the lambdas are part of the calling function.

				
					fun main() {
   for (i in 1..10) {
       doAction {
           println("Action called -> $i")
       }
       println("End action called")
   }
   println("Done")
}


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

				
			

The bytecode simply becomes this for the above example

				
					public static final void main() {
  for(int i = 1; i < 11; ++i) {
     int $i$f$doAction = false;
     int var2 = false;
     String var3 = "Action called -> " + i;
     System.out.println(var3);
     String var5 = "End action called";
     System.out.println(var5);
  }


  String var4 = "Done";
  System.out.println(var4);
}

				
			

In this case, the lambda is directly inlined into the calling function, eliminating object creation and the method call.

We can get heavily benefited from inline functions when

  • The function is small and frequently called
  • The code is performance critical and additional object allocation and function call is costly

We can’t be benefited from inline functions if

  • The function is large
  • The function needs to be stored or passed in another lambda

Kotlin makes heavy use of the inline functionality. If we look at the source code of the Collections file in Kotlin, we can see that most of the methods are inlined. For more information please have a loot at https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/src/kotlin/collections/Collections.kt

noinline

When using an inline function, all lambdas are inlined by default. However, there might be cases where we do not want to inline certain lambdas for specific reasons (e.g., the lambda needs to be stored or passed around). The noinline modifier is used in such cases to prevent a lambda from being inlined.

				
					

inline fun performActions(
   action1: () -> Unit,      // This lambda will be inlined
   noinline action2: () -> Unit  // This lambda will NOT be inlined
) {
   println("Executing action 1:")
   action1()  // This will be inlined at the call site


   println("Storing action 2 for later execution.")
   val storedAction = action2  // action2 is not inlined, so it can be stored
   storedAction()  // We can call it later, when needed
}


fun main() {
   performActions(
       action1 = { println("Inline Action 1 Executed!") },
       action2 = { println("Noinline Action 2 Executed!") }
   )
}


//Output
Executing action 1:
Inline Action 1 Executed!
Storing action 2 for later execution.
Noinline Action 2 Executed!

				
			

In the above example, Kotlin will still inline the performActions() method call and action1 lambda. However, it won’t do the same for the action2 lambda function because of the noinline modifier. noinline allowed us to store action2 for later use, which wouldn’t be possible if it were inlined.

The decompiled bytecode looks like this for the above example

				
					

public static final void main() {
  Function0 action2$iv = (Function0)null.INSTANCE;
  int $i$f$performActions = false;
  String var2 = "Executing action 1:";
  System.out.println(var2);
  int var3 = false;
  String var4 = "Inline Action 1 Executed!";
  System.out.println(var4);
  var2 = "Storing action 2 for later execution.";
  System.out.println(var2);
  Function0 storedAction$iv = action2$iv;
  storedAction$iv.invoke();
}

				
			

We can verify the behavior of noinline modifier from this code. We can see action2 is declared as Function and later executed as a regular lambda by the invoke method.

crossinline

In order to understand crossinline, we need to understand what a non-local return means first. A non-local return occurs when a return statement inside a lambda or an inline function causes the outer function to return, effectively terminating the execution of that outer function.

Kotlin allows non-local returns from lambdas in inline functions. While useful, this behavior is not always desirable.

In the following code, the return statement is used in the lambda function; the entire program ends instead of only exiting from the lambda.

				
					fun main() {
   for (i in 1..10) {
       doAction {
           if (i == 5)
               return
           println("$i")
       }


   }
   println("Done")
}




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


//Output
1
2
3
4

				
			

crossinline is used to prevent the non local return from a lambda. When crossline is used in any of the lambdas of an inline function, the non-local return will throw a compilation error.

				
					

fun main() {
   for (i in 1..10) {
       doAction {
           if (i == 5)
               return //Error
           println("$i")
       }


   }
   println("Done")
}


inline fun doAction(crossinline action: () -> Unit) {
   action()
}

				
			

By understanding when and how to use these modifiers, we can write more efficient and readable Kotlin code that maintains both flexibility and performance. It’s important to remember to balance the use of inline functions with careful consideration of their potential drawbacks, such as code bloat in large functions or incompatibility with recursion. When used correctly, these tools can significantly enhance the performance and readability of the codebase, especially in highly functional and concurrent environments.

It’s high time we leveraged them in our projects!

Rakibul Huda

25 February, 2025