Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Google I/0 2017: Kotlin officially supported on Android

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

Popularity boost

Slide 5

Slide 5 text

Agenda - Introduction to hidden costs - Exploring various Kotlin language features - Benchmarks ?

Slide 6

Slide 6 text

Hidden costs ? Performance penalties of some Kotlin constructs, not immediately visible in the Kotlin code.

Slide 7

Slide 7 text

By looking at the Java bytecode

Slide 8

Slide 8 text

Types of costs - Boxing of primitive types - Hidden objects instantiation - Generating extra methods (Android dex methods count)

Slide 9

Slide 9 text

Don’t be paranoid, Kotlin (mostly) does it right by default.

Slide 10

Slide 10 text

Example: Top-level field val topLevelConstant = 1

Slide 11

Slide 11 text

Example: Top-level field val topLevelConstant = 1

Slide 12

Slide 12 text

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 ()V L0 LINENUMBER 3 L0 ICONST_1 PUTSTATIC be/digitalia/sample/myapplication/HiddenCostsKt.topLevelConstant : I RETURN MAXSTACK = 1 MAXLOCALS = 0 // compiled from: HiddenCosts.kt }

Slide 13

Slide 13 text

Example: Top-level field (Java equivalent) public final class HiddenCostsKt { private static final int topLevelConstant = 1; public static final int getTopLevelConstant() { return topLevelConstant; } }

Slide 14

Slide 14 text

1. Companion Objects

Slide 15

Slide 15 text

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 } }

Slide 16

Slide 16 text

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.

Slide 17

Slide 17 text

Companion object class MyClass { companion object { private val TAG = "TAG" } fun helloWorld() { println(TAG) } }

Slide 18

Slide 18 text

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)); } }

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

Good news, everyone! Kotlin 1.2.40 fixed it

Slide 21

Slide 21 text

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!

Slide 22

Slide 22 text

Accessing a local variable inside a Companion object class MyClass { companion object { private val TAG = "TAG" fun helloWorld() { println(TAG) } } }

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Use const for primitive and String constants class MyClass { companion object { private const val TAG = "TAG" fun helloWorld() { println(TAG) } } }

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Alternative: Use a private top level declaration private val TAG = "TAG" class MyClass { fun helloWorld() { println(TAG) } }

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

2. Higher-order functions + lambda expressions

Slide 29

Slide 29 text

A function taking one or more functions as arguments (or returning a function as its result)

Slide 30

Slide 30 text

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() } }

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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); } }

Slide 34

Slide 34 text

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.

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

Non-specialized Function interfaces /** A function that takes 1 argument. */ public interface Function1 : Function { /** Invokes the function with the specified argument. */ public operator fun invoke(p1: P1): R }

Slide 38

Slide 38 text

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); } }

Slide 39

Slide 39 text

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() } }

Slide 40

Slide 40 text

inline to the rescue db.beginTransaction(); try { int result$iv = db.delete("Customers", null, null); db.setTransactionSuccessful(); } finally { db.endTransaction(); }

Slide 41

Slide 41 text

inline to the rescue - No instantiation of Function objects - No boxing on input/ouput primitive values - No actual function call.

Slide 42

Slide 42 text

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.

Slide 43

Slide 43 text

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.

Slide 44

Slide 44 text

3. Local functions

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

Local function fun someMath(a: Int): Int { fun sumSquare(b: Int) = (a + b) * (a + b) return sumSquare(1) + sumSquare(2) } no inline !

Slide 47

Slide 47 text

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); }

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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.

Slide 51

Slide 51 text

4. vararg

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

2. Single array (spread operator) val values = intArrayOf(1, 2, 3) printDouble(*values)

Slide 56

Slide 56 text

2. Single array int[] values = new int[]{1, 2, 3}; printDouble(Arrays.copyOf(values, values.length)); Array copy

Slide 57

Slide 57 text

3. Mix of arrays and arguments val values = intArrayOf(1, 2, 3) printDouble(0, *values, 42)

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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.

Slide 60

Slide 60 text

5. Null safety

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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); }

Slide 63

Slide 63 text

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.

Slide 64

Slide 64 text

Disabling runtime checks in release builds: Method 1 build.gradle (Android project) buildTypes { release { kotlinOptions { freeCompilerArgs = [ 'Xno-param-assertions' ] } } }

