In this paper, a permanent update address: https://xiaozhuanlan.com/topic/3458207169

Relearning Kotlin has come to the third installment, which was introduced earlier:

Object, the fastest singleton ever?

Typealias, I don’t recognize you if you wear a vest?

Today’s hero is inline, which is not a Kotlin specific concept, and most programming languages support inline.

The semantics of inline functions are simple: copy and paste the function body into the function call. It is also easy to use, using inline keywords to modify functions.

However, the question is not how to use inline, but when to use inline? Since Kotlin provides inline, it’s certainly there for performance optimization, but is it really a panacea for performance?

Today, let’s dig to the bottom and find the answer.

directory

  1. The essence of the inline
  2. Advise me not to use inline?
  3. Does Java support inlining?
  4. Save the Lambda
  5. How does Java optimize Lambda?
  6. What if you don’t want to inline?
  7. How to return from Lambda?
  8. The last

The essence of the inline

We’ve already said that inline is a function body that is copied and pasted into a function call, which is a compiler trick. In the spirit of scientific rigor, let’s decompile and verify.

inline fun test() {
    println("I'm a inline function")
}
Copy the code

fun run() { test() }

The inline function test() is called in the run() function. Decompile to see the corresponding Java code:

public static final void test(a) {
    String var1 = "I'm a inline function";
    System.out.println(var1);
}

public static final void run(a) {  String var1 = "I'm a inline function";  System.out.println(var1); } Copy the code

You can see that the run() function does not call test() directly, but puts the test() code directly into its own function body. That’s what inline does.

So, here’s the problem. Does this increase operational efficiency? If so, why?

Let’s start with the JVM’s method execution mechanism.

The JVM’s method invocation and method execution depend on stack frames, and the process of each method from invocation to completion corresponds to the process of a stack frame in the virtual machine stack from loading to unloading.

Thread stack frames are stored in the virtual machine stack. Taking the unlined version of the above example code as an example, the corresponding method execution process and corresponding stack frame structure are shown as follows:


Without inlining, the entire execution generates two method stack frames, each containing a local variable table, operand stack, dynamic concatenation, method return address, and some additional information.

In the case of inlining, only one method stack frame is required, reducing the cost of method calls.

At first glance, it does seem to improve the running efficiency, after all, there is one less stack frame.

Yet?

Do not use inline?

Everything looked good, except for the glaring hints the IDE gave me.

Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types

The implication is that using inline here has little or no performance impact. Kotlin’s inlining is best used in function parameter types.

There’s no rush to explain. First, a soul test.

Does Java support inlining?

You can say no, because Java doesn’t provide a way to display inline declarations like inline functions.

But the JVM is supported. Java leaves inline optimization to the virtual machine to avoid developer abuse.

A typical abuse, the inline hyperlength method, greatly increases the bytecode length, but does more harm than good. Note that the inline functions in the Kotlin library are basically short functions.

For ordinary function calls, the JVM already provides adequate inline support. Therefore, in Kotlin, there is no need to use inlining for normal functions, just leave it to the JVM.

In addition, Java code is compiled by Javac, Kotlin code is compiled by Kotlinc, and the JVM can do uniform inline optimization for bytecodes. Therefore, it can be inferred that neither Javac nor Kotlinc has inline optimization at compile time.

As for the JVM’s specific inlining optimization mechanism, I don’t know enough about it to go into too much detail here. If I see relevant materials later, I will supplement them here.

So, the hints from the IDE for developers in the previous section are clear.

The JVM already provides inline support, so there is no need to inline normal functions in Kotlin.

So the question is again. Now that the JVM already supports inline optimization, what is the point of Kotlin’s inline existence? The answer is Lambda.

Save the Lambda

To save Lambda, we first need to know what Kotlin’s Lambda really is to the JVM.

There’s a function called runCatching in the Kotlin library, and I’m going to implement a simplified version of runCatch that takes a function type.

fun runCatch(block: () -> Unit){
    try {
        block()
    }catch (e:Exception){
        e.printStackTrace()
 } }  fun run(a){  runCatch { println("xxx")}} Copy the code

The Java code generated by decompilation looks like this:

