skip to content

What are the codegen and performance implications of marking a lambda `noinline`, and how does it interact with variable capture?

level: seniorimportance: should knowfreq 32%

answer

  1. noinline = back to FunctionN object + virtual invoke
  2. Non-capturing -> singleton, zero per-call alloc
  3. Capturing -> new object each call
  4. Other inlined lambdas keep zero-alloc benefit
  5. All-noinline => maybe shouldn't be inline at all

basics

~20 s

A noinline lambda becomes a real object, so it costs an allocation and a virtual call, just like a lambda passed to any normal function. If it captures no variables Kotlin can reuse a single shared instance; if it captures, a new object is created each call.

solid answer

~40 s

Inlining eliminates the `FunctionN` allocation and the `invoke` megamorphic call. Marking a parameter `noinline` re-introduces those costs for that lambda: the compiler generates a concrete `FunctionN` subclass and creates an instance. Capture matters: a **non-capturing** noinline lambda can be compiled to a singleton (one shared instance, like a static field), so repeated calls allocate nothing extra; a **capturing** noinline lambda must allocate a fresh object each invocation to hold the captured values. The rest of the inline function and its other inlined lambdas keep their zero-allocation benefit. The practical guidance: keep the hot, frequently-called lambdas inlined and reserve `noinline` for the one(s) you genuinely need as objects, so you pay for exactly one allocation rather than abandoning inlining wholesale.

code

kotlin · 10 lines
kotlin
inline fun pair(
    noinline first: () -> Int,
    noinline second: () -> Int
): Pair<() -> Int, () -> Int> = first to second

fun use(n: Int) {
    // {42} non-capturing -> shared singleton instance
    // {n}  capturing     -> fresh allocation per use() call
    pair({ 42 }, { n })
}

go deeper

for a junior

Knows noinline reintroduces an object/allocation versus a fully inlined lambda.

for a middle

Explains it costs an allocation and virtual call, and that only that parameter pays it.

for a senior

Distinguishes capturing vs non-capturing allocation, the singleton optimization, and keeps the hot path inlined.

for a principal

Reasons about bytecode-size vs allocation tradeoffs and when an all-noinline inline function should just be a normal function.

## Baseline: what inlining saves For a normal (non-inline) higher-order function, each lambda argument is a `FunctionN` object and each call is a virtual `invoke`. Inlining removes both: the body is spliced in, so there's **no allocation and no virtual dispatch**. ## What `noinline` costs Marking a parameter `noinline` opts it back into the object model: - The compiler synthesizes a class implementing the right `kotlin.jvm.functions.FunctionN` interface. - An instance is created to represent the lambda. - Calls go through `invoke`. Crucially, **only that parameter** pays this; sibling inlined lambdas and the function body remain inlined. ## Capture changes the allocation story Whether the noinline lambda allocates per-call depends on **variable capture**: - **Non-capturing** (references nothing from the enclosing scope): the compiler can emit a **singleton** — a single static `INSTANCE` reused for every call. Effectively zero per-call allocation. - **Capturing** (closes over locals/params): the object must store the captured values, so a **new instance is allocated each time** the surrounding code runs. ```kotlin inline fun build( noinline a: () -> Unit, // if non-capturing -> singleton noinline b: () -> Unit ): List<() -> Unit> = listOf(a, b) fun caller(x: Int) { build({ println("hi") }, // non-capturing -> shared instance { println(x) }) // captures x -> new object each caller() call } ``` ## Practical performance guidance - Reserve `noinline` for the lambda(s) you truly need as values; keep the hot path inlined. - A non-capturing noinline lambda is essentially free at runtime — don't fear it. - A capturing noinline lambda in a tight loop reintroduces allocation pressure; hoist or redesign if it's hot. - If *every* lambda in a function needs to be an object, the function probably shouldn't be `inline` at all — you'd be paying the inline machinery (bigger bytecode) for no benefit. See the inline cost/codegen tradeoffs. ## Bytecode size angle Inlining also increases bytecode at each call site (the body is duplicated). `noinline` doesn't grow per-site for that lambda, but the surrounding inline function still duplicates its body. This feeds into the general 'keep inline functions small' rule.

  • Does a non-capturing noinline lambda allocate on every call?
    No. The compiler can emit it as a singleton (a shared static INSTANCE), so it's reused with no per-call allocation. Only capturing lambdas allocate per use.
  • If every lambda parameter ends up noinline, is `inline` still worth it?
    Usually not — you lose the allocation/dispatch savings while still duplicating the function body's bytecode. A plain function is often the better design.

saying these in an interview costs you the question

  • Claiming noinline lambdas always allocate per call (capture-dependent)
  • Saying noinline makes the whole function slow
  • Ignoring that non-capturing lambdas can be singletons
  • Not realizing all-noinline defeats the purpose of inline
  • Confusing bytecode-size cost with allocation cost

context