Stripping the pants of Kotlin intrinsic functions by looking at bytecode instructions

Exposing Kotlin intrinsic functions by looking at bytecode instructions

There are many articles on the Internet about Kotlin inline functions. Most of them tell you the conclusion. Just use the xxx keyword and forget about it after a while. This article will guide you. From the JVM bytecode, I will take you step by step to analyze its principles.

Ordinary function calls

I defined a class like this:

package com.tans.test

object Main {<!-- -->

    @JvmStatic
    fun main(args: Array<String>) {<!-- -->
        foo1()
    }

    fun foo1() {<!-- -->
        val data: Int = 1
        val returnData = foo2(data) {<!-- -->
            println("Callback: do something")
        }
        println("Foo1: returnData=$returnData")
    }

    fun <T> foo2(data: T, callback: () -> Unit): T {<!-- -->
        println("Foo2: do something")
        callback()
        return data
    }
}

The code is very simple. We mainly analyze the functions foo1() and foo2(). The tool used to view the bytecode is jclasslib. Not much nonsense. Say go directly to the bytecode.

foo1() Function bytecode instructions:

 0 iconst_1
 1istore_1
 2 load_0
 3 iload_1
 4 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
 7 getstatic #40 <com/tans/test/Main$foo1$returnData$1.INSTANCE : Lcom/tans/test/Main$foo1$returnData$1;>
10 checkcast #42 <kotlin/jvm/functions/Function0>
13 invokevirtual #46 <com/tans/test/Main.foo2 : (Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;>
16 checkcast #48 <java/lang/Number>
19 invokevirtual #52 <java/lang/Number.intValue: ()I>
22istore_2
23 ldc #54 <Foo1: returnData=>
25 iload_2
26 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
29 invokestatic #58 <kotlin/jvm/internal/Intrinsics.stringPlus: (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;>
32 astore_3
33 iconst_0
34 istore 4
36 getstatic #64 <java/lang/System.out : Ljava/io/PrintStream;>
39 aload_3
40 invokevirtual #70 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
43 return

I analyze some bytecodes that I think are valuable:

4 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>

Before entering the parameters, the int variable will be converted into an Integer object through the static method of Integer.valueOf(). This is what we often say of packing.

7 getstatic #40 <com/tans/test/Main$foo1$returnData$1.INSTANCE : Lcom/tans/test/Main$foo1$returnData$1;>
10 checkcast #42 <kotlin/jvm/functions/Function0>
13 invokevirtual #46 <com/tans/test/Main.foo2 : (Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;>
16 checkcast #48 <java/lang/Number>
19 invokevirtual #52 <java/lang/Number.intValue: ()I>
22istore_2

