Question

When to Use Inline Functions in Kotlin

kotlinfunctioninline-functions

Question

In Kotlin, I understand that an inline function can sometimes improve performance, but it can also increase generated code size. I am not sure when it is actually appropriate to use one.

For example, Kotlin documentation often shows a pattern like this:

lock(l) {
    foo()
}

The idea is that instead of creating a function object for the lambda parameter and then calling it, the compiler could generate code similar to this:

l.lock()
try {
    foo()
} finally {
    l.unlock()
}

However, when I inspect a non-inline version like this, it seems that Kotlin does not always create a visible function object in the way I expected:

fun lock(lock: Lock, block: () -> Unit) {
    lock.lock()
    try {
        block()
    } finally {
        lock.unlock()
    }
}

Why does it seem like no function object is created for the non-inline case, and when should inline actually be used in Kotlin?

Short Answer

By the end of this page, you will understand what Kotlin inline functions do, why they are mainly useful for higher-order functions, why lambdas in non-inline functions may still appear optimized, and how to decide whether inline is worth using in real code. You will also learn the trade-off between runtime overhead and code size.

Concept

Kotlin inline functions tell the compiler to copy the function body directly into the call site instead of generating a normal function call.

This matters most for higher-order functions: functions that take other functions, usually lambdas, as parameters.

Consider this function:

fun lock(lock: Lock, block: () -> Unit) {
    lock.lock()
    try {
        block()
    } finally {
        lock.unlock()
    }
}

This is a higher-order function because it accepts block, which is a function.

Without inline, the usual model is:

  • the lambda may be represented as a function object
  • that object is passed to lock
  • lock calls block()

With inline, the compiler can replace the call with the actual code of both the function and the lambda.

inline fun lock(lock: Lock, block: () -> Unit) {
    lock.lock()
    try {
        block()
    } finally {
        lock.unlock()
    }
}

Call site:

lock(l) {
    foo()
}

Can become conceptually similar to:

l.lock()
try {
    foo()
} finally {
    l.unlock()
}

Why this matters

Inlining can help because it may:

  • avoid allocation of lambda objects
  • avoid virtual calls through function objects
  • enable further compiler optimizations
  • allow features like non-local returns from lambdas

But inlining also has a cost:

  • generated bytecode becomes larger
  • large inline functions repeated many times can hurt performance due to code bloat
  • debugging stack traces can sometimes be less straightforward

Why it may look like no function object was created

This is the part that often confuses beginners.

Even if a function is not marked inline, the generated code may still look efficient because:

  • the compiler can optimize non-capturing lambdas
  • the JVM can optimize allocations at runtime
  • decompiled output may hide implementation details
  • singleton lambda instances may be reused when the lambda captures no outside variables

For example, this lambda:

{ foo() }

captures nothing, so the compiler may generate a reusable singleton object instead of allocating a new object every time.

That does not mean inlining is useless. It means the runtime overhead may already be small in some cases.

The real purpose of inline

Use inline mainly when:

  • the function is small
  • it takes lambdas
  • it is called frequently
  • avoiding lambda-call overhead matters
  • you want lambda control flow features like return from the caller

Do not use inline just because a function is small. If a function has no lambda parameters, inlining usually gives little benefit and can just increase bytecode size.

Mental Model

Think of a normal higher-order function call like hiring a helper.

  • You write down instructions in a lambda.
  • The program wraps those instructions into a little object.
  • The function receives that object and says, “Now execute these instructions.”

An inline function is more like skipping the helper entirely.

  • Instead of packaging the instructions,
  • the compiler pastes the instructions directly where the function is called.

So:

  • non-inline = pass a callable package around
  • inline = paste the code in place

If the instructions are tiny and used often, pasting them can be faster. If the instructions are large and used in many places, pasting them everywhere can make the program bigger.

Take Quiz

Syntax and Examples

The basic syntax is:

inline fun runAction(action: () -> Unit) {
    action()
}

Calling it:

runAction {
    println("Hello")
}

A more realistic example is a lock wrapper:

inline fun <T> withLock(lock: Lock, block: () -> T): T {
    lock.lock()
    return try {
        block()
    } finally {
        lock.unlock()
    }
}

Usage:

val result = withLock(myLock) {
    "done"
}

Why this is a good inline candidate

  • The function body is small.
  • It takes a lambda.
  • It is a wrapper around common repeated logic.
  • It may be called frequently.

Non-inline version

fun <T> withLockNormal(lock: Lock, block: () -> T): T {
    lock.lock()
    return try {
        block()
    } finally {
        lock.unlock()
    }
}

This still works correctly. The difference is how the compiler and runtime handle the lambda and function call.

Example of a lambda that captures a variable

fun example() {
    var count = 0

    withLock(myLock) {
        count++
    }
}

