martes, 2 de agosto de 2016

Kotlin incluye codigo muerto en el bytecode



http://lulachronicles.blogspot.nl/2016/07/kotlin-includes-dead-code-in-generated.html

Hay dos presentaciones sobre Kotlin que disfrute mucho, ambas describen lo mejor de este nuevo lenguaje.






Pero hay una cosa no descripta en esas presentaciones, un detalle sobre cómo Kotlin genera el bytecode:

Las 'inlined functions' de Kotlin no remueven el codigo muerto y lo incluyen en el bytecode.



No es un gran problema ya que Proguard o una herramienta similar remueve el codigo muerto al optimizar las clases, pero en los proyectos Android a veces es necesario deshabilitar las optimizaciones de Proguard debido a la complejidad del proyecto, las librerias usadas, etc.

Un ejemplo mostrando el código y el bytecode de Java y Kotlin


Veamos un ejemplo. Queremos mostrar mensajes de debug, pero no queremos incluir en el bytecode de la release build este código de debug.:
  • En java podemos meter los logs dentro de una condición, chequeando una constante booleana. En este caso el compilador ignora todo lo que está dentro de la condición cuando el valor de la constante es falso.
  • En Kotlin podemos usar inlined functions, una gran ventaja de este lenguaje, para no chequear una cosntante cada vez que queremos mostrar un log. Un ejemplo de una inlined function en Kotlin

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


Por ejemplo, mostramos un mensaje de debug dentro de un método llamado "doSomething"

Java
void doSomething() {
    if (BuildConfig.DEBUG) {
        System.out.println("Este es un mensaje de debug");
    }
}

Kotlin
fun doSomething() {
    debug { "Este es un mensaje de debug" }
}

+1 para Kotlin, menos código, limpio y simple.

En Java podríamos crear un método static y poner la condición dentro del método, así tendríamos un código limpio como el de Kotlin, pero el compilador de Java no removería todos los llamados a la función y todos los strings de debug se incluirían en la build de release. Incluso optimizando con Proguard los llamados al método se remueven, pero no los parámetros, en realidad depende de la cantidad de pasos en la optimización dentro de proguard.properties.


Veamos ahora el bytecode generado por Java y Kotlin.


Para analizar el bytecode uso el plugin de IntelliJ/Eclipse creado por los autores de la librería ASM, el OW2 Consortium. Para Kotlin podría usar el plugin que viene con IntelliJ pero no tiene una opción para ignorar los números de linea, los labels y la info de stack.


Bytecode generado cuando la constante DEBUG es true

Java
  void doSomething() {
    getstatic 'BuildConfig.DEBUG','Z'
    ifeq l0
    getstatic 'java/lang/System.out','Ljava/io/PrintStream;'
    ldc "Este es un mensaje de debug"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   l0
    return
  }


Kotlin (el bytecode no es óptimo como el de java, ver el subrayado
  public final static void doSomething() {
    nop
    getstatic 'BuildConfig.DEBUG','Z'
    ifeq l0
    ldc "Este es un mensaje de debug"
    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 para Java, menos código que ejecutar.

La diferencia no es grande pero Kotlin incluye algunos nops inútiles el la forma en que carga el string "Este es un mensaje de debug" es redundante, carga el string, lo guarda en el stack (astore 0), y lo carga de nuevo (aload 0).



Bytecode generado cuando la constante DEBUG es false

Java
  void doSomething() {
    return
  }


Kotlin (dead code is underlined)
  public final static void doSomething() {
    nop
    iconst_0
    ifeq l0
    ldc "Este es un mensaje de debug"
    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 para Java, el compilador ignora el código dentro de la condición, pero el inline de Kotlin no hace ningún chequeo, simplemente copia y pega el código dentro del método, sin tener en cuenta el valor de la constante.


Cómo evitarlo usando gradle flavors


Lo que reomiendo para Kotlin es, en vez de chequear el valor de una constante, usar gradle flavors, el método para la build de debug muestra el mensaje sin chequear nada, el método de release no hace nada.

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


Así el bytecode generado en la build de release es casi nulo .


Kotlin Bytecode: Debug flavor
  public final static void doSomething() {
    nop
    ldc "Este es un mensaje de debug"
    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 agrega un nop dentro del bytecode de release, pero es mejor que el código muerto incluido antes.


Estamos resolviendo un gran problema con esto? Para la mayoria de los proyectos no, pero estos pequeños detalles pueden ser relevantes en proyectos que necesitan alta performance.








No hay comentarios:

Publicar un comentario en la entrada