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