Skip to Content
Course content

167: Inline Functions and Performance

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're following a complex recipe to bake a cake. Halfway through, the instructions say, "Now, refer to page 42 to learn how to make the frosting." You have to stop what you're doing, flip through the book, find page 42, follow those steps, and then flip back to where you left off to finish the cake. In a simple recipe, that's no big deal. But if the recipe told you to "refer to page 42" every single time you added a sprinkle of sugar, you'd spend more time flipping pages than actually baking.

In Kotlin, when you pass a lambda to a function, the JVM treats that lambda as an object. It's essentially "flipping to page 42." The system has to create a function object, manage the call stack, and jump to a different piece of memory. If you're doing this inside a loop that runs 10,000 times, those "page flips" start to add up and slow your app down.

The hidden cost of lambdas

You've been using higher-order functions for a while now, and for most cases, the performance hit is negligible. However, I want you to see what's actually happening under the hood. Consider a simple utility function that executes a block of code and logs the result:

fun performTask(action: () -> Unit) {
    println("Starting task...")
    action()
    println("Task finished.")
}

Every time you call performTask { println("Doing work") }, Kotlin creates an instance of a Function object to hold that lambda. If this is called in a high-frequency loop—say, inside a custom animation frame or a heavy data processing pipeline—you're putting unnecessary pressure on the Garbage Collector by creating thousands of short-lived objects.

Telling the compiler to copy-paste

This is where the inline keyword comes in. When you mark a function as inline, you're telling the compiler: "Don't actually call this function. Instead, just copy the code of the function and the code of the lambda directly into the place where I called it."

Let's rewrite that utility:

inline fun performTask(action: () -> Unit) {
    println("Starting task...")
    action()
    println("Task finished.")
}

Now, if you call performTask { println("Doing work") }, the compiled bytecode doesn't contain a function call at all. It literally becomes:

println("Starting task...")
println("Doing work")
println("Task finished.")

The "page flip" is gone. The code is injected directly into the flow, and no lambda object is ever created. It's a pure performance win for higher-order functions.

When inlining becomes too much

You might be wondering, "Why not just make every single function inline?" I've seen developers do this, and it's a mistake. If you inline a massive function that is called in fifty different places, your final binary size will bloat because the compiler is copy-pasting that huge block of code fifty times. Use inline specifically for functions that take lambdas as arguments.

Sometimes, you might have a function with multiple lambdas, but you only want one of them to be inlined. In those cases, you can use the noinline modifier. Similarly, if your inline function passes a lambda to another block (like a nested try-catch) where a "non-local return" would be dangerous, you'll need crossinline. But for now, just remember the golden rule: inline for performance when lambdas are involved, but don't overdo it.




📋 Practical Task

Building a High-Precision Benchmarking Utility

You need to create a utility function called benchmark that measures how long a specific block of code takes to execute. To ensure the measurement is accurate and doesn't include the overhead of object creation, this function must be implemented as an inline function.

Requirements:

  • Create an inline function named benchmark that takes a lambda as its only parameter.
  • Inside the function, capture the start time using System.nanoTime().
  • Execute the lambda.
  • Capture the end time and calculate the difference.
  • Print the result in the format: "Execution time: [X] ns".
  • In your main function, use benchmark to measure a loop that sums numbers from 1 to 1,000,000.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.