📜 ⬆️ ⬇️

Kotlin, bytecode compilation and performance (part 2)



This is a continuation of the publication. The first part can be found here.

Content:


Cycles
When
Delegates
Object and companion object
lateinit properties
coroutines
findings

Cycles:


Kotlin lacks a classic three-part for, as in Java. This may seem a problem to someone, but if you look at all the cases of using such a cycle in more detail, you can see that for the most part it is used just for enumerating values. Kotlin has a simplified construction to replace it.
')
//Kotlin fun rangeLoop() { for (i in 1..10) { println(i) } } 

1..10 here is the range for which the iteration occurs. The Kotlin compiler is smart enough, it understands what we are going to do in this case, and therefore removes all the extra overhead. The code is compiled into a regular while loop with a loop counter variable. No iterators, no overhead projector, everything is quite compact.

 //Java public static final void rangeLoop() { int i = 1; byte var1 = 10; if(i <= var1) { while(true) { System.out.println(i); if(i == var1) { break; } ++i; } } } 

A similar loop through the array (which is written to Kotlin as Array <*>) is compiled in a similar way into a for loop.

 //Kotlin fun arrayLoop(x: Array<String>) { for (s in x) { println(s) } } 

 //Java public static final void arrayLoop(@NotNull String[] x) { Intrinsics.checkParameterIsNotNull(x, "x"); for(int var2 = 0; var2 < x.length; ++var2) { String s = x[var2]; System.out.println(s); } } 

A slightly different situation arises when the enumeration of elements from the list occurs:

 //Kotlin fun listLoop(x: List<String>) { for (s in x) { println(s) } } 

In this case, you have to use an iterator:

 //Java public static final void listLoop(@NotNull List x) { Intrinsics.checkParameterIsNotNull(x, "x"); Iterator var2 = x.iterator(); while(var2.hasNext()) { String s = (String)var2.next(); System.out.println(s); } } 

Thus, depending on which elements are searched, the Kotlin compiler itself chooses the most efficient way to convert a cycle into bytecode.

The following is a comparison of performance for loops with similar solutions in Java:

Cycles




As you can see, the difference between Kotlin and Java is minimal. Baytkod turns out very close to that generates javac. According to the developers, they still plan to improve this in the next versions of Kotlin so that the resulting bytecode will be as close as possible to the patterns that javac generates.

When


When is an analog switch from Java, only with more functionality. Consider the following examples and what they are compiled into:

 /Kotlin fun tableWhen(x: Int): String = when(x) { 0 -> "zero" 1 -> "one" else -> "many" } 

For such a simple case, the resulting code is compiled into an ordinary switch; no magic happens here:

 //Java public static final String tableWhen(int x) { String var10000; switch(x) { case 0: var10000 = "zero"; break; case 1: var10000 = "one"; break; default: var10000 = "many"; } return var10000; } 

If, however, slightly change the example above, and add constants:

 //Kotlin val ZERO = 1 val ONE = 1 fun constWhen(x: Int): String = when(x) { ZERO -> "zero" ONE -> "one" else -> "many" } 

That code in this case is already compiled into the following form:

 //Java public static final String constWhen(int x) { return x == ZERO?"zero":(x == ONE?"one":"many"); } 

This is because at the moment the Kotlin compiler does not understand that the values ​​are constants, and instead of converting to switch, the code is converted to a set of comparisons. Therefore, instead of the constant time, a transition to a linear one takes place (depending on the number of comparisons). According to the developers of the language, in the future this can be easily corrected, but in the current version it is still so.

It is also possible to use the const modifier for constants known at compile time.
 //Kotlin ( When2.kt) const val ZERO = 1 const val ONE = 1 fun constWhen(x: Int): String = when(x) { ZERO -> "zero" ONE -> "one" else -> "many" } 

Then, in this case, the compiler already correctly optimizes when:
 public final class When2Kt { public static final int ZERO = 1; public static final int ONE = 2; @NotNull public static final String constWhen(int x) { String var10000; switch(x) { case 1: var10000 = "zero"; break; case 2: var10000 = "one"; break; default: var10000 = "many"; } return var10000; } } 