public final class InlineKt {
    public static final void runCatch(@NotNull Function0<Unit> block) {
        Intrinsics.checkParameterIsNotNull(block, (String)"block");
        try {
            block.invoke();
 }  catch (Exception e) {  e.printStackTrace();  }  }   public static final void run(a) {  InlineKt.runCatch((Function0<Unit>)((Function0)run.1.INSTANCE));  } }  static final class InlineKt.run1.extends Lambda implements Function0<Unit> {  public static final InlineKt.run.1 INSTANCE = new /* invalid duplicate definition of identical inner class */;   public final void invoke(a) {  String string = "xxx";  boolean bl = false;  System.out.println((Object)string);  }   InlineKt.run.1() {  } } Copy the code

Java compatibility has been Kotlin’s primary goal since its inception. Thus, Kotlin’s approach to Lambda expressions is to compile and generate anonymous classes.

After compiler compilation, Lambda arguments in the runCatch() method are replaced with Function0

types, The actual call to runCatch() in the run() method takes inlinekt.run.1 that implements the Function0

interface and overwrites the invoke() method.

So, when runCatch() is called, an extra class inlinekt.run.1 is created. This is a scenario where Lambda does not capture variables. What happens if you capture variables?

fun runCatch(block: () -> Unit){
    try {
        block()
    }catch (e:Exception){
        e.printStackTrace()
 } }  fun run(a){  var message = "xxx"  runCatch { println(message) } } Copy the code

The external variable Message is captured inside the Lambda, and the corresponding Java code looks like this:

