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 lockcallsblock()
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
returnfrom 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.
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
- The call to
withMessageis found. - Because the function is marked
inline, the compiler copies its body into the call site. - The lambda body is also copied into the place where
block()appears. - The final code behaves like regular sequential code.
Compare with non-inline
fun withMessageNormal(block: () -> Unit) {
println("Start")
block()
println("End")
}
Possible conceptual flow:
- Create or reuse a function object for the lambda.
- Pass that object into
withMessageNormal. - Execute
println("Start"). - Call
block()through the function object. - 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:
letrunapplyalsoforEachmapfilter(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.
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
| Concept | What it does | Best use case | Trade-off |
|---|---|---|---|
| Normal function | Performs a regular function call | Default choice for most functions | Small call overhead |
inline function | Copies function body into call site | Small higher-order functions with lambdas | Larger generated code |
| Non-inline lambda | Lambda may be represented as an object | General-purpose higher-order code | Possible allocation/call overhead |
| Inline lambda | Lambda body is copied into caller | Performance-sensitive wrappers and control-flow helpers | Can increase bytecode size |
noinline parameter | Prevents a specific lambda from being inlined | When lambda must be stored or passed onward | Loses inline-only benefits |
crossinline parameter | Inlines lambda but forbids non-local return | When lambda runs in another execution context | More restrictive control flow |
Inline vs non-inline in practice
| Question | Inline | Non-inline |
|---|---|---|
| Takes a lambda? | Often useful | Fine if simplicity is enough |
| Tiny wrapper function? | Good candidate | Also valid |
| Large function body? | Usually avoid | Usually better |
| Need non-local return? | Required | Not possible |
| No lambda parameter? | Rarely useful | Usually 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.
Cheat Sheet
Quick rules
- Use
inlinemainly 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 lambdacrossinline: inline it, but forbid non-localreturn
Decision rule
Ask:
- Does the function take lambdas?
- Is it small?
- Is it used often or on a hot path?
- 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.
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
withLockfunction that accepts aLockand 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.
Keep learning
Related questions
Accessing Kotlin Extension Functions from Java
Learn how Kotlin extension functions are compiled and how to call them correctly from Java with clear examples and common pitfalls.
Add a Maven Repository URL in Gradle Kotlin DSL (build.gradle.kts)
Learn how to add Maven Central and a custom Maven repository URL in Gradle Kotlin DSL using build.gradle.kts syntax.
Allow HTTP and HTTPS in Android 9 Pie with Network Security Configuration
Learn how Android 9 Pie handles cleartext HTTP traffic and how to allow HTTP and HTTPS safely using network security config.