If we replace constants with Enum:

 //Kotlin ( When3.kt) enum class NumberValue { ZERO, ONE, MANY } fun enumWhen(x: NumberValue): String = when(x) { NumberValue.ZERO -> "zero" NumberValue.ONE -> "one" NumberValue.MANY -> "many" } 

That code, as well as in the first case, will be compiled into a switch (almost the same as in the case of enum enumeration in Java).

 //Java public final class When3Kt$WhenMappings { // $FF: synthetic field public static final int[] $EnumSwitchMapping$0 = new int[NumberValue.values().length]; static { $EnumSwitchMapping$0[NumberValue.ZERO.ordinal()] = 1; $EnumSwitchMapping$0[NumberValue.ONE.ordinal()] = 2; $EnumSwitchMapping$0[NumberValue.MANY.ordinal()] = 3; } } public static final String enumWhen(@NotNull NumberValue x) { Intrinsics.checkParameterIsNotNull(x, "x"); String var10000; switch(When3Kt$WhenMappings.$EnumSwitchMapping$0[x.ordinal()]) { case 1: var10000 = "zero"; break; case 2: var10000 = "one"; break; case 3: var10000 = "many"; break; default: throw new NoWhenBranchMatchedException(); } return var10000; } 

The ordinal number of the element determines the branch number in the switch, which is followed by the selection of the desired branch.

Let's look at the performance comparison of solutions on Kotlin and Java:

When




As you can see, a simple switch works the same way. In the case when the Kotlin compiler could not determine that the variables are constants and turned to comparisons, Java runs a little faster. And in a situation where we enumerate enum values, there is also a small loss for fussing with the definition of the branch by the value of ordinal. But all these shortcomings will be corrected in future versions, and besides, the loss in performance is not very large, and in critical places you can rewrite the code to another option. Quite reasonable price for usability.

Delegates


Delegation is a good alternative to inheritance, and Kotlin supports it right out of the box. Consider a simple example with class delegation:

 //Kotlin package examples interface Base { fun print() } class BaseImpl(val x: Int) : Base { override fun print() { print(x) } } class Derived(b: Base) : Base by b { fun anotherMethod(): Unit {} } 

The Derived class in the constructor receives an instance of the class that implements the Base interface, and in turn delegates the implementation of all methods of the Base interface to the transmitted instance. The decompiled code of the Derived class will look like this:

 public final class Derived implements Base { private final Base $$delegate_0; public Derived(@NotNull Base b) { Intrinsics.checkParameterIsNotNull(b, "b"); super(); this.$$delegate_0 = b; } public void print() { this.$$delegate_0.print(); } public final void anotherMethod() { } } 

An instance of the class is passed to the class constructor, which is stored in the immutable internal field. The Base interface's print method is also redefined, where the method is simply called from the delegate. Everything is quite simple.

It is also possible to delegate not only the implementation of the entire class, but also its individual properties (and from version 1.1 it is still possible to delegate initialization to local variables).

Code on Kotlin:

 //Kotlin class DeleteExample { val name: String by Delegate() } 

Compiled into code:

 public final class DeleteExample { @NotNull private final Delegate name$delegate = new Delegate(); static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.property1(new PropertyReference1Impl(Reflection.getOrCreateKotlinClass(DeleteExample.class), "name", "getName()Ljava/lang/String;"))}; @NotNull public final String getName() { return this.name$delegate.getValue(this, $$delegatedProperties[0]); } } 

When the DeleteExample class is initialized, an instance of the Delegate class is created, which is stored in the name field $ delegate. And then the getName function call is redirected to the getValue function call from name $ delegate.

Kotlin already has several standard delegates:

- lazy, for lazy evaluation of the field value.
- observable, which allows you to receive notifications about all changes in the field
- map used to initialize field values ​​from Map values.

Object and companion object


Kotlin has no static modifier for methods and fields. Instead, for the most part, it is recommended to use functions at the file level. If you need to declare functions that can be called without an instance of the class, then for this there is an object and a companion object. Let's look at examples of how they look in bytecode:

