Kennis Blogs Devoxx: Kotlin under the hood

Devoxx: Kotlin under the hood

At Devoxx BE 2019, I attended a talk that was very interesting for Kotlin enthusiasts (like myself). The talk was given by Chet Haase and Romain Guy. Chet is the chief Android advocate at Google and combines tech talks with comedy. Romain is the lead developer on the Android Toolkit at Google. This blog will give some examples of Kotlin functions and how they are compiled. This blog will also explain how to show compiled bytecode and Java code, so you can make your own discoveries. There will be a link to the talk posted below for more information.

 

The talk focused on showing how different Kotlin functions are compiled to bytecode and then decompiled to readable Java. This was interesting for several reasons. One reason was seeing how Kotlin functions are implemented by the Jetbrains team. Jetbrains is the company behind Kotlin and they also develop IntelliJ. Some functions are implemented really well and effective. Other functions are unexpectedly compiled to quite complex and heavy implementations. This brings us to the other reason why I found this talk very interesting: this knowledge can be used to further improve the performance of Kotlin applications. When an application performs heavy calculations and has strict memory constraints, the developer should avoid certain constructions in order to make the compiled code perform as efficiently as possible.
 

Show me the code

Showing the bytecode is very easy in IntelliJ. Just open a Kotlin file, go to tools → Kotlin → Show bytecode. Unfortunately, bytecode is not easy to understand and very few people can read it. In order to read the code, we decompile the bytecode to Java by pressing the decompile button.

 

Nullability

Many would agree that one of Kotlin's strong suits is its nullability. But it compiles to Java bytecode and Java is not null safe (which is often called 'the billion dollar mistake'). It's interesting to know how a nullable Kotlin property compiles to bytecode. You can already see the difference by looking at a very simple Kotlin class:


  class Nullable{
    val int1: Int = 123
    var int2: Int? = 123
}

It compiles and then decompiles into the following Java code:
This code also generated getters and setters for the properties which I have left out of this example.

 


public final class Nullable {
    private final int int1 = 123;
   @org.jetbrains.annotations.Nullable
   private Integer int2 = 123;
}

As you can see, the Kotlin Int data type is compiled to the primitive Int data type. However, the nullable Int is compiled to the non-primitive data type Integer. This is because Java primitives can't be null.

 

Enums

Enums are available in Java and in Kotlin and they work the same. Kotlin has when statements that cover all the possible enum values.


class Colours {
    fun isPrimitive(colour: Colour): Boolean = when(colour){
        Colour.BLUE -> true
        Colour.RED -> true
        Colour.YELLOW -> true
        Colour.GREEN -> false
    }
}
 
enum class Colour {
    BLUE,
    RED,
    YELLOW,
    GREEN
}

This code compiles and decompiles into Java:

 


public static final int[] $EnumSwitchMapping$0 = new int[Colour.values().length];static {
     $EnumSwitchMapping$0[Colour.BLUE.ordinal()] = 1;
     $EnumSwitchMapping$0[Colour.RED.ordinal()] = 2;
     $EnumSwitchMapping$0[Colour.YELLOW.ordinal()] = 3;
     $EnumSwitchMapping$0[Colour.GREEN.ordinal()] = 4;
}

 

As you can see, an array is created just for this when statement. When multiple when statements are used, the compiler doesn't reuse the created array but just creates more. This code seems ineffective: memory is allocated when this seemingly could be avoided. This means developers should think twice about when statements on large enums when memory is in short supply.

 

Lazy

The last example I will give here is on the lazy keyword. Lazy is used on expensive variables (high CPU and/or memory usage) that aren't always used. By using lazy, the variable isn't loaded until it is used in runtime. Below is an example. In a real-world example, 42 could be replaced with some heavy calculations.


val autoLazy by lazy { 42 }

If we would implement lazy manually, it would look something like this in both Kotlin and Java:

 


private var manualLazy: Int? = null
val myLazy: Int
get() {
   if(manualLazy == null){
       manualLazy = 42
   }
   return manualLazy!!
}

 

We would expect the compiled code would look like this. But it doesn't! Instead of code that looks like the manual implementation of lazy, the compiler uses Kproperty. This is a class that extends Java objects with functions Kotlin needs.

 


static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.property1(new PropertyReference1Impl(Reflection.getOrCreateKotlinClass(AutoLazy.class), "autoLazy", "getAutoLazy()I"))};
@NotNull
private final Lazy autoLazy$delegate;
 
public final int getAutoLazy() {
  Lazy var1 = this.autoLazy$delegate;
  KProperty var3 = $$delegatedProperties[0];
  boolean var4 = false;
  return ((Number)var1.getValue()).intValue();
}

 

The decompiled version of lazy shows the initialization of various variables and an array. When called, several methods are needed to initialize the value and/or use the value. This means the implementation of lazy, which aims to lower execution costs of a certain function, itself is quite heavy. Developers should be aware of this so that decisions to use the lazy keyword are made knowing the extra 'hidden' costs when compiled!

 

Conclusion

In conclusion, most developers won't benefit daily from knowing what Kotlin does under the hood. But knowing more about how the code compiles definitely makes you more aware of possible improvements when highly efficient execution is required! All the credits for this blog go to Chet and Romain. I hope your interest is piqued and you will take a look at their talk! It can be found here: