skip to content

What is the runtime cost of passing lambdas to higher-order functions in Kotlin, and how does the inline modifier change that? Explain noinline and crossinline.

level: seniorimportance: should knowfreq 55%

answer

  1. Lambda => FunctionN object; capturing => per-creation allocation
  2. inline copies body + lambda into call site
  3. inline enables non-local return and reified
  4. noinline: keep a real object so you can store/forward it
  5. crossinline: stay inlined but ban non-local return (indirect calls)

basics

~20 s

Normally a lambda becomes an object, which costs an allocation. Marking the higher-order function inline copies its body and the lambda into the call site, removing that object. noinline and crossinline tweak which lambdas get inlined.

solid answer

~40 s

A non-inlined lambda compiles to a FunctionN instance; a capturing lambda allocates a new object per creation, and each call is a virtual invoke. For hot, small HOFs (map, filter, forEach) this overhead matters, so the standard library marks them inline. With inline, the compiler substitutes the HOF body and each lambda directly into the call site: no Function object, no invoke indirection, and lambdas may use non-local return. Costs: larger bytecode and you cannot store/pass the lambda elsewhere. noinline on a specific lambda parameter opts it out (so you can store or forward it). crossinline forbids non-local returns from a lambda that is invoked from another execution context (e.g. inside a nested lambda or runnable), preserving inlining while keeping control flow sound. reified type params require inline.

code

kotlin · 9 lines
kotlin
inline fun <T> List<T>.firstMatching(predicate: (T) -> Boolean): T? {
    for (e in this) if (predicate(e)) return e // non-local-style: returns from firstMatching
    return null
}

inline fun launchUnit(crossinline body: () -> Unit) {
    val r = Runnable { body() } // indirect call -> crossinline required
    r.run()
}

go deeper

for a junior

Knows lambdas can allocate and that some stdlib functions are inline.

for a middle

Explains inline removes the Function object and enables non-local return.

for a senior

Correctly uses noinline/crossinline and reified, and reasons about code-size vs allocation tradeoffs.

for a principal

Sets library policy on which HOFs are inline, measures impact, and weighs bytecode bloat vs hot-path gains.

## The default cost A function type compiles to a `kotlin.FunctionN` interface implementation. A lambda passed to a normal (non-inline) HOF therefore becomes an **object**: - A **non-capturing** lambda can be a reusable singleton, so allocation is often avoided. - A **capturing** lambda (closes over variables) allocates a new instance carrying the captured state, typically **one allocation per creation**. - Each call is a **virtual `invoke`** through the interface (an extra indirection vs a direct call). In tight loops or per-element collection operations, these allocations and megamorphic calls add up. ## `inline` Marking the HOF `inline` tells the compiler to **copy the function body into the call site** and **inline the lambda body** too: ```kotlin inline fun <T> measure(block: () -> T): T { val start = System.nanoTime() val r = block() println(System.nanoTime() - start) return r } ``` Results: - **No `Function` object**, no `invoke` indirection — the lambda body executes inline. - **Non-local return** becomes legal: a `return` inside the lambda returns from the *enclosing* function, because the code is physically there. - **`reified`** type parameters are possible (you can do `T::class`, `is T`) — only available on `inline` functions. Costs/limits: - **Code-size growth** (the body is duplicated at every call site). - You **cannot store or pass the inlined lambda** to a non-inline context, because there is no object to hold. - Inlining large functions is discouraged (the compiler warns). ## `noinline` Applied to an individual lambda parameter of an inline function, `noinline` opts **that** parameter out of inlining so you *can* store it, return it, or pass it on: ```kotlin inline fun f(a: () -> Unit, noinline b: () -> Unit) { a() // inlined val saved = b // allowed because b is a real object } ``` ## `crossinline` `crossinline` keeps a lambda inlined but **forbids non-local returns** from it. You need this when the lambda is invoked from **another execution context** inside the function — e.g. passed into a nested lambda, a `Runnable`, or an object — where a non-local return would be unsound: ```kotlin inline fun runLater(crossinline body: () -> Unit) { val r = Runnable { body() } // body executed indirectly r.run() } ``` Without `crossinline` the compiler rejects calling `body()` from inside the nested `Runnable`. ## Rules of thumb - Mark **small** HOFs that take lambdas and are called often `inline` (like the stdlib does). - Don't `inline` large bodies or HOFs whose lambda you must store — use `noinline` for the stored param. - Use `crossinline` when an inlined lambda is invoked indirectly. - Profile before optimizing: for cold paths the object overhead is negligible.

  • Why is reified only allowed on inline functions?
    Generics are normally erased; inlining substitutes the actual type at the call site, so the compiler can replace T with the concrete type, enabling T::class and is T checks.
  • When would you add noinline?
    When you must store, return, or forward a particular lambda parameter to a non-inline context — it needs to exist as a real Function object.
  • Does inline always improve performance?
    No. It removes allocation/indirection but grows bytecode; over-inlining large bodies can hurt instruction cache and increase method size. Profile.

inline is like pasting a recipe's steps directly into your cooking instead of phoning a chef each time (the call); noinline keeps the chef on call; crossinline lets the recipe be handed to a helper but bans walking out mid-dish.

saying these in an interview costs you the question

  • Claiming every lambda always allocates (non-capturing ones can be singletons)
  • Believing inline is purely free with no downside
  • Confusing noinline and crossinline roles
  • Thinking you can store an inline (non-noinline) lambda parameter
  • Not connecting reified to inline

context