Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kotlin’s hidden costs, revisited

Kotlin’s hidden costs, revisited

Kotlin has become the new default modern programming language for many Android and backend developers. But exactly how much performance and code size are we sacrificing for making use of all the additional features this language provides compared to Java?
We'll study the Java bytecode generated by the Kotlin compiler in various scenarios in order to find out which code constructs come with hidden performance penalties and learn how to become better Kotlin programmers. Some significant improvements in the recent versions of Kotlin will also be highlighted.
This talk is based on the series of blog posts from the speaker and extends it in some areas.

Christophe Beyls

April 24, 2018
Tweet

More Decks by Christophe Beyls

Other Decks in Programming

Transcript

  1. Types of costs - Boxing of primitive types - Hidden

    objects instantiation - Generating extra methods (Android dex methods count)
  2. public final class be/digitalia/sample/myapplication/HiddenCostsKt { // access flags 0x1A private

    final static I topLevelConstant = 1 // access flags 0x19 public final static getTopLevelConstant()I L0 LINENUMBER 3 L0 GETSTATIC be/digitalia/sample/myapplication/HiddenCostsKt.topLevelConstant : I IRETURN L1 MAXSTACK = 1 MAXLOCALS = 0 // access flags 0x8 static <clinit>()V L0 LINENUMBER 3 L0 ICONST_1 PUTSTATIC be/digitalia/sample/myapplication/HiddenCostsKt.topLevelConstant : I RETURN MAXSTACK = 1 MAXLOCALS = 0 // compiled from: HiddenCosts.kt }
  3. Example: Top-level field (Java equivalent) public final class HiddenCostsKt {

    private static final int topLevelConstant = 1; public static final int getTopLevelConstant() { return topLevelConstant; } }
  4. Instead: - Top level fields or methods - Objects (singletons)

    No static fields or methods in Kotlin val topLevelConstant = 1 object UniverseConstants { val meaningOfLife = 42 } class Cat { companion object { val totalLives = 7 } }
  5. Accessing private fields/methods between a class and its companion object

    - The class and its companion object are actually compiled to 2 separate classes. - Synthetic static accessor methods are added by the compiler to make private and protected fields accessible to the other part. - Kotlin doesn’t have package visibility.
  6. Companion object class MyClass { companion object { private val

    TAG = "TAG" } fun helloWorld() { println(TAG) } }
  7. public final class MyClass { private static final String TAG

    = "TAG"; public static final Companion companion = new Companion(); // synthetic method public static final String access$getTAG$cp() { return TAG; } public static final class Companion { private final String getTAG() { return MyClass.access$getTAG$cp(); } // synthetic method public static final String access$getTAG$p(Companion c) { return c.getTAG(); } } public final void helloWorld() { System.out.println(Companion.access$getTAG$p(companion)); } }
  8. public final class MyClass { private static final String TAG

    = "TAG"; public static final Companion companion = new Companion(); // synthetic method public static final String access$getTAG$cp() { return TAG; } public static final class Companion { private final String getTAG() { return MyClass.access$getTAG$cp(); } // synthetic method public static final String access$getTAG$p(Companion c) { return c.getTAG(); } } public final void helloWorld() { System.out.println(Companion.access$getTAG$p(companion)); } } 3 extra methods
  9. public final class MyClass { private static final String TAG

    = "TAG"; public static final Companion companion = new Companion(); public final void helloWorld() { System.out.println(TAG); } public static final class Companion { } } no extra method!
  10. Accessing a local variable inside a Companion object class MyClass

    { companion object { private val TAG = "TAG" fun helloWorld() { println(TAG) } } }
  11. public final class MyClass { private static final String TAG

    = "TAG"; public static final Companion companion = new Companion(); // synthetic method public static final String access$getTAG$cp() { return TAG; } public static final class Companion { public final void helloWorld() { System.out.println(MyClass.access$getTAG$cp()); } } } 1 extra method
  12. Use const for primitive and String constants class MyClass {

    companion object { private const val TAG = "TAG" fun helloWorld() { println(TAG) } } }
  13. Use const for primitive and String constants public final void

    helloWorld() { String var1 = "TAG"; System.out.println(var1); } The compiler inlines the value at the call site
  14. Alternative: Use a private top level declaration private val TAG

    = "TAG" class MyClass { fun helloWorld() { println(TAG) } }
  15. Use a private top level declaration public final class HiddenCostsKt

    { private static final String TAG = "TAG"; // synthetic method public static final String access$getTAG$p() { return TAG; } } public final class MyClass { public final void helloWorld() { String var1 = HiddenCostsKt.access$getTAG$p(); System.out.println(var1); } } 1 extra method
  16. A function taking one or more functions as arguments (or

    returning a function as its result)
  17. Higher-order function definition fun transaction(db: Database, body: (Database) -> Int):

    Int { db.beginTransaction() try { val result = body(db) db.setTransactionSuccessful() return result } finally { db.endTransaction() } }
  18. Higher-order function call: lambda class MyClass$myMethod$1 implements Function1 { //

    $FF: synthetic method // $FF: bridge method public Object invoke(Object var1) { return Integer.valueOf(this.invoke((SQLiteDatabase)var1)); } public final int invoke(@NotNull SQLiteDatabase it) { Intrinsics.checkParameterIsNotNull(it, "it"); return it.delete("Customers", null, null); } }
  19. Generated function classes - Each lambda expression adds one class

    and 3 or 4 methods to the total methods count. - Instances are only created when necessary: - Capturing lambdas: new instance on each function call - Non-capturing lambdas: a singleton instance gets reused.
  20. Non-specialized Function interfaces /** A function that takes 1 argument.

    */ public interface Function1<in P1, out R> : Function<R> { /** Invokes the function with the specified argument. */ public operator fun invoke(p1: P1): R }
  21. Boxing overhead class MyClass$myMethod$1 implements Function1 { // $FF: synthetic

    method // $FF: bridge method public Object invoke(Object var1) { return Integer.valueOf(this.invoke((SQLiteDatabase)var1)); } public final int invoke(@NotNull SQLiteDatabase it) { Intrinsics.checkParameterIsNotNull(it, "it"); return it.delete("Customers", null, null); } }
  22. inline to the rescue inline fun transaction(db: Database, body: (Database)

    -> Int): Int { db.beginTransaction() try { val result = body(db) db.setTransactionSuccessful() return result } finally { db.endTransaction() } }
  23. inline to the rescue db.beginTransaction(); try { int result$iv =

    db.delete("Customers", null, null); db.setTransactionSuccessful(); } finally { db.endTransaction(); }
  24. inline to the rescue - No instantiation of Function objects

    - No boxing on input/ouput primitive values - No actual function call.
  25. inline functions limitations - Can not be called recursively. -

    A public inline function only has access to the public members of a class. - Will grow code size.
  26. Summary: higher-order functions - Declare them as inline when possible

    and keep them short. Inline functions may be decomposed to call multiple regular functions. - In other cases, prefer non-capturing lambda expressions.
  27. Local function fun someMath(a: Int): Int { fun sumSquare(b: Int)

    = (a + b) * (a + b) return sumSquare(1) + sumSquare(2) }
  28. Local function fun someMath(a: Int): Int { fun sumSquare(b: Int)

    = (a + b) * (a + b) return sumSquare(1) + sumSquare(2) } no inline !
  29. Local function public static final int someMath(final int a) {

    LocalFunction1 sumSquare$ = new Function1() { // $FF: synthetic method // $FF: bridge method public Object invoke(Object var1) { return Integer.valueOf(this.invoke(((Number)var1).intValue())); } public final int invoke(int b) { return (a + b) * (a + b); } }; return sumSquare$.invoke(1) + sumSquare$.invoke(2); }
  30. Local function public static final int someMath(final int a) {

    LocalFunction1 sumSquare$ = new Function1() { // $FF: synthetic method // $FF: bridge method public Object invoke(Object var1) { return Integer.valueOf(this.invoke(((Number)var1).intValue())); } public final int invoke(int b) { return (a + b) * (a + b); } }; return sumSquare$.invoke(1) + sumSquare$.invoke(2); } ALOAD 1 ICONST_1 INVOKEVIRTUAL be/myapplication/MyClassKt$someMath$1.invoke (I)I ALOAD 1 ICONST_2 INVOKEVIRTUAL be/myapplication/MyClassKt$someMath$1.invoke (I)I IADD IRETURN
  31. Local function public static final int someMath(final int a) {

    LocalFunction1 sumSquare$ = new Function1() { // $FF: synthetic method // $FF: bridge method public Object invoke(Object var1) { return Integer.valueOf(this.invoke(((Number)var1).intValue())); } public final int invoke(int b) { return (a + b) * (a + b); } }; return sumSquare$.invoke(1) + sumSquare$.invoke(2); } no boxing
  32. Summary: local functions fun someMath(a: Int): Int { fun sumSquare(a:

    Int, b: Int) = (a + b) * (a + b) return sumSquare(a, 1) + sumSquare(a, 2) } Non-capturing local functions avoid repeated object allocations … but then you lose the main benefit of a local function.
  33. 2. Single array int[] values = new int[]{1, 2, 3};

    printDouble(Arrays.copyOf(values, values.length)); Array copy
  34. 3. Mix of arrays and arguments int[] values = new

    int[]{1, 2, 3}; IntSpreadBuilder var10000 = new IntSpreadBuilder(3); var10000.add(0); var10000.addSpread(values); var10000.add(42); printDouble(var10000.toArray()); IntSpreadBuilder allocation + array copy
  35. Consider the array argument fun printDouble(values: IntArray) { values.forEach {

    println(it * 2) } } In critical portions of the code where you want to avoid array copies.
  36. Non-null arguments runtime checks public static final void sayHello(@NotNull String

    who) { Intrinsics.checkParameterIsNotNull(who, "who"); String var1 = "Hello " + who; System.out.println(var1); }
  37. Non-null arguments runtime checks - One static call for each

    non-null reference argument - No checks in private functions - Performance impact should be negligible But it can optionnally be disabled in release builds.
  38. Disabling runtime checks in release builds: Method 1 build.gradle (Android

    project) buildTypes { release { kotlinOptions { freeCompilerArgs = [ 'Xno-param-assertions' ] } } }
  39. Disabling runtime checks in release builds: Method 2 proguard-rules.pro -dontoptimize

    -assumenosideeffects class kotlin.jvm.internal.Intrinsics { static void checkParameterIsNotNull(java.lang.Object, java.lang.String); }
  40. Nullable basic types Byte, Short, Int, Long, Float, Double, Char,

    Boolean - Nullable types are always reference types. - Prefer non-null primitive types to avoid boxing. Kotlin type Int Int? JVM type int, Integer Integer
  41. Kotlin array types Prefer the specialized ones Kotlin type Array<Int>

    IntArray JVM type Integer[] int[] val x = intArrayOf(1, 2, 3) val x = arrayOf(1, 2, 3)
  42. public final class Example { // $FF: synthetic field static

    final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.mutableProperty1(new MutablePropertyReference1Impl(Reflection.getOrCreateKotlinClass(Example.class), "p", "getP()Ljava/lang/String;"))}; @NotNull private final Delegate p$delegate = new Delegate(); @NotNull public final String getP() { return this.p$delegate.getValue(this, $$delegatedProperties[0]); } public final void setP(@NotNull String var1) { Intrinsics.checkParameterIsNotNull(var1, "<set-?>"); this.p$delegate.setValue(this, $$delegatedProperties[0], (String)var1); } } Metadata Extra getter/setter methods
  43. Delegated properties class Example { private val nameView by BindViewDelegate<TextView>(R.id.name)

    private val textView by BindViewDelegate<EditText>(R.id.text) private val imageView by BindViewDelegate<ImageView>(R.id.image) private val streetView by BindViewDelegate<TextView>(R.id.street) private val scrollView by BindViewDelegate<ScrollView>(R.id.scroll) private val buttonView by BindViewDelegate<Button>(R.id.button) } 6 delegate instances + 6 private getter methods
  44. Delegated properties A new delegate instance is required for a

    property when: - The delegate is stateful (example: caching), or - The delegate requires extra arguments: class Example { private val nameView by BindViewDelegate<TextView>(R.id.name) }
  45. Reducing the number of delegate instances - A stateless delegate

    can be implemented as an object - Any existing object can implement the delegate contract, using instance methods or extension functions. Examples: Map and MutableMap public operator fun getValue(thisRef: R, property: KProperty<*>): T public operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
  46. Generic delegates and boxing Generic delegate classes capable of handling

    multiple types of properties will introduce boxing for non-null primitive types each time a property is read or written to. private var maxDelay: Long by SharedPreferencesDelegate<Long>() Prefer specialized delegate classes for non-null primitive types.
  47. Standard delegates: lazy() lazy() is a function returning one of

    3 implementations: - LazyThreadSafetyMode.SYNCHRONIZED (default) - LazyThreadSafetyMode.PUBLICATION - LazyThreadSafetyMode.NONE When thread safety is not needed, use LazyThreadSafetyMode.NONE: val dateFormat: DateFormat by lazy(LazyThreadSafetyMode.NONE) { SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()) }
  48. Ranges: inclusion tests - Ranges represent finite sets of values

    - The main operator function to create a range is .. Primitive types: if (i in 1..10) { println(i) }
  49. Ranges: inclusion tests if(1 <= i) { if(10 >= i)

    { System.out.println(i); } } No cost
  50. Ranges: inclusion tests Indirection - a function returning a range:

    fun myRange() = 1..10 fun rangeTest(i: Int) { if (i in myRange()) { println(i) } }
  51. Ranges: inclusion tests @NotNull public final IntRange myRange() { return

    new IntRange(1, 10); } public final void rangeTest(int i) { if(this.myRange().contains(i)) { System.out.println(i); } } Allocation of a specialized Range object No boxing
  52. Ranges: inclusion tests - Declare ranges directly in the tests

    where they are used (no indirection) or as constants. - inline functions don’t prevent allocations.
  53. Ranges: iterations Available for integral type ranges (any primitive type

    except for Float, Double and Boolean) for (i in 1..10) { println(i) }
  54. Ranges: iterations int i = 1; for(byte var2 = 11;

    i < var2; ++i) { System.out.println(i); } No cost
  55. Ranges: iterations int i = 10; byte var2 = 1;

    while(true) { System.out.println(i); if(i == var2) { return; } --i; } No cost
  56. Ranges: iterations int i = 0; for(int var3 = size;

    i < var3; ++i) { System.out.println(i); } No cost (since 1.1.4)
  57. Ranges: iterations Combining 2 or more functions to create a

    range: for (i in 1..10 step 2) { println(i) } for (i in (1..10).reversed()) { println(i) }
  58. IntProgression var10000 = RangesKt.reversed((IntProgression)(new IntRange(1, 10))); int i = var10000.getFirst();

    int var3 = var10000.getLast(); int var4 = var10000.getStep(); if(var4 > 0) { if(i > var3) { return; } } else if(i < var3) { return; } while(true) { System.out.println(i); if(i == var3) { return; } i += var4; } Allocation of 2 or more Progression objects
  59. Ranges: iterations Kotlin has built-in indices extensions properties to easily

    iterate over arrays and classes implementing Collection. val list = listOf("A", "B", "C") for (i in list.indices) { println(list[i]) }
  60. Ranges: iterations List list = CollectionsKt.listOf(new String[]{"A", "B", "C"}); int

    i = 0; for(int var4 = ((Collection)list).size(); i < var4; ++i) { System.out.println(list.get(i)); } No cost
  61. Warning: using forEach() on a range Iterable $receiver$iv = (Iterable)(new

    IntRange(1, 10)); Iterator var3 = $receiver$iv.iterator(); while(var3.hasNext()) { int element$iv = ((IntIterator)var3).nextInt(); System.out.println(element$iv); } IntRange allocation + IntIterator allocation
  62. Summary: ranges iteration - Prefer the for loop. - Prefer

    using a single function call to .., downTo or until to create the range. - Use the built-in indices property on arrays and Collection classes but don’t create your own.
  63. when on integer value fun whenTest(value: Int): String { return

    when (value) { 1 -> "A" 2, 3 -> "B" else -> "" } }
  64. @NotNull public static final String whenTest(int value) { String var10000;

    switch(value) { case 1: var10000 = "A"; break; case 2: case 3: var10000 = "B"; break; default: var10000 = ""; } return var10000; } ILOAD 0 TABLESWITCH 1: L1 2: L2 3: L2 default: L3
  65. when on integer value with range fun whenTest(value: Int): String

    { return when (value) { 1 -> "A" in 2..3 -> "B" else -> "" } }
  66. @NotNull public static final String whenTest(int value) { String var10000;

    if(value == 1) { var10000 = "A"; } else { if(2 <= value) { if(3 >= value) { var10000 = "B"; return var10000; } } var10000 = ""; } return var10000; } No tableswitch optimization
  67. JVM microbenchmarks: flawed methodology fun runKotlinLambda(db: Database): Int { val

    deletedRows = transaction(db) { it.delete("Customers", null, null) } return deletedRows } fun transaction(db: Database, body: (Database) -> Int): Int { db.beginTransaction() try { val result = body(db) db.setTransactionSuccessful() return result } finally { db.endTransaction() } }
  68. JVM microbenchmarks: flawed methodology fun runKotlinLambda(db: Database): Int { val

    deletedRows = transaction(db) { it.delete("Customers", null, null) } return deletedRows } fun transaction(db: Database, body: (Database) -> Int): Int { db.beginTransaction() try { val result = body(db) db.setTransactionSuccessful() return result } finally { db.endTransaction() } } Non-capturing lambda: no Function object allocation Boxing of return value ?
  69. JVM microbenchmarks: flawed methodology public class Database { public void

    endTransaction() { } public void setTransactionSuccessful() { } public void beginTransaction() { } public int delete(String s, @Nullable Object var1, @Nullable Object var2 ) { return 0; } } Constant value in the Integer cache: no allocation
  70. Most microbenchmarks are flawed or meaningless - Memory consumption should

    be measured as well, because temporary objects have to be garbage collected eventually. - Negative impacts are usually amplified with nested loops. - Performance varies per platform (JVM, Android). No simple answers: profile your own code on your own target platform.