skip to content

What is the runtime cost of all these nested receiver lambdas in a Kotlin DSL, and how do `inline` (and sometimes `reified`) change the picture?

level: seniorimportance: nice to knowfreq 30%

answer

  1. Lambda = FunctionN object by default
  2. inline copies body -> no allocation, no virtual invoke
  3. apply/with/buildString are inline
  4. noinline / crossinline modifiers
  5. reified needs inline; reifies the type argument

basics

~20 s

A lambda is normally a small object created at runtime. If a builder function is inline, the compiler copies the lambda's code into the call site, so no lambda object is allocated. That makes deeply nested DSLs basically free.

solid answer

~40 s

Each non-inlined lambda becomes a function-object instance (a `Function`/`FunctionN` implementation), so a deeply nested DSL could allocate one object per block plus the builder objects themselves. Marking the builder function `inline` makes the compiler substitute the lambda body directly at the call site — no `Function` allocation, no virtual `invoke` call, and non-local `return` becomes possible. The standard helpers (`apply`, `with`, `run`, `buildString`) are `inline`, which is why `apply(block)`-based builders are cheap. `reified` is unrelated to the receiver mechanism but pairs with `inline`: it lets a generic builder access its type argument at runtime (e.g. `inline fun <reified T> create(init: T.() -> Unit)`), enabling reflection-free type lookups. Caveats: inlining duplicates code (larger bytecode), and you may need `noinline`/`crossinline` for lambdas you store or call from another context.

code

kotlin · 5 lines
kotlin
inline fun buildConfig(init: Config.() -> Unit): Config =
    Config().apply(init)   // apply is itself inline -> zero extra lambda objects

// reified pairs with inline:
inline fun <reified T : Any> typeName(): String = T::class.simpleName ?: "?"

go deeper

for a junior

Knows a lambda is an object and that inline exists, but not the details.

for a middle

Explains that inline avoids lambda allocation and that stdlib scope functions are inline.

for a senior

Discusses non-local return, code-size trade-offs, noinline/crossinline, and the reified pairing.

for a principal

Reasons about when inlining is worth the bytecode cost, library API stability of inline functions, and binary-compat implications.

## Where the cost is In a DSL like `html { body { p { +"x" } } }` there are two kinds of runtime objects: 1. **The builder/node objects** (`HTML`, `BODY`, `P`) — unavoidable; they hold the data. 2. **The lambdas** you pass to each builder. A lambda, by default, is compiled to an instance of a synthetic class implementing `Function1<...>` (or `FunctionN`). So a naive (non-inline) builder allocates one lambda object per block, each with an `invoke` method call. For most apps that's negligible, but in hot loops or large generated trees it adds up. ## `inline` removes the lambda object Marking the builder function `inline` tells the compiler to **copy the lambda's body into the call site** rather than create and invoke a function object: ```kotlin inline fun <T : Tag> T.initTag(child: T, init: T.() -> Unit): T { child.init() // body inlined here -> no Function object return child } ``` Consequences: - **No allocation** of the lambda instance, **no virtual `invoke`** dispatch. - **Non-local `return`** works: a `return` inside the lambda can return from the enclosing function. - The standard library's `apply`, `also`, `let`, `run`, `with`, `buildString`, `buildList` are all `inline` — that's why receiver-lambda builders built on them are cheap. ## Costs / modifiers of inlining - **Code size**: the body is duplicated at every call site; over-inlining bloats bytecode. - **`noinline`**: mark a specific lambda parameter to *not* inline (e.g. when you store it in a field or pass it on). - **`crossinline`**: mark a lambda that must not use non-local return because it's invoked from another execution context (e.g. inside another lambda or object). - Inline functions have visibility constraints on what they can reference and can't be recursive. ## Where `reified` fits `reified` is a *separate* feature that only works on `inline` functions. Because the body is inlined, the compiler knows the concrete type argument and can 'reify' it — you can use `T` as if it were a real type at runtime: ```kotlin inline fun <reified T> tag(init: T.() -> Unit): T { val obj = T::class.java.getDeclaredConstructor().newInstance() // T known at runtime obj.init() return obj } ``` Without `reified`, generic type arguments are erased and `T::class` wouldn't be available. In DSLs `reified` is occasionally used to look up a type-keyed registry or deserialize, but it's not part of the core receiver-lambda model. ## Practical guidance - Default to building on `apply`/`also` (already inline) for cheap builders. - Add `inline` to your own builder helpers if profiling shows lambda-allocation pressure. - Don't inline large functions indiscriminately — measure bytecode/perf.

  • Why can't `reified` be used without `inline`?
    Type arguments are erased on the JVM. `reified` works only because `inline` substitutes the function body at the call site, where the concrete type is known and can be baked in. A non-inline generic has no runtime type info for `T`.
  • When would you mark a builder lambda `crossinline`?
    When the inlined function invokes that lambda from a *different* execution context (e.g. inside another lambda, a `Runnable`, or a nested object), where a non-local `return` would be unsafe. `crossinline` forbids non-local returns in it.

A non-inline lambda is like calling a contractor each time; inline is pasting their instructions straight into your own to-do list — no separate person to hire.

saying these in an interview costs you the question

  • Claiming receiver lambdas are inherently free without inlining
  • Saying `inline` improves performance always (ignores code bloat)
  • Thinking `reified` works on any generic function
  • Confusing `noinline` and `crossinline`
  • Believing inlining changes the DSL's call-site syntax

context