A simple object declaration with one method is as follows:

 //Kotlin object ObjectExample { fun objectFun(): Int { return 1 } } 

In the code, you can then access the objectFun method without creating an ObjectExample instance. The code is compiled into almost canonical singleton:

 public final class ObjectExample { public static final ObjectExample INSTANCE; public final int objectFun() { return 1; } private ObjectExample() { INSTANCE = (ObjectExample)this; } static { new ObjectExample(); } } 

And the place of the call:

 //Kotlin val value = ObjectExample.objectFun() 

Compiled to the INSTANCE call:

 //Java int value = ObjectExample.INSTANCE.objectFun(); 

The companion object is used to create similar methods only already in the class for which it is supposed to create instances.

 //Kotlin class ClassWithCompanion { val name: String = "Kurt" companion object { fun companionFun(): Int = 5 } } //method call ClassWithCompanion.companionFun() 

Calling the companionFun method also does not require creating an instance of the class, and in Kotlin it will look like a simple call to a static method. But in fact there is an appeal to the companion class. Let's see the decompiled code:

 //Java public final class ClassWithCompanion { @NotNull private final String name = "Kurt"; public static final ClassWithCompanion.Companion Companion = new ClassWithCompanion.Companion((DefaultConstructorMarker)null); @NotNull public final String getName() { return this.name; } public static final class Companion { public final int companionFun() { return 5; } private Companion() { } public Companion(DefaultConstructorMarker $constructor_marker) { this(); } } } //  ClassWithCompanion.Companion.companionFun(); 

The Kotlin compiler simplifies calls, but from Java, however, it doesn’t look so beautiful. Fortunately, it is possible to declare methods to be truly static. For this there is a summary @JvmStatic. It can be added to both the object methods and the companion object methods. Consider the example object:

 //Kotlin object ObjectWithStatic { @JvmStatic fun staticFun(): Int { return 5 } } 

In this case, the staticFun method will actually be declared static:

 public final class ObjectWithStatic { public static final ObjectWithStatic INSTANCE; @JvmStatic public static final int staticFun() { return 5; } private ObjectWithStatic() { INSTANCE = (ObjectWithStatic)this; } static { new ObjectWithStatic(); } } 

For the methods from the companion object, you can also add the @JvmStatic annotation:

 class ClassWithCompanionStatic { val name: String = "Kurt" companion object { @JvmStatic fun companionFun(): Int = 5 } } 