public final class InlineKt {
    public static final void runCatch(@NotNull Function0<Unit> block) {
        Intrinsics.checkParameterIsNotNull(block, (String)"block");
        try {
            block.invoke();
 }  catch (Exception e) {  e.printStackTrace();  }  }   public static final void run(a) {  void message;  Ref.ObjectRef objectRef = new Ref.ObjectRef();  objectRef.element = "xxx";  // The new object is created each time  InlineKt.runCatch((Function0<Unit>)((Function0)new Function0<Unit>((Ref.ObjectRef)message){  final /* synthetic */ Ref.ObjectRef $message;   public final void invoke(a) {  String string = (String)this.$message.element;  boolean bl = false;  System.out.println((Object)string);  }  {  this.$message = objectRef;  super(0);  }  }));  } } Copy the code

If the Lambda captures an external variable, each run will new a Function0

object that holds the value of the external variable. This is worse than if the variable capture had not occurred.

All in all, Kotlin’s Lambda not only increases the size of compiled code for full compatibility with Java6, but also incurs additional runtime overhead. To solve this problem, Kotlin provides the inline keyword.

The purpose of the Kotlin inline function is to eliminate the extra overhead associated with lambda

Add inline to runCatch() :

inline fun runCatch(block: () -> Unit){
    try {
        block()
    }catch (e:Exception){
        e.printStackTrace()
 } }  fun run(a){  var message = "xxx"  runCatch { println(message) } } Copy the code

Decompile to view Java code:

public static final void run(a) {
      Object message = "xxx";
      boolean var1 = false;
      try {
         int var2 = false;
 System.out.println(message);  } catch (Exception var5) {  var5.printStackTrace();  } } Copy the code

The code for runCatch() is directly inlined into the run() method without generating additional classes, eliminating the extra overhead associated with Lambda.

How does Java optimize Lambda?

If Kotlin’s Lambda has a performance problem, so does its Java cousin.

Starting with Java8, Java uses InvokeDynamic to optimize Lambda.

Invokedynamic is used to support dynamic language calls. On the first invocation, it generates a call point and binds the corresponding method handle to that call point. In subsequent calls, run the method handle corresponding to the call point directly. To put it bluntly, the first time you call InvokedDynamic, you find the method that should run there and bind it, and the subsequent runtime tells you exactly which method to run there.

For a detailed introduction to Invokedynamic, read Geek Time’s In Depth Dismantling the Java Virtual Machine, lectures 8 and 9.

How does the JVM implement Invokedynamic? (on)

How does the JVM implement Invokedynamic? (below)

What if you don’t want to inline?

Once a higher-order function is marked inline, its method body and all Lambda arguments are inline.

inline fun test(block1: () -> Unit, block2: () -> Unit) {
    block1()
    println("xxx")
    block2()
}
Copy the code

The test() function is marked inline, so its function body and both Lambda arguments are inline. But because the block1 block I’m passing in is so long (or whatever), I don’t want it to be inline, so USE noinline instead.

inline fun test(noinline block1: () -> Unit, block2: () -> Unit) {
    block1()
    println("xxx")
    block2()
}
Copy the code

This way, block1 is not inlined. I won’t show Java code here for space reasons, but I’m sure you can easily understand Noinline.

How to return from Lambda?

First, regular lambdas do not allow direct use of returns.

fun runCatch(block: () -> Unit) {
    try {
        print("before lambda")
        block()
        print("after lambda")
 } catch (e: Exception) {  e.printStackTrace()  } }  fun run(a) {  // Normal lambda does not allow return  runCatch { return } } Copy the code

The above code will not compile and the IDE will prompt you to return is not allowed here. Inline lets us break through this limitation.

// Mark inline
inline fun runCatch(block: () -> Unit) {
    try {
        print("before lambda")
        block()
 print("after lambda")  } catch (e: Exception) {  e.printStackTrace()  } }  fun run(a) {  runCatch { return } } Copy the code

The above code will compile and run normally. The only difference from the previous example is the addition of inline.

Since return is allowed, what are we doing here to return from the Lambda and continue with the following code? Or do you just end the outer function? Take a look at the result of the run() method.

before lambda
Copy the code

From the running result, the outer function is terminated directly. The return is directly inlined inside the run() method, which is equivalent to calling the return directly inside the run() method. From the decompiled Java code, it is clear.

   public static final void run(a) {
      boolean var0 = false;

      try {
         String var1 = "before lambda";
 System.out.print(var1);  int var2 = false;  } catch (Exception var3) {  var3.printStackTrace();  }  } Copy the code

The compiler simply optimizes the code after return. Such a scenario is called a non-local return.

But sometimes I don’t want to exit the outer function directly, I just want to exit the Lambda, and I can write this.

inline fun runCatch(block: () -> Unit) {
    try {
        print("before lambda")
        block()
        print("after lambda")
 } catch (e: Exception) {  e.printStackTrace()  } }  fun run(a) {  // Return from lambda  runCatch { return@runCatch } } Copy the code

return@label, which will continue with the code after Lambda. Such a scenario is called a local return.

In another scenario, I’m the API designer, and I don’t want the API user to make a non-local return and change my code flow. I also want to use inline, which is actually conflicting. As described earlier, inlining causes the Lambda to allow nonlocal returns.

Crossinline was born to resolve this conflict. It prevents lambda from returning directly from the outer function, while remaining inline.

inline fun runCatch(crossinline block: () -> Unit) {
    try {
        print("before lambda")
        block()
        print("after lambda")
 } catch (e: Exception) {  e.printStackTrace()  } }  fun run(a) {  runCatch { return } } Copy the code

After adding Crossinline, the above code will not compile. However, the following code can still be compiled and run.

inline fun runCatch(crossinline block: () -> Unit) {
    try {
        print("before lambda")
        block()
        print("after lambda")
 } catch (e: Exception) {  e.printStackTrace()  } }  fun run(a) {  runCatch { return@runCatch } } Copy the code

Crossinline prevents non-local returns, but it doesn’t prevent local returns, and it doesn’t have to.

The last

So I’ve said so much in one breath about inline functions, so to summarize.

In Kotlin, inline functions are used to compensate for the extra running overhead of lambdas in higher-order functions. For normal functions, there is no need to use inlining because the JVM already provides some support for inlining.

Using noinline for the specified Lambda argument prevents the Lambda from being inlined.

Regular lambdas do not support non-local returns, but allow non-local returns after inlining. To both inline and disable non-local returns, use Crossinline.

In addition to inline functions, Kotlin 1.3 started supporting inline classes, but this was an experimental API that required manual compiler support to be turned on. I don’t know if you have any unique views on the inline class, please feel free to share them in the comments section.

This article is formatted using MDNICE

Here is bingxin said, pay attention to me, do not get lost!