Here you will get a Main$foo1$returnData$1 static singleton object, which is actually our labmda expression object, and then cast it into Function0 object, then push it onto the stack, and then call the foo2() function. Let’s take a closer look at the signature of foo2() function, com/tans/test/Main .foo2: (Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;, passing in a Object and Fuction0 (In Kotlin, it means lambda without parameters). The return value is also Object. What we clearly define is to pass in a paradigm. Return is also a paradigm, so why is it replaced by Object? In fact, this is what is often called paradigm erasure. The paradigms in JVM are pseudo-paradigms. The paradigms in runtime methods are all implemented through forced conversion. This also explains the common method. You cannot get its class object through T::class, even if you get it, it will be Object‘s class object. Finally, the return value will be coerced into a Number object, and then its intValue() method will be called to complete the unboxing operation.

23 ldc #54 <Foo1: returnData=>
25 iload_2
26 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
29 invokestatic #58 <kotlin/jvm/internal/Intrinsics.stringPlus: (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;>
32 astore_3
33 iconst_0
34 istore 4
36 getstatic #64 <java/lang/System.out : Ljava/io/PrintStream;>
39 aload_3
40 invokevirtual #70 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
43 return

Here, the Intrinsics#stringPlus method is used to combine the return values of the "Foo1: returnData=" and foo2() methods to form a new String object, and then call the System.out#println() method to print to the console.

Let’s take a look at the lambda object mentioned above. Its class name is com/tans/test/Main$foo1$returnData$1. Let’s take a look at its invoke() method bytecode instructions:

 0 ldc #17 <Callback: do something>
 2 astore_1
 3 iconst_0
 4 istore_2
 5 getstatic #23 <java/lang/System.out : Ljava/io/PrintStream;>
 8 aload_1
 9 invokevirtual #29 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
12 return

This bytecode instruction is very simple. It loads Callback: do something directly from the constant pool and then calls the System.out#println() method to print to the console.

Let’s take a look at the bytecode instructions of the foo2() method:

 0 aload_2
 1 ldc #76 <callback>
 3 invokestatic #22 <kotlin/jvm/internal/Intrinsics.checkNotNullParameter: (Ljava/lang/Object;Ljava/lang/String;)V>
 6 ldc #78 <Foo2: do something>
 8 astore_3
 9 iconst_0
10 istore 4
12 getstatic #64 <java/lang/System.out : Ljava/io/PrintStream;>
15 aload_3
16 invokevirtual #70 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
19 aload_2
20 invokeinterface #82 <kotlin/jvm/functions/Function0.invoke: ()Ljava/lang/Object;> count 1
25 pop
26 aload_1
27 areturn

The method first calls the System.out#println() method to print Foo2: do something, and then calls the second parameter Function0#invoke() method , that is, calling the lambda object, and finally returning the first input parameter as the return value.

The analysis of bytecode instructions for ordinary function calls is over. From the perspective of bytecode instructions, we looked at the boxing and unboxing of int and the of Kotlin lambda implementation and paradigm erasure.

Inline functions

Ordinary inline functions

In Kotlin, if you want the function to add an inline keyword to the inline function, we transform the above foo2() function into an inline function:

 // ...
    inline fun <T> foo2(data: T, callback: () -> Unit): T {<!-- -->
        println("Foo2: do something")
        callback()
        return data
    }
    // ...

Then let’s take a look at the bytecode instructions of the foo1() method:

 0 iconst_1
 1 istore_1
 2 load_0
 3 astore_3
 4 iload_1
 5 istore 4
 7 iconst_0
 8 istore 5
10 ldc #31 <Foo2: do something>
12 astore 6
14 iconst_0
15 istore 7
17 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
20 load 6
22 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
25 iconst_0
26 istore 8
28 ldc #45 <Callback: do something>
30 astore 9
32 iconst_0
33 istore 10
35 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
38 load 9
40 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
43 nop
44 iload 4
46 istore_2
47 ldc #47 <Foo1: returnData=>
49 iload_2
50 invokestatic #53 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
53 invokestatic #57 <kotlin/jvm/internal/Intrinsics.stringPlus: (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;>
56 astore_3
57 iconst_0
58 istore 4
60 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
63 aload_3
64 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
67 return

A brief glance at this bytecode shows that the foo2() method is not called, and the labmda object is not constructed. Let’s briefly analyze its process. Due to the The local variable table has become a bit complicated. I will post the contents of its table.

Slot Value
0 this(Main object)
1 1
2
3 this(Main object)
4 1
5 0
6 “Foo2: do something”
7 0
8 0
9 “Callback: do something”
10 0

I was confused when I first saw this local variable table. I found that many of the objects stored in it were duplicates, and Slot 2 was still empty. I didn’t know whether this was Kotlin The compiler did not optimize the inline function, or it was done intentionally for other reasons.

Let’s take a look at what the bytecode does step by step.

10 ldc #31 <Foo2: do something>
12 astore 6
14 iconst_0
15 istore 7
17 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
20 load 6
22 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>

Print Foo2: do something to the console.

25 iconst_0
26 istore 8
28 ldc #45 <Callback: do something>
30 astore 9
32 iconst_0
33 istore 10
35 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
38 load 9
40 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>

Print Callback: do something to the console.

47 ldc #47 <Foo1: returnData=>
49 iload_2
50 invokestatic #53 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
53 invokestatic #57 <kotlin/jvm/internal/Intrinsics.stringPlus: (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;>
56 astore_3
57 iconst_0
58 istore 4
60 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
63 aload_3
64 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>

Print Foo1: returnData= + 1 to the console.

The above bytecode is equivalent to the following source code:

    // ...
    fun foo1() {<!-- -->
        val data: Int = 1
        println("Foo2: do something")
        println("Callback: do something")
        println("Foo1: returnData=$data")
    }
    // ...


Based on the above results, we make a summary of the inline function. It will move the bytecode in the inline function directly to the current function for execution, including the instructions in lambda. Doing this can reduce the creation of method stack frames and lambda objects at runtime, and can improve program performance under certain conditions; but the disadvantages are also very obvious. If there are many places to call, and at the same time, The logic of the connection function is relatively complex, which will cause the space occupied by class to increase significantly, because its bytecode will be copied to all called methods.

Add the reified keyword to the template

Let’s just say the conclusion here. If you don’t add the reified keyword, the compiled bytecode instructions will be exactly the same. Haha, I didn’t expect that after the reified keyword is marked. The paradigm is a class object that can directly obtain the paradigm object directly in the inline function.
Then we modify the foo2() function to the following:

    // ...
    inline fun <reified T> foo2(data: T, callback: () -> Unit): T {<!-- -->
        valclazz = data!!::class.java
        println("Foo2: do something")
        callback()
        return data
    }
    // ...


If there is no reified keyword here, data!!::class.java cannot be compiled.

Take a look at the bytecode:

 0 iconst_1
 1 istore_1
 2 load_0
 3 astore_3
 4 iload_1
 5 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
 8 astore 4
10 iconst_0
11 istore 5
13 load 4
15 invokevirtual #39 <java/lang/Object.getClass : ()Ljava/lang/Class;>
18 astore 6
20 ldc #41 <Foo2: do something>
22 astore 7
24 iconst_0
25 istore 8
27 getstatic #47 <java/lang/System.out : Ljava/io/PrintStream;>
30 load 7
32 invokevirtual #53 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
35 iconst_0
36 istore 9
38 ldc #55 <Callback: do something>
40 astore 10
42 iconst_0
43 istore 11
45 getstatic #47 <java/lang/System.out : Ljava/io/PrintStream;>
48 aload 10
50 invokevirtual #53 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
53 nop
54 load 4
56 checkcast #57 <java/lang/Number>
59 invokevirtual #61 <java/lang/Number.intValue: ()I>
62 istore_2
63 ldc #63 <Foo1: returnData=>
65 iload_2
66 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
69 invokestatic #67 <kotlin/jvm/internal/Intrinsics.stringPlus: (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;>
72 astore_3
73 iconst_0
74 istore 4
76 getstatic #47 <java/lang/System.out : Ljava/io/PrintStream;>
79 aload_3
80 invokevirtual #53 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
83 return

There is no special operation compared to the ordinary inline function above. It just adds the logic of obtaining the class object, as follows:

 5 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
 8 astore 4
10 iconst_0
11 istore 5
13 load 4
15 invokevirtual #39 <java/lang/Object.getClass : ()Ljava/lang/Class;>
18 astore 6

Directly call the getClass() method of the Integer object to obtain the class object.
At present, it seems that the reified keyword has no special role in bytecode instructions. I guess that reified is just a marking function, which may be related to bytecode optimization.

Add crossinline keyword to lambda parameters

Here is a direct conclusion: crossinline does not modify the bytecode instructions and is the same as the reified keyword. Surprise or surprise, hahahahaha. So what is it used for? This keyword is used to control return in lambda.
Suppose I use return in the above lambda, such as the following code:

    // ...
    fun foo1() {<!-- -->
        val data: Int = 1
        val returnData = foo2(data) {<!-- -->
            println("Callback: do something")
            return
        }
        println("Foo1: returnData=$returnData")
    }

    inline fun <T> foo2(data: T, callback: () -> Unit): T {<!-- -->
        println("Foo2: do something")
        callback()
        return data
    }
    // ...


Due to the inline feature, the above method will directly return the foo1() function. If you want to return lambda, you have to use return@foo2 ; If it is not an inline function, it is forbidden to return the foo1() function in lambda of foo2().

crossinline is used to limit the return method of the above example. It cannot return foo1()< in foo2()‘s labmda /code> function.

If I add a foo3() function as follows:

    // ...
        fun foo1() {<!-- -->
        val data: Int = 1
        val returnData = foo2(data) {<!-- -->
            println("Callback: do something")
        }
        println("Foo1: returnData=$returnData")
    }

    inline fun <T> foo2(data: T, callback: () -> Unit): T {<!-- -->
        foo3(callback)
        println("Foo2: do something")
        callback()
        return data
    }

    inline fun foo3(crossinline callback: () -> Unit) {<!-- -->
        
    }
    // ...


In fact, the above code cannot be compiled because foo2() does not add crossinline, but foo3() does. crossinline, but foo2() passes lambda to foo3(), then foo2() The definitions of code> and foo3() conflict. One allows return and the other does not allow return, so foo2() and foo3() must both have no crossinline or both have crossinline.

Add noinline keyword to lambda parameters

I modified the code in the Ordinary Inline Functions chapter to be as follows:

    // ...
    inline fun <T> foo2(data: T, noinline callback: () -> Unit): T {<!-- -->
        println("Foo2: do something")
        callback()
        return data
    }
    // ...


The corresponding bytecode has changed this time, please refer to the following:

 0 iconst_1
 1istore_1
 2 load_0
 3 astore_3
 4 iload_1
 5 istore 4
 7 getstatic #34 <com/tans/test/Main$foo1$returnData$1.INSTANCE : Lcom/tans/test/Main$foo1$returnData$1;>
10 checkcast #36 <kotlin/jvm/functions/Function0>
13 astore 5
15 iconst_0
16 istore 6
18 ldc #38 <Foo2: do something>
20 astore 7
22 iconst_0
23 istore 8
25 getstatic #44 <java/lang/System.out : Ljava/io/PrintStream;>
28 load 7
30 invokevirtual #50 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
33 load 5
35 invokeinterface #54 <kotlin/jvm/functions/Function0.invoke: ()Ljava/lang/Object;> count 1
40 pop
41 iload 4
43 istore_2
44 ldc #56 <Foo1: returnData=>
46 iload_2
47 invokestatic #62 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
50 invokestatic #66 <kotlin/jvm/internal/Intrinsics.stringPlus: (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;>
53 astore_3
54 iconst_0
55 istore 4
57 getstatic #44 <java/lang/System.out : Ljava/io/PrintStream;>
60 aload_3
61 invokevirtual #50 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
64 return

After adding noinline, the inlining of lambda will be prohibited, and it will be an object like the ordinary lambda.

In some cases, we cannot let lambda be inlined, but need a lambda object, such as the following code:

    // ...
    inline fun <T> foo2(data: T, callback: () -> Unit): T {<!-- -->
        foo3(callback)
        println("Foo2: do something")
        callback()
        return data
    }

    fun foo3(callback: () -> Unit) {<!-- -->

    }
    // ...


The above code cannot be compiled. foo2() is an inline function, but foo3() is not. foo2() will convert lambda is inlined, and the lambda required by foo3() must be an object. In order to pass the compilation, it can be passed in foo2( ) in callback plus noinline to prevent it from being inlined, so that foo2() and foo3() are all non-inline lambda objects, so they can be compiled.

Summary

inline

Adding the inline keyword before a function enables the function to be inlined, including the lambda parameter. The bytecode instructions will be equivalently moved to the called function. The advantage is that it can improve the running efficiency of the program to a certain extent; the disadvantage is that it will cause the bytecode file to become larger. Use it reasonably.

reified

Added to the paradigm in the inline function, it allows the added paradigm object to directly obtain the Class object. This keyword will not modify the bytecode instructions.

crossinline

Add in the lambda parameter of the inline function to disable the inline lambda to return the method of the previous level function. If the inline function puts this crossinline‘s lambda When the parameter is passed to another inline function as a parameter, then the lambda parameter in the newly called inline function must also be crossinline of. It’s a bit confusing to read, but IDEA will prompt you anyway. It also does not modify bytecode instructions.

noinline

Add it to the lambda parameter of the inline function to disable the lambda parameter from being inlined. It is used when objectified lambda objects are needed in certain situations. For example, when you want to pass its lambda parameter to an ordinary function in an inline function, you need to disable inlining.

Finally

If you want to become an architect or want to break through the 20-30K salary range, then don’t be limited to coding and business, you must be able to select and expand, and improve your programming thinking. In addition, good career planning is also very important, and learning habits are important, but the most important thing is to be able to persevere. Any plan that cannot be implemented consistently is empty talk.

If you have no direction, here is a set of “Advanced Notes on the Eight Modules of Android” written by a senior architect at Alibaba to help you systematically organize messy, scattered, and fragmented knowledge, so that you can systematically and efficiently Master various knowledge points of Android development.
img
Compared with the fragmented content we usually read, the knowledge points in this note are more systematic, easier to understand and remember, and are strictly arranged according to the knowledge system.

Everyone is welcome to support with one click and three links. If you need the information in the article, just scan the CSDN official certification WeChat card at the end of the article to get it for free ↓↓↓ (There is also a small bonus of the ChatGPT robot at the end of the article, don’t miss it)

PS: There is also a ChatGPT robot in the group, which can answer everyone’s work or technical questions

image