Blog Infos
Author
Published
Topics
, ,
Published

I conduct a lot of technical interviews and see that many developers do not understand the benefits of using inline functions . Why is crossinline needed and how reified works. Part of the source of common misconceptions about inline functions is that there used to be an inaccurate description on the kotlinlang.org website. I want to fix this and clearly show how inline functions work and what benefits we get from using them.

Popular misconception: inline functions save stack.

If you try to create an inline function like this

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

Then the compiler will give you a warning “Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types.” Which roughly means that the JIT compiler itself is excellent at embedding code and there is no need to try to help it with this.

Inline functions should only be used when passing parameters of a functional type to the function.

This example demonstrates very well the fallacy of this misconception. Inline functions do not save the stack, or rather, that is not their essence.

Inline should only be used if you are passing a functional type parameter to your function.

Popular misconception: inline functions save the number of methods.

Let’s see what our inline function compiles into in Java

inline fun inlineFun(body: () -> String) {
     println("inline func code, " + body.invoke())
 }

 fun testInline() {
     inlineFun { "external code" }
 }

If you look at the decompiled Java code you will see the following

public final void inlineFun(Function0 body) {
     String var2 = "inline func code, " + (String)body.invoke();
     System.out.println(var2);
 }

 public final void testInline() {
     String var1 = (new StringBuilder())
             .append("inline func code, ")
             .append("external code")
             .toString();
     System.out.println(var1);
 }

As you can see, the inline function code has been inlined in place of the calling function, but despite this, the original inlineFun function remains in the source code.

The original function was retained specifically to maintain compatibility with Java. After all, you can call kotlin functions from Java code, but Java doesn’t know anything about inlining.

This example shows very well that inlining does not help us reduce the number of methods.

Profit of inline functions

In order to understand what kind of profit you get from using inline functions, let’s look at an example of calling an inline function and a regular function.

private inline fun inlineFun(body: () -> String) {
     println("inline func code, " + body.invoke())
 }

 fun testInline() {
     inlineFun { "external inline code" }
 }

 private fun regularFun(body: () -> String) {
     println("regular func code, " + body.invoke())
 }

 fun testRegular() {
     regularFun { "external regular code" }
 }

If you look at the decompiled Java code, you see the following (I’m going to simplify the decompiled Java code a bit so as not to overload you with unnecessary variables and Kotlin checks)

public final void testInline() {
    String var4 = (new StringBuilder())
            .append("inline func code, ")
            .append("external inline code")
            .toString();
    System.out.println(var4);
}

public final void testRegular() {
    Function0 body = (Function0)(new Function0() {
        public final String invoke() {
            return "external regular code";
        }
    });
    this.regularFun(body);
}

The main difference between calls to an inline function and a regular function is that to call a regular function in Java, an instance of the anonymous class body is created that implements our lambda and its instance is passed to the regular function.

public final void testRegular() {
       Function0 body = (Function0)(new Function0() {
           public final String invoke() {
               return "external regular code";
           }
       });
       this.regularFun(body);
   }

In the case of an inline function, the calling code and the inline function code are combined and inserted directly into the call location, which eliminates the need to create an anonymous class for the passed lambda.

public final void testInline() {
     String var4 = (new StringBuilder())
             .append("inline func code, ")
             .append("external inline code")
             .toString();
     System.out.println(var4);
 }

Creating an anonymous class in Java is a costly operation and this is precisely the benefit of inline functions.

Inline functions allow you to eliminate the creation of anonymous classes for passing lambdas to function parameters

Profit measurement from inline functions

To demonstrate this clearly in numbers, let’s run a small test.

@State(Scope.Benchmark)
@Fork(1)
@Warmup(iterations = 0)
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
class InlineTest {

    private inline fun inlineFun(body: () -> Int): Int {
        return body() + Random.nextInt(1000 )
    }

    private fun nonInlineFun(body: () -> Int): Int {
        return body() + Random.nextInt(1000 )
    }

    @Benchmark
    fun inlineBenchmark(): Int {
        return inlineFun { Random.nextInt(1000 ) }
    }

    @Benchmark
    fun nonInlineBenchmark(): Int {
        return nonInlineFun { Random.nextInt(1000 ) }
    }
}

As you can see, the test measurement results clearly show that in this particular test, the inline function works almost one and a half times faster. In this case, the entire profit was obtained solely due to the refusal to create anonymous classes for our lambda

This doesn’t mean that inline functions are always 1.5X times faster. This test only shows that creating anonymous classes has additional overhead and inline functions allow you to avoid them.

Crossinline

To understand the essence of crossinline, let’s look at the following example. Here we create a local lambda func inside the inline function and use the incoming body parameter inside it. And then we pass our local lambda func outside the inline function, into the regular function regularFun.

private inline fun crossInlineFun(body: () -> String) {
       val func = {
           "crossInline func code, " + body.invoke()
       }
       regularFun(func)
   }

