Inline functions in Kotlin ๐Ÿ’š

Inline functions in Kotlin ๐Ÿ’š

Dive into inline functions in Kotlin and understand crossinline and noinline keywords.

ยท

6 min read

Introduction ๐Ÿ“–

In this article, we will discuss inline functions in Kotlin. Inline functions can be tricky to grasp, especially how to use them effectively in your code. We will aim to explain things as clearly as we can.

For the beginning let's try to understand these two simple functions

fun testInline() {
    print("Hello")
    foo {
        print("World")
    }
}

fun foo(print: () -> Unit) {
    print()
}

Here we have a function called testInline. Inside this function, we utilize a higher-order function called foo with a lambda function named print as an argument. Since this code is quite straightforward, I will skip the explanation and just offer the definition of a lambda function (or anonymous function) from Wikipedia:

In computer programming, an anonymous function (function literal, lambda abstraction, lambda function, lambda expression or block) is a function definition that is not bound to an identifier. Anonymous functions are often arguments being passed to higher-order functions or used for constructing the result of a higher-order function that needs to return a function.
- Wikipedia

Now what we really care about the above code is the compiled code.

So, first things first, in Android Studio, if you click Tools -> Kotlin -> Show Kotlin byteCode, a new window will open. If we press the Decompile button, we will likely see something like this:

public final void testInline() {
      String var1 = "Hello";
      System.out.print(var1);
      ((Scratch)this).foo((Function0)null.INSTANCE);
   }

   public final void foo(@NotNull Function0 print) {
      Intrinsics.checkNotNullParameter(print, "print");
      print.invoke();
   }

This code is the equivalent java code that produced by the compiler. Now if we check this line:

((Scratch)this).foo((Function0)null.INSTANCE);

we can see that the same code in java in order to handle the higher-order function foo, the compiler generates a new object every time foo is called. Performance wise this is not a great option because it affects the memory by new object memory allocations.

Inline

In order to overcome this memory overhead Kotlin provides us with the inline keyword. So we can add this keyword in the beginning of foo function like this:

inline fun foo(print: () -> Unit) {
    print()
}

With the help of inline keyword the compiler instead of generating new objects each time foo is called, it copy the actual code of foo.print() and replace the new Function call.

Let's provide an example code to understand it better:

public final void testInline() {
      String var1 = "Hello";
      System.out.print(var1);
      Scratch this_$iv = (Scratch)this;
      String var4 = "World";
      System.out.print(var4);
   }

   public final void foo(@NotNull Function0 print) {
      Intrinsics.checkNotNullParameter(print, "print");
      print.invoke();
   }

As we can see instead of the ((Scratch)this).foo((Function0)null.INSTANCE) line, compiler replace it with the actual code of print function

String var4 = "World";
System.out.print(var4);

When to use inline

Inline functions reduce memory overhead by reducing function calls, especially when using higher-order functions inside loops. However, this reduction comes with a trade-off. It's best to inline only small functions. Inlining a large function called from multiple points in the code can significantly increase the size of the generated code.

Crossinline

If we want to return from a higher-order function we must use return with a label like this: return@functionName. This is a local return because it exits only from the higher order function. Non local return is a bare return without any label and it exits the enclosing function instead of anonymous function.

If we try to call a non inline function from an inline function we are going to get a complain from compiler that we can't use inline function here because it may contains non local returns

In order to solve this issue, Kotlin provides us with crossinline keyword. We can use crossinline keyword to lambda parameter inside the inline foo function.

fun testInline() {
    print("Hello")
    foo {
        print("World")
    }
}

inline fun foo(crossinline print: () -> Unit) {
    foo2 {
        print()
    }
}

fun foo2(printNoInline: () -> Unit) {
    printNoInline()
}

In the above example we added a new "foo2" function which is not indicated as inline and we call it inside foo which is an inline function. In order to pass lambda print() inside foo we have to mark print as crossinline. Under the hood the compiler this time is initialise a new function (for foo2) inside foo to address the issue with non local returns because foo2 is no inline.

 public final void foo(@NotNull final Function0 print) {
      int $i$f$foo = 0;
      Intrinsics.checkNotNullParameter(print, "print");
      ((Scratch)this).foo2((Function0)(new Function0() {
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke() {
            this.invoke();
            return Unit.INSTANCE;
         }

         public final void invoke() {
            print.invoke();
         }
      }));
   }

Noinline

If we have a function with multiple lambda parameters and we want to add inline keyword to this function then all the parameters will be indicated as inline.

inline fun foo(print: () -> Unit, print2: () -> Unit) {
    print()
    print2()
}

So in the above code print and print 2 are inlined lambda functions. In many cases we don't want to inline every parameter inside a function. For example let's assume that the print2 is a large function with many lines of code. In this case we can declare print2 as no inline like this

fun testInline() {
    foo( { print("Hello") } ) {
        print("World")
    }
}

inline fun foo(print: () -> Unit, noinline print2: () -> Unit) {
    print()
    print2()
}

In this case the compiler will handle print as an inline parameter and print2 as a noinline parameter.

Let's check the generated code

    public final void testInline() {
      Scratch var1 = (Scratch)this;
      Function0 print2$iv = (Function0)null.INSTANCE;
      int $i$f$foo = false;
      int var4 = false;
      String var5 = "Hello";
      System.out.print(var5);
      print2$iv.invoke();
   }

As we can see the compiler creates a new Function only for print2 lambda parameter

Function0 print2$iv = (Function0)null.INSTANCE;
print2$iv.invoke();

Instead the print1 is treated like an inline parameter

String var5 = "Hello";
System.out.print(var5);

Use cases in Android ๐Ÿ—’๏ธ ๐Ÿ–‹๏ธ

Sometimes it's really useful to add inline in extension functions in order to reduce runtime overhead. Especially in conjunction with refied keyword.

I attach here some examples:

// this code give us the ability to directly use TAG in any object
// in our code and return the class name
inline val <reified T> T.TAG: String
    get() = T::class.java.simpleName
// this generic extension function adds a generic object T in
// lru cache
inline fun <reified T : Any> T?.saveToCache(
applicationContext: BaseApplication,
 objectId: String?
): Boolean =
    this?.let { object ->
        objectId?.let { id ->
            applicationContext.lruCache.put(id, object)
            true
        } ?: false
    } ?: false
// it's useful to add inline in a loop function to reduce the memory
// allocations
inline fun List<String>.loopList(name: (String) -> Unit) {
    this.forEach {
        name(it)
    }
}

Conclusion ๐Ÿ”ฅ

So we have to keep in mind that it's a good idea to use inline:

  • if the outer function includes a lambda function as a parameter and it is relatively small โœ…

  • if we have a small lambda function inside a for loop โœ…

Inline with no functional (lambda) types โœ…
In this point we have to note that we can also use inline with a function with no lambdas inside, but in this case the expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types

๐Ÿ’ก
We must always try to keep a balance between the memory performance and the size of generated code, so it's not wise to overuse inline


You can also check this video about inline functions ๐ŸŽฅ ๐ŸŽฌ


Send your feedback ๐Ÿš€

Feel free to share your feedback with us and let us know if this article has been helpful to you.

Thank you for taking the time to read. Happy coding! ๐Ÿ™๐Ÿผ

ย