Slide 65

Slide 65 text

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); }

Slide 66

Slide 66 text

Choose the right type to avoid boxing Integer Long Float Double

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

Kotlin array types Prefer the specialized ones Kotlin type Array IntArray JVM type Integer[] int[] val x = intArrayOf(1, 2, 3) val x = arrayOf(1, 2, 3)

Slide 69

Slide 69 text

6. Delegated properties

Slide 70

Slide 70 text

Delegated property example class Example { var p: String by Delegate() }

Slide 71

Slide 71 text

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, ""); this.p$delegate.setValue(this, $$delegatedProperties[0], (String)var1); } } Metadata Extra getter/setter methods

Slide 72

Slide 72 text

Delegated properties class Example { private val nameView by BindViewDelegate(R.id.name) private val textView by BindViewDelegate(R.id.text) private val imageView by BindViewDelegate(R.id.image) private val streetView by BindViewDelegate(R.id.street) private val scrollView by BindViewDelegate(R.id.scroll) private val buttonView by BindViewDelegate(R.id.button) } 6 delegate instances + 6 private getter methods

Slide 73

Slide 73 text

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(R.id.name) }

Slide 74

Slide 74 text

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)

Slide 75

Slide 75 text

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() Prefer specialized delegate classes for non-null primitive types.

Slide 76

Slide 76 text

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()) }

Slide 77

Slide 77 text

7. Ranges

Slide 78

Slide 78 text

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) }

Slide 79

Slide 79 text

Ranges: inclusion tests if(1 <= i) { if(10 >= i) { System.out.println(i); } } No cost

Slide 80

Slide 80 text

Ranges: inclusion tests Comparable types: if (name in "Alfred".."Alicia") { println(name) }

Slide 81

Slide 81 text

Ranges: inclusion tests if(name.compareTo("Alfred") >= 0) { if(name.compareTo("Alicia") <= 0) { System.out.println(name); } } No cost (since 1.1.50)

Slide 82

Slide 82 text

Ranges: inclusion tests Indirection - a function returning a range: fun myRange() = 1..10 fun rangeTest(i: Int) { if (i in myRange()) { println(i) } }

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

Ranges: inclusion tests - Declare ranges directly in the tests where they are used (no indirection) or as constants. - inline functions don’t prevent allocations.

Slide 85

Slide 85 text

Ranges: iterations Available for integral type ranges (any primitive type except for Float, Double and Boolean) for (i in 1..10) { println(i) }

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

Ranges: iterations Backwards: for (i in 10 downTo 1) { println(i) }

Slide 88

Slide 88 text

Ranges: iterations int i = 10; byte var2 = 1; while(true) { System.out.println(i); if(i == var2) { return; } --i; } No cost

Slide 89

Slide 89 text

Ranges: iterations until: for (i in 0 until size) { println(i) }

Slide 90

Slide 90 text

Ranges: iterations int i = 0; for(int var3 = size; i < var3; ++i) { System.out.println(i); } No cost (since 1.1.4)

Slide 91

Slide 91 text

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) }

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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]) }

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

Warning: using forEach() on a range (1..10).forEach { println(it) }

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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.

Slide 98

Slide 98 text

8. Bonus: when

Slide 99

Slide 99 text

when on integer value fun whenTest(value: Int): String { return when (value) { 1 -> "A" 2, 3 -> "B" else -> "" } }

Slide 100

Slide 100 text

@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

Slide 101

Slide 101 text

when on integer value with range fun whenTest(value: Int): String { return when (value) { 1 -> "A" in 2..3 -> "B" else -> "" } }

Slide 102

Slide 102 text

@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

Slide 103

Slide 103 text

Benchmarks ?

Slide 104

Slide 104 text

JVM microbenchmarks

Slide 105

Slide 105 text

JVM microbenchmarks: passing the wrong message

Slide 106

Slide 106 text

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() } }

Slide 107

Slide 107 text

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 ?

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

Android microbenchmarks: completely different conclusions

Slide 110

Slide 110 text

Microbenchmarks: same tests, different results

Slide 111

Slide 111 text

No content

Slide 112

Slide 112 text

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.

Slide 113

Slide 113 text

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 ?