Here the lambda uses count from the outer scope. Capturing lambdas are more likely to require extra generated machinery in the non-inline case, so inline can be more beneficial.

Step by Step Execution

Consider this inline function:

inline fun withMessage(block: () -> Unit) {
    println("Start")
    block()
    println("End")
}

And this call:

withMessage {
    println("Work")
}

Conceptual inline expansion

The compiler can treat it roughly like this:

println("Start")
println("Work")
println("End")

Step by step

  1. The call to withMessage is found.
  2. Because the function is marked inline, the compiler copies its body into the call site.
  3. The lambda body is also copied into the place where block() appears.
  4. The final code behaves like regular sequential code.

Compare with non-inline

fun withMessageNormal(block: () -> Unit) {
    println("Start")
    block()
    println("End")
}

Possible conceptual flow:

  1. Create or reuse a function object for the lambda.
  2. Pass that object into withMessageNormal.
  3. Execute println("Start").
  4. Call block() through the function object.
  5. Execute println("End").

Important detail

In real JVM execution, the compiler and JIT may optimize some of this. So the actual overhead of the non-inline version may be small, especially for simple non-capturing lambdas.

Real World Use Cases

Inline functions are commonly used in situations like these:

Resource handling

inline fun <T> useResource(block: () -> T): T {
    println("open")
    return try {
        block()
    } finally {
        println("close")
    }
}

Useful for wrapping setup and cleanup logic.

Locking and concurrency

inline fun <T> withLock(lock: Lock, block: () -> T): T {
    lock.lock()
    return try {
        block()
    } finally {
        lock.unlock()
    }
}

This keeps critical-section code short and safe.

Collection utilities

Kotlin's standard library uses inline heavily in functions like:

  • let
  • run
  • apply
  • also
  • forEach
  • map
  • filter (implementation details may vary by platform)

These functions often take lambdas and are used frequently.

DSL-style APIs

Builders and configuration blocks often use inline lambdas for cleaner syntax and lower overhead.

config {
    host = "localhost"
    port = 8080
}

Logging and guards

Inline wrappers can help avoid unnecessary work, especially when a lambda should only run conditionally.

Take Quiz

Real Codebase Usage

In real projects, developers use inline selectively, not everywhere.

Common pattern: tiny wrappers around lambdas

inline fun <T> measure(block: () -> T): T {
    val start = System.nanoTime()
    return try {
        block()
    } finally {
        println(System.nanoTime() - start)
    }
}

This is common because the wrapper is small and the lambda is the main work.

Guard-style helper functions

inline fun ifDebug(block: () -> Unit) {
    if (BuildConfig.DEBUG) {
        block()
    }
}

If not in debug mode, the block is skipped. Inlining can reduce wrapper overhead.

Validation and error handling helpers

inline fun requireValid(condition: Boolean, lazyMessage: () -> String) {
    if (!condition) throw IllegalArgumentException(lazyMessage())
}

This pattern is useful because the message is only computed when needed.

Non-local returns

One major reason Kotlin uses inline is control flow.

inline fun process(block: () -> Unit) {
    block()
}

fun test() {
    process {
        return
    }
}

This return returns from test(), not just from the lambda. That is possible because the lambda is inlined.

Without inline, that kind of return is not allowed.

When teams avoid inline

Teams usually avoid marking functions inline when:

  • the function has no lambda parameters
  • the function body is large
  • the function is not performance-sensitive
  • the benefit is unclear

A good rule is: inline for small higher-order utilities, not for ordinary functions by default.

Common Mistakes

1. Using inline on ordinary functions with no lambdas

inline fun add(a: Int, b: Int): Int {
    return a + b
}

This usually brings little value.

Better approach

Just write a normal function unless you have a measured reason.

fun add(a: Int, b: Int): Int = a + b

2. Assuming non-inline always creates a new object every call

Beginners often think this always happens:

  • call function
  • allocate a brand new lambda object
  • invoke it

But the compiler may reuse a singleton for non-capturing lambdas, and the JVM may optimize further.

So the cost is often possible overhead, not always a guaranteed heavy overhead.

3. Inlining large functions

inline fun hugeFunction(block: () -> Unit) {
    // many lines of code
    // repeated logic
    // lots of branches
    block()
    // many more lines
}

If called from many places, this can significantly grow bytecode size.

Avoid it when

  • the wrapper is large
  • it is used in many call sites
  • profiling shows no real benefit

4. Confusing inlining with runtime magic

Inlining is a compile-time transformation. It does not mean “faster in every case.”

You still need to think about:

  • code size
  • readability
  • actual hot paths

5. Forgetting that captured variables matter

Broken expectation:

fun demo() {
    var x = 10
    lock(myLock) {
        println(x)
    }
}

A lambda that captures x needs access to outside state. In the non-inline case, that can require additional generated structures.

