skip to content

Inline Cost & Codegen Tradeoffs

Inlining removes allocation and call overhead but duplicates bytecode at every call site, so inlining a large function is a net loss. The other constraint is visibility: an inline public function cannot touch non-public declarations without @PublishedApi.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

Why does Kotlin offer the `inline` keyword for higher-order functions, and what runtime cost does inlining remove?

level: juniorimportance: must knowfreq 70%

answer

  1. Lambda = FunctionN object + invoke() call
  2. inline copies body into call site
  3. Removes allocation + dispatch
  4. Enables non-local return + reified
  5. Best for small lambda-taking helpers

basics

~20 s

Normally passing a lambda creates an extra object and an extra method call. Marking the function inline copies the lambda's code straight into the caller, so no object is created and no extra call happens.

solid answer

~40 s

A higher-order function takes a lambda as a parameter. Without `inline`, the Kotlin compiler turns that lambda into a `Function` object (e.g. `Function1`) allocated on the heap, and invoking it costs a virtual `invoke()` call. For small helpers like `forEach`, `let`, or `repeat` called in hot loops, those allocations and megamorphic calls add up. Marking the function `inline` makes the compiler substitute the function body — and the bodies of its lambda parameters — directly at each call site, so the lambda object and the `invoke()` dispatch vanish. This also enables features that need the body present at compile time: non-local returns from the lambda and `reified` type parameters. The tradeoff is that the inlined bytecode is duplicated at every call site, so `inline` pays off for small functions taking lambdas, not large ones.

code

kotlin · 8 lines
kotlin
inline fun <T> measured(tag: String, block: () -> T): T {
    val start = System.nanoTime()
    val result = block()           // no Function object, no invoke() dispatch
    println("$tag took ${System.nanoTime() - start}ns")
    return result
}

val value = measured("load") { expensiveLoad() }  // body pasted at call site

go deeper

for a junior

Knows inline copies the lambda body in and avoids creating a lambda object.

for a middle

Explains the FunctionN allocation and invoke() dispatch that inlining removes, and names stdlib examples.

for a senior

Connects inlining to enabling non-local returns and reified, and notes the bytecode-bloat tradeoff.

for a principal

Reasons about when JIT would devirtualize anyway, and frames inline as a source-level guarantee with codegen/API consequences.

## What a higher-order function compiles to A *higher-order function* is one that takes a function (lambda) as a parameter or returns one. In Kotlin a lambda is a value, and by default the compiler represents it as an instance of a `FunctionN` interface (`Function0`, `Function1`, …). So this: ```kotlin fun runTwice(action: () -> Unit) { action(); action() } runTwice { println("hi") } ``` normally allocates a `Function0<Unit>` object for the lambda and calls its `invoke()` method twice. Two costs appear: - **Allocation**: a heap object per lambda (unless the compiler can cache a stateless one). - **Call overhead**: each `action()` is a virtual `invoke()` dispatch the JIT may not devirtualize. ## What `inline` does Marking the function `inline` tells the compiler to copy the function body — *and* the body of each lambda argument — into the call site: ```kotlin inline fun runTwice(action: () -> Unit) { action(); action() } // call site after inlining is effectively: println("hi"); println("hi") ``` No `Function0` is created, and there is no `invoke()` indirection. The standard library uses this for tiny building blocks: `forEach`, `map` on sequences, `let`, `run`, `also`, `apply`, `repeat`, `synchronized`, `use`. ## Why it matters - Hot loops calling `list.forEach { ... }` would otherwise allocate a lambda per call and dispatch through `invoke`. Inlining removes both. - Inlining is also what makes **non-local return** (`return` out of the enclosing function from inside the lambda) and **`reified`** type parameters possible, because the lambda/type is physically present at the call site. ## The tradeoff to remember The body is duplicated at every call site, so bytecode grows. `inline` is a win for *small* functions taking lambdas; for large bodies the bloat outweighs the saved allocation. The compiler even warns ("expected performance impact is insignificant") when you inline a function that has no functional parameters.

  • Does inlining always eliminate the lambda allocation?
    For lambdas passed to an `inline` function, yes — they're inlined and never allocated. But if a lambda parameter is marked `noinline`, or the lambda is stored/captured, an object may still be created.
  • Why does the compiler warn when you mark a function with no lambda parameters `inline`?
    Because the main benefit (removing lambda allocation/dispatch) only exists for functional parameters; without them you just duplicate bytecode for negligible gain.

Like pasting a short recipe step directly into your notes instead of writing 'see page 12' and flipping back every time.

saying these in an interview costs you the question

  • Claiming inline makes code run faster in all cases
  • Saying lambdas are 'free' even without inline
  • Confusing inline with the JIT's own inlining
  • Not knowing inline removes the lambda OBJECT, not just a call
  • Thinking inline only affects readability

context

open as a page

Inlining isn't free. Describe concrete situations where marking a function `inline` hurts more than it helps.

level: middleimportance: must knowfreq 60%

basics

~20 s

Inlining copies the whole function body into every place it's called. If the body is big or it's called from many places, your bytecode balloons. And if the function has no lambda parameter, you gain almost nothing.

open as a page

Explain the core tradeoff inlining makes between heap allocation/call overhead and bytecode size, and how you'd decide for a given function.

level: middleimportance: should knowfreq 40%

basics

~20 s

Inlining trades one thing for another: it removes the lambda object and extra call, but copies the function's code into every call site, making the program bigger. Inline when the body is small and called a lot; skip it when the body is big.

open as a page

Why does the Kotlin compiler forbid a `public inline` function from referencing `private` members of its class, and how does `@PublishedApi` relate to this?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Because an inline function's body gets copied into the code that calls it. If that body touched something private, the caller's compiled code would reference a member it isn't allowed to see. @PublishedApi lets you mark an internal member as safe to expose to such inlined code.

open as a page

You maintain a published Kotlin library. What policy would you adopt for `inline` functions on your public API, and why?

level: principalimportance: should knowfreq 30%

basics

~20 s

Use public inline sparingly. Because the body is copied into every user's compiled code, changing it later can break them without warning. Reserve inline for tiny helpers that truly need it, and keep the body stable.

open as a page