Interview QuestionPractical QuestionFollow-up Questions

Inline Functions and Bytecode Optimization

skydovesJaewoong Eum (skydoves)||11 min read

Inline Functions and Bytecode Optimization

Kotlin's inline keyword resolves a fundamental tension in JVM language design: how to offer high level abstractions like higher order functions without paying the runtime cost of anonymous class allocation and virtual dispatch. The compiler achieves this by substituting the function body and all lambda arguments directly at the call site, producing bytecode that is structurally identical to hand-written imperative code. Understanding how this transformation works at the bytecode level, and how related modifiers like noinline, crossinline, and reified interact with it, is essential for writing performant idiomatic Kotlin. By the end of this lesson, you will be able to:

  • Trace the bytecode differences between a standard higher order function call and an inlined call.
  • Explain why inlining eliminates Function object allocation and virtual method dispatch.
  • Describe how non local returns become possible through inlining and why they are prohibited without it.
  • Identify the roles of noinline and crossinline in controlling inlining behavior for individual lambda parameters.
  • Apply reified type parameters to understand how inlining enables runtime access to generic type information.

Standard Higher Order Functions on the JVM

On the JVM, functions are not first class values. When Kotlin compiles a lambda expression passed to a standard (non inline) higher order function, it must represent that lambda as an object. The compiler generates an anonymous inner class that implements one of the kotlin.jvm.functions.FunctionN interfaces, and the call site instantiates that class.

Consider the following Kotlin source:

fun standardAction(block: () -> Unit) {
    println("Before")
    block()
    println("After")
}

fun main() {
    standardAction { println("Executing lambda") }
}

The decompiled Java equivalent of main reveals the cost:

// Decompiled from main():
Function0 lambdaInstance = new Function0() {
    public void invoke() {
        System.out.println("Executing lambda");
    }
};
standardAction(lambdaInstance);

Two sources of overhead are visible. First, a Function0 object is allocated on the heap every time the call site executes. In a tight loop or a frequently invoked utility, this creates steady pressure on the garbage collector. Second, invoking the lambda requires a virtual method call through Function0.invoke(). The JVM must resolve the concrete implementation at runtime through vtable dispatch, which prevents certain JIT optimizations and adds a layer of indirection to every invocation.

Inlined Functions and the Copy-Paste Transformation

The inline keyword instructs the Kotlin compiler to eliminate both the function call and the lambda object entirely. Instead of emitting a call instruction, the compiler copies the body of the inline function and the body of every lambda argument directly into the call site.

inline fun inlineAction(block: () -> Unit) {
    println("Before")
    block()
    println("After")
}

fun main() {
    inlineAction { println("Executing lambda") }
}

The decompiled output for main becomes a flat sequence of statements:

// Decompiled from main():
System.out.println("Before");
System.out.println("Executing lambda");
System.out.println("After");

The inlineAction function no longer exists as a separate method in the compiled output. No Function0 object is allocated. No invoke() call is dispatched. The bytecode is identical to what a developer would write if they had manually inlined the logic. This transformation is performed entirely at compile time, so there is no reflection or runtime machinery involved.

This interview continues for subscribers

Subscribe to Dove Letter for full access to exclusive interviews about Android and Kotlin development.

Become a Sponsor