Saturday, July 2, 2016

Be careful, Kotlin includes dead code in generated bytecode.



I enjoyed two Kotlin presentations. Both combined describe the best of this new language.






There is one thing not covered by those presentations, it is a tiny detail in how Kotlin generates the bytecode:

Kotlin inlined functions include dead code in the generated bytecode.



Not a big issue when using Proguard or similar tool to optimize/shrink the classes, but in in case of Android development sometimes is needed to avoid proguard optimization/shrink due to the complexity of the project, libraries, etc.

An example showing code and bytecode in Java and Kotlin


Let's see an example.  We want to log debug messages, but we dont want to include code for debugging purposes in the bytecode of the release build:
  • In java we call a log method inside a condition checking for a boolean constant, so the compiler ignores the code in the release build when the constant is false.
  • In Kotlin we use the advantage of inlined functions, we dont need to always include the condition when calling the log method. The function to log the message is the following

    inline fun debug(func: () -> String) {
        if (BuildConfig.DEBUG) {
            println(func())
        }
    }


We log the message inside a method named "doSomething"

Java
void doSomething() {
    if (BuildConfig.DEBUG) {
        System.out.println("This is a debug message");
    }
}

Kotlin
fun doSomething() {
    debug { "This is a debug message" }
}

+1 for Kotlin, cleaner code.

In java to have a cleaner code we could create a static utility method named "debug" and put the condition inside, but the compiler will include all the calls to "debug" in the bytecode of the release build. Even after optimizing with proguard the method calls will be removed, but not the parameters, depending on how many optimization passes we define in proguard.properties


Let's check now the bytecode generated by Java and Kotlin compilers.


To analyze the bytecode I use the plugin for IntelliJ/Eclipse made by the creators of the ASM library, the OW2 Consortium. I could use the Kotlin plugin included in IntelliJ in menu Tools -> Kotlin -> Show Kotlin Bytecode but it doesnt add an option to ignore the line numbers, unused labels and stack information.


Generated bytecode when DEBUG constant is true

Java
  void doSomething() {
    getstatic 'BuildConfig.DEBUG','Z'
    ifeq l0
    getstatic 'java/lang/System.out','Ljava/io/PrintStream;'
    ldc "This is a debug message"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   l0
    return
  }


Kotlin (useless bytecode is underlined)
  public final static void doSomething() {
    nop
    getstatic 'BuildConfig.DEBUG','Z'
    ifeq l0
    ldc "This is a debug message"
    astore 0
    nop
    getstatic 'java/lang/System.out','Ljava/io/PrintStream;'
    aload 0
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
   l0
    return
  }

+1 for Java.

Why? The difference is small but Kotlin includes some useless nops and the loading of the string "This is a debug message" is a bit dumb, loads the string, stores it, and loads it again.



Generated bytecode when DEBUG constant is false

Java
  void doSomething() {
    return
  }


Kotlin (dead code is underlined)
  public final static void doSomething() {
    nop
    iconst_0
    ifeq l0
    ldc "This is a debug message"
    astore 0
    nop
    getstatic 'java/lang/System.out','Ljava/io/PrintStream;'
    aload 0
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
   l0
    return
  }

+1 for Java, the java compiler ignores the code inside the condition, but Kotlin does a copy/paste of the code when inlining the function, not taking into account the value of the constant is always false.


A better approach using gradle flavors


What I recommend to do for Kotlin is, instead of checking for a constant, to use gradle flavors, the debug method in the flavor for the debug build shows the message, the debug method in the flavor for the release build does nothing.

Debug flavor Release flavor
inline fun debug(func: () -> String) {
    println(func())
}
inline fun debug(func: () -> String) {
}


Using it in this way we produce clean code and clean bytecode.


Kotlin Bytecode: Debug flavor
  public final static void doSomething() {
    nop
    ldc "This is a debug message"
    astore 0
    nop
    getstatic 'java/lang/System.out','Ljava/io/PrintStream;'
    aload 0
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
    return
  }

Kotlin Bytecode: Release flavor
  public final static void doSomething() {    
    nop
    return
  }



Kotlin adds a useless nop in the bytecode generated by the release flavor but is clean enough for our needs.


Are we solving a big issue here? Not really, but these small details help us to create clean code without adding an overhead in the compiled classes.









3 comments:

  1. ASTORE 0 / ALOAD 0 in the example you provide are not "useless".
    Otherwise you'll not see corresponding arguments of the inline function in the debugger.
    Mature JVM implementations can optimize such simple things.

    ReplyDelete
  2. Hi DireBunny,

    you are right they are not useless due to the way the bytecode is generated. I just wanted to mark is not needed to do it in that way.

    Thanks for your comment ;)

    ReplyDelete
  3. Hello. Thank you for an interesting research.
    In fact, kotlinc removes code inside `if (false)` (inline functions' bodies getting removed too).
    But kotlinc does not remove code inside of `if (CONST_WITH_FALSE_VALUE)`.

    ReplyDelete