For this code, the static method companionFun will also be created. But the method itself will still call the method from the companion:

 public final class ClassWithCompanionStatic { @NotNull private final String name = "Kurt"; public static final ClassWithCompanionStatic.Companion Companion = new ClassWithCompanionStatic.Companion((DefaultConstructorMarker)null); @NotNull public final String getName() { return this.name; } @JvmStatic public static final int companionFun() { return Companion.companionFun(); } public static final class Companion { @JvmStatic public final int companionFun() { return 5; } private Companion() { } // $FF: synthetic method public Companion(DefaultConstructorMarker $constructor_marker) { this(); } } } 

As shown above, Kotlin provides various options for declaring both static methods and companion methods. Calling static methods is a little faster, so in places where performance is important, it’s still better to put @JvmStatic annotations on methods (but you still shouldn’t count on a big gain in speed)

lateinit properties


Sometimes there is a situation when you need to declare a notnull property in a class, the value for which we cannot immediately specify. But when initializing the notnull field, we are obliged to assign a default value to it, or to make the Nullable property and write null to it. In order not to go to nullable, there is a special modifier lateinit in Kotlin, which tells the Kotlin compiler that we are committed to initializing the property ourselves later.

 //Kotlin class LateinitExample { lateinit var lateinitValue: String } 

If we try to access the property without initialization, then a UninitializedPropertyAccessException will be thrown. This functionality works quite simply:

 //Java public final class LateinitExample { @NotNull public String lateinitValue; @NotNull public final String getLateinitValue() { String var10000 = this.lateinitValue; if(this.lateinitValue == null) { Intrinsics.throwUninitializedPropertyAccessException("lateinitValue"); } return var10000; } public final void setLateinitValue(@NotNull String var1) { Intrinsics.checkParameterIsNotNull(var1, "<set-?>"); this.lateinitValue = var1; } } 

An additional check of the property value is inserted into getter, and if it contains null, an exception is thrown. By the way, precisely because of this, in Kotlin it is impossible to make a lateinit property with the type Int, Long and other types that correspond to the primitive types of Java.

coroutines


In Kotlin 1.1, there is a new functionality called coroutines. With it, you can easily write asynchronous code in a synchronous form. In addition to the main library (kotlinx-coroutines-core) to support interrupts, there is also a large set of libraries with various extensions:

kotlinx-coroutines-jdk8 - additional library for JDK8
kotlinx-coroutines nio are extensions for asynchronous IO from JDK7 +.

kotlinx-coroutines-reactive - utilities for jet streams
kotlinx-coroutines-reactor - utilities for Reactor
kotlinx-coroutines-rx1 - utilities for RxJava 1.x
kotlinx-coroutines-rx2 - utilities for RxJava 2.x

kotlinx-coroutines-android - UI context for Android.
kotlinx-coroutines-javafx - JavaFx context for JavaFX UI applications.
kotlinx-coroutines-swing - Swing context for Swing UI applications.

Note: Functionality is still in the experimental stage, so everything said below may still change.

To indicate that a function can be interrupted and used in the context of an interrupt, the suspend modifier is used.

 //Kotlin suspend fun asyncFun(x: Int): Int { return x * 3 } 

The decompiled code looks like this:

 //Java public static final Object asyncFun(int x, @NotNull Continuation $continuation) { Intrinsics.checkParameterIsNotNull($continuation, "$continuation"); return Integer.valueOf(x * 3); } 

It turns out almost the original function, except that one additional parameter is passed that implements the Continuation interface.

 interface Continuation<in T> { val context: CoroutineContext fun resume(value: T) fun resumeWithException(exception: Throwable) } 

It contains the execution context, defines the function of returning the result and the function of returning the exception, in case of an error.

Korutiny compiled into a state machine (state machine). Consider an example:

 val a = a() val y = foo(a).await() //   #1 b() val z = bar(a, y).await() //   #2 c(z) 

The foo and bar functions return the CompletableFuture, on which the await function is called. Decompiling such code in Java will not work (mostly due to goto), so consider it in pseudocode:

 class <anonymous_for_state_machine> extends CoroutineImpl<...> implements Continuation<Object> { //     int label = 0 //    A a = null Y y = null void resume(Object data) { if (label == 0) goto L0 if (label == 1) goto L1 if (label == 2) goto L2 else throw IllegalStateException() L0: a = a() label = 1 data = foo(a).await(this) // 'this'   continuation if (data == COROUTINE_SUSPENDED) return // ,  await   L1: //     ,    data y = (Y) data b() label = 2 data = bar(a, y).await(this) // 'this'   continuation if (data == COROUTINE_SUSPENDED) return // ,  await   L2: //         data Z z = (Z) data c(z) label = -1 //      return } } 

As you can see, there are 3 states: L0, L1, L2. Execution starts in the L0 state, then from which it switches to the L1 state and after in L2. At the end, the state is switched to -1 as an indication that no more steps are allowed.

Korutin themselves can be performed in different threads, there is a convenient mechanism for managing this by specifying a pool in the context of launching a korutina. You can see a detailed guide with a large number of examples and a description of their use.

All source codes on Kotlin are available in github . You can open them on your own and experiment with the code, at the same time looking at which final bytecode the sources are compiled into.

findings


Application performance on Kotlin will not be much worse than in Java, and using the inline modifier may even turn out to be better. The compiler in all places tries to generate the most optimized bytecode. Therefore, do not be afraid that when you switch to Kotlin you will get a big performance degradation. And in especially critical places, knowing what Kotlin compiles, you can always rewrite the code to a more suitable option. A small fee for the fact that the language allows you to implement complex structures in a fairly concise and simple form.

Thanks for attention! I hope you enjoyed the article. I ask all those who have noticed any errors or inaccuracies to write me about it in a personal message.

Source: https://habr.com/ru/post/330064/


All Articles