What are the codegen and performance implications of marking a lambda `noinline`, and how does it interact with variable capture?
answer
- noinline = back to FunctionN object + virtual invoke
- Non-capturing -> singleton, zero per-call alloc
- Capturing -> new object each call
- Other inlined lambdas keep zero-alloc benefit
- All-noinline => maybe shouldn't be inline at all
basics
~20 sA 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 sInlining 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 linesinline 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
Knows noinline reintroduces an object/allocation versus a fully inlined lambda.
Explains it costs an allocation and virtual call, and that only that parameter pays it.
Distinguishes capturing vs non-capturing allocation, the singleton optimization, and keeps the hot path inlined.
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