I enjoyed two Kotlin presentations. Both combined describe the best of this new language.
- Advancing Android development with Kotlin
https://realm.io/news/oredev-jake-wharton-kotlin-advancing-android-dev
- Kotlin bytecode generation and runtime performance
http://www.slideshare.net/intelliyole/kotlin-bytecode-generation-and-runtime-performance
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()) } |
|
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.
ASTORE 0 / ALOAD 0 in the example you provide are not "useless".
ReplyDeleteOtherwise you'll not see corresponding arguments of the inline function in the debugger.
Mature JVM implementations can optimize such simple things.
Hi DireBunny,
ReplyDeleteyou 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 ;)
Hello. Thank you for an interesting research.
ReplyDeleteIn 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)`.
Excellent Blog, I like your blog and It is very informative. Thank you
ReplyDeletekotlin online course
Learn kotlin online