If you write such code, you will get a compiler error. This is because the compiler can’t inline your function since it uses the incoming body lambda inside a local func lambda. In the case of inlining, we do not have an anonymous class for body and we cannot pass it to the local lambda.

But we can mark our parameter as noinline and in this case everything will compile. When we mark a parameter as noinline, an anonymous class will be created for it and can be passed to the local lambda.

private inline fun crossInlineFun(noinline body: () -> String) {
    val func = {
        "crossInline func code, " + body.invoke()
    }
    regularFun(func)
}

fun testCrossInline() {
    crossInlineFun { "external code" }
}

Let’s look at the decompiled Java code for this case.

public final void testCrossInline() {
    Function0 body = (Function0)(new Function0() {
        public final String invoke() {
            return "external code";
        }
    });
    Function0 func = (Function0)(new Function0() {
        public final String invoke() {
            return "crossInline func code, " + (String)body.invoke();
        }
    });
    regularFun(func);
}

As you can see, the inline function was built into the place where it was called, but since the parameter is marked as noinline, we lost all the profit from inlining. We create two anonymous classes and the second anonymous class func calls the first anonymous class body.

Now let’s mark our parameter as crossinline and see how the Java code changes.

private inline fun crossInlineFun(crossinline body: () -> String) {
    val func = {
        "crossInline func code, " + body.invoke()
    }
    regularFun(func)
}

fun testCrossInline() {
    crossInlineFun { "external code" }
}

Let’s look at the decompiled Java code for the crossinline case.

public final void testCrossInline() {
      Function0 func = (Function0)(new Function0() {
          public final String invoke() {
              return (new StringBuilder())
                      .append("crossInline func code, ")
                      .append("external code")
                      .toString();
          }
      });
      regularFun(func);
  }

As you can see, in the case of crossinline, we have only one anonymous class, which combines the inline function code and the external lambda code.

When we use an incoming functional type parameter in a local lambda, adding crossinline eliminates the creation of an additional anonymous class. Instead of two anonymous classes, only one will be created.

Reified

The documentation states that adding this parameter allows you to know inside the inline function the type of generic being passed.

Many people think that there is some kind of kotlin magic here that cancels type erasure for Java generics. But in fact, reified is just a side effect of code inlining and there is no magic here.

To demonstrate this, let’s put this magic under a microscope. If you try to write such code, you will receive a compilation error “Cannot use ‘T’ as reified type parameter. Use a class instead.”.

inline fun <reified T> genericInline(param: T) {
       println("my type is " + param!!::class.java.simpleName)
   }
   
  fun externalGenericCall() {
       testReifiedCall("I'm a String, but I'm an external generic")
   }

   fun <T> testReifiedCall(externalGeneric: T) {
      genericInline(externalGeneric)
      genericInline("I'm a String and I'm not generic here")
   }

Essentially, this error warns you that where the inline function is called, the type of the parameter externalGeneric is unknown and you cannot use the inline function with a reified parameter.

Previously, before the release of kotlin 1.6, such code compiled perfectly and people received errors at runtime and created an issue that the reified parameter did not work correctly. Since kotlin 1.6, a special compilation error has been added that checks for this case and protects us from it.

To understand that such code cannot work correctly, it is enough to understand the principle of operation of inline functions. The code of your inline function is combined with the code that calls your function. Naturally, at the place where the function is called, all the local types are known.

But if you try to use a variable whose type is unknown where your function is called, then naturally you will end up with an Object type for it.

To understand this better, let’s look at the decompiled Java code for this case.

public final void externalGenericCall() {
    this.testReifiedCall("I'm a String, but I'm an external generic");
}

public final <T> void testReifiedCall(T externalGeneric) {
    // We will get the type Object here instead of the expected String
    // because it is an external generic and its type is unknown here
    String var5 = (new StringBuilder())
        .append("my type is ")
        .append(externalGeneric.getClass().getSimpleName())
        .toString();
    System.out.println(var5);

    // Here we will get the correct type because its type is known here.
    String localGeneric = "I'm a String and I'm not generic here";
    var5 = (new StringBuilder())
        .append("my type is ")
        .append(localGeneric.getClass().getSimpleName())
        .toString();
    System.out.println(var5);
}

From this code, it becomes clear that the kotlin compiler has to do extra work and force type clearing for generics of inline functions if they are not marked with the reified keyword. And the ability to recognize local types of generics was left as a useful side effect of inlining, and the reified keyword was introduced for this purpose.

Conclusions

Inline functions should be used if you are writing a generic function or if your function is expected to be used in loops. This will allow you to make your function a little faster.

The body of your inline function should not be large. Otherwise, you will increase the amount of code, because the code of an inline function is copied to each place where it is called.

Inline functions should only be used when passing function type parameters.

The whole benefit of inline functions lies in the abandonment of anonymous classes for passing lambdas to function parameters.

If you are interested in how kotlin works under the hood, you can read my other articles about kotlin.

This article was previously published on proandroiddev.com

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu