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.

by Christophe Beyls
presented on June 14, 2018 @moovel

More Decks by Kotlin User Group Hamburg

Other Decks in Programming

Transcript

  1. Kotlin’s hidden costs, revisited Christophe Beyls @BladeCoder Android Makers Paris

    2018
  2. Google I/0 2017: Kotlin officially supported on Android

  3. June 2017: I wrote a series of blog posts https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-1-fbb9935d9b62

  4. Popularity boost

  5. Agenda - Introduction to hidden costs - Exploring various Kotlin

    language features - Benchmarks ?
  6. Hidden costs ? Performance penalties of some Kotlin constructs, not

    immediately visible in the Kotlin code.
  7. By looking at the Java bytecode

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

    objects instantiation - Generating extra methods (Android dex methods count)
  9. Don’t be paranoid, Kotlin (mostly) does it right by default.

  10. Example: Top-level field val topLevelConstant = 1

  11. Example: Top-level field val topLevelConstant = 1

  12. 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 }
  13. Example: Top-level field (Java equivalent) public final class HiddenCostsKt {

    private static final int topLevelConstant = 1; public static final int getTopLevelConstant() { return topLevelConstant; } }
  14. 1. Companion Objects

  15. 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 } }
  16. 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.
  17. Companion object class MyClass { companion object { private val

    TAG = "TAG" } fun helloWorld() { println(TAG) } }
  18. 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)); } }
  19. 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
  20. Good news, everyone! Kotlin 1.2.40 fixed it

  21. 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!
  22. Accessing a local variable inside a Companion object class MyClass

    { companion object { private val TAG = "TAG" fun helloWorld() { println(TAG) } } }
  23. 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
  24. Use const for primitive and String constants class MyClass {

    companion object { private const val TAG = "TAG" fun helloWorld() { println(TAG) } } }
  25. 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
  26. Alternative: Use a private top level declaration private val TAG

    = "TAG" class MyClass { fun helloWorld() { println(TAG) } }
  27. 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
  28. 2. Higher-order functions + lambda expressions

  29. A function taking one or more functions as arguments (or

    returning a function as its result)
  30. 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() } }
  31. Higher-order function call val deletedRows = transaction(db) { it.delete("Customers", null,

    null) }
  32. Higher-order function call: lambda val deletedRows = transaction(db) { it.delete("Customers",

    null, null) }
  33. 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); } }
  34. 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.
  35. Higher-order function call int deletedRows = transaction(db, (Function1)MyClass$myMethod$1.INSTANCE); singleton

  36. Higher-order function call int deletedRows = transaction(db, (Function1)MyClass$myMethod$1.INSTANCE);

  37. 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 }
  38. 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); } }
  39. 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() } }
  40. inline to the rescue db.beginTransaction(); try { int result$iv =

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

    - No boxing on input/ouput primitive values - No actual function call.
  42. 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.
  43. 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.
  44. 3. Local functions

  45. Local function fun someMath(a: Int): Int { fun sumSquare(b: Int)

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

    = (a + b) * (a + b) return sumSquare(1) + sumSquare(2) } no inline !
  47. 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); }
  48. 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
  49. 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
  50. 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.
  51. 4. vararg

  52. vararg fun printDouble(vararg values: Int) { values.forEach { println(it *

    2) } }
  53. 1. Multiple arguments printDouble(1, 2, 3)

  54. 1. Multiple arguments printDouble(new int[]{1, 2, 3}); new array allocation

  55. 2. Single array (spread operator) val values = intArrayOf(1, 2,

    3) printDouble(*values)
  56. 2. Single array int[] values = new int[]{1, 2, 3};

    printDouble(Arrays.copyOf(values, values.length)); Array copy
  57. 3. Mix of arrays and arguments val values = intArrayOf(1,

    2, 3) printDouble(0, *values, 42)
  58. 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
  59. 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.
  60. 5. Null safety

  61. Non-null arguments runtime checks fun sayHello(who: String) { println("Hello $who")

    }
  62. 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); }
  63. 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.
  64. Disabling runtime checks in release builds: Method 1 build.gradle (Android

    project) buildTypes { release { kotlinOptions { freeCompilerArgs = [ 'Xno-param-assertions' ] } } }
  65. 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); }
  66. Choose the right type to avoid boxing Integer Long Float

    Double
  67. 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
  68. 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)
  69. 6. Delegated properties

  70. Delegated property example class Example { var p: String by

    Delegate() }
  71. 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
  72. 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
  73. 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) }
  74. 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)
  75. 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.
  76. 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()) }
  77. 7. Ranges

  78. 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) }
  79. Ranges: inclusion tests if(1 <= i) { if(10 >= i)

    { System.out.println(i); } } No cost
  80. Ranges: inclusion tests Comparable types: if (name in "Alfred".."Alicia") {

    println(name) }
  81. Ranges: inclusion tests if(name.compareTo("Alfred") >= 0) { if(name.compareTo("Alicia") <= 0)

    { System.out.println(name); } } No cost (since 1.1.50)
  82. Ranges: inclusion tests Indirection - a function returning a range:

    fun myRange() = 1..10 fun rangeTest(i: Int) { if (i in myRange()) { println(i) } }
  83. 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
  84. Ranges: inclusion tests - Declare ranges directly in the tests

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

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

    i < var2; ++i) { System.out.println(i); } No cost
  87. Ranges: iterations Backwards: for (i in 10 downTo 1) {

    println(i) }
  88. Ranges: iterations int i = 10; byte var2 = 1;

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

    println(i) }
  90. Ranges: iterations int i = 0; for(int var3 = size;

    i < var3; ++i) { System.out.println(i); } No cost (since 1.1.4)
  91. 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) }
  92. 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
  93. 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]) }
  94. 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
  95. Warning: using forEach() on a range (1..10).forEach { println(it) }

  96. 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
  97. 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.
  98. 8. Bonus: when

  99. when on integer value fun whenTest(value: Int): String { return

    when (value) { 1 -> "A" 2, 3 -> "B" else -> "" } }
  100. @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
  101. when on integer value with range fun whenTest(value: Int): String

    { return when (value) { 1 -> "A" in 2..3 -> "B" else -> "" } }
  102. @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
  103. Benchmarks ?

  104. JVM microbenchmarks

  105. JVM microbenchmarks: passing the wrong message

  106. 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() } }
  107. 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 ?
  108. 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
  109. Android microbenchmarks: completely different conclusions

  110. Microbenchmarks: same tests, different results

  111. None
  112. 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.
  113. Thank you ! @BladeCoder https://medium.com/@BladeCoder/ Benchmarks: https://sites.google.com/a/athaydes.com/renato- athaydes/posts/kotlinshiddencosts-benchmarks https://willowtreeapps.com/ideas/kotlins-hidden- costs-android-benchmarks

    Questions ?