6. Ignoring noinline and crossinline when needed

If a function has multiple lambda parameters, not all of them must be inlined.

inline fun example(inlined: () -> Unit, noinline stored: () -> Unit) {
    inlined()
    stored()
}

Use noinline when a lambda must be stored, passed around, or treated like a regular object.

Comparisons

ConceptWhat it doesBest use caseTrade-off
Normal functionPerforms a regular function callDefault choice for most functionsSmall call overhead
inline functionCopies function body into call siteSmall higher-order functions with lambdasLarger generated code
Non-inline lambdaLambda may be represented as an objectGeneral-purpose higher-order codePossible allocation/call overhead
Inline lambdaLambda body is copied into callerPerformance-sensitive wrappers and control-flow helpersCan increase bytecode size
noinline parameterPrevents a specific lambda from being inlinedWhen lambda must be stored or passed onwardLoses inline-only benefits
crossinline parameterInlines lambda but forbids non-local returnWhen lambda runs in another execution contextMore restrictive control flow

Inline vs non-inline in practice

QuestionInlineNon-inline
Takes a lambda?Often usefulFine if simplicity is enough
Tiny wrapper function?Good candidateAlso valid
Large function body?Usually avoidUsually better
Need non-local return?RequiredNot possible
No lambda parameter?Rarely usefulUsually best

Key takeaway

Choose inline because of a specific benefit:

  • lower overhead for higher-order functions
  • support for non-local returns
  • better utility API design

Do not choose it only because the function is short.

Take Quiz

Cheat Sheet

Quick rules

  • Use inline mainly for small higher-order functions.
  • The biggest benefit is with lambda parameters.
  • Do not inline large functions without a reason.
  • Do not expect huge gains automatically.
  • Non-inline lambdas may still be optimized by the compiler or JVM.

Basic syntax

inline fun doWork(block: () -> Unit) {
    block()
}

Good candidates

  • lock wrappers
  • timing helpers
  • validation helpers with lazy messages
  • scope-style functions
  • tiny DSL/builders

Usually bad candidates

  • ordinary arithmetic or utility functions with no lambdas
  • large functions called in many places
  • code with no measurable overhead issue

Important terms

  • higher-order function: a function that takes or returns another function
  • lambda: an anonymous function like { println("Hi") }
  • capturing lambda: a lambda that uses values from outer scope
  • non-capturing lambda: a lambda that uses no outside variables

Why non-inline may still look cheap

  • non-capturing lambdas can be reused as singletons
  • JVM JIT can optimize calls and allocations
  • decompiled code may not show all generated details clearly

Related modifiers

inline fun demo(
    action: () -> Unit,
    noinline stored: () -> Unit,
    crossinline safe: () -> Unit
) {
    action()
    stored()
    safe()
}
  • noinline: do not inline this lambda
  • crossinline: inline it, but forbid non-local return

Decision rule

Ask:

  1. Does the function take lambdas?
  2. Is it small?
  3. Is it used often or on a hot path?
  4. Do I need inline-only control flow?

If most answers are yes, inline is probably reasonable.

FAQ

Why doesn't a non-inline Kotlin lambda always create a new object?

Because the compiler can optimize non-capturing lambdas by reusing a singleton instance, and the JVM can apply further runtime optimizations.

Should I mark every higher-order function as inline?

No. Use inline when the function is small and the benefit is meaningful. Too much inlining increases code size.

Is inline only about performance?

No. It also enables language features such as non-local returns from lambdas.

Are inline functions always faster?

Not always. They can reduce call overhead, but they also increase bytecode size. The best choice depends on the function's size and usage.

When is inline most useful in Kotlin?

It is most useful for small wrapper functions that take lambdas, such as locking, resource handling, timing, logging, and scope-style utilities.

Why does Kotlin standard library use inline so often?

Because many standard functions are tiny higher-order utilities called frequently, so inlining often improves ergonomics and can reduce overhead.

Can I inline a function without lambda parameters?

Yes, but it is usually not helpful. Kotlin primarily benefits from inline when lambdas or reified type parameters are involved.

What is the simplest rule for beginners?

Default to normal functions. Reach for inline when writing a small higher-order helper and you understand the reason for using it.

Take Quiz

Mini Project

Description

Build a small Kotlin utility that safely executes code while holding a lock. This demonstrates when an inline higher-order function is useful: wrapping repeated setup and cleanup logic around a lambda.

Goal

Create a reusable withLock helper and use it to update shared state safely.

Requirements

  • Define a withLock function that accepts a Lock and a lambda.
  • Mark the function inline.
  • Ensure the lock is always released with try/finally.
  • Use the helper to modify shared data.
  • Print the final result to verify the code ran correctly.
Take Quiz

Keep learning

Related questions

View more