2017-08-29 | translate

[翻译]kotlin-hidden-cost-part-1

https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-1-fbb9935d9b62

Jake Wharton 大佬写过一篇 Java’s hidden costs 的文章,balabala
这里介绍 kotlin 相关内容

Kotlin 是一种更现代的语言,与 Java 相比提供了更多的语法糖,这些语法糖的背后有着同样多的“黑魔法”。,其中一些有着不可忽视的性能消耗,尤其是需要兼容老设备或者低端设备的项目。

我也不是反对 Kotlin,正相反我非常喜欢这门语言,它可以提升工作效率。

Kotlin Bytecode inspector

action -> “Show Kotlin Bytecode”
“Decompile” -> 查看反编译后 java 文件

每次提及 kotlin feature 都包含以下几点

  • 原始类型装箱,这会创建生命周期很短的对象
  • 代码中不存在,但却需要额外例化的对象
  • 额外生成的方法。因为 Android 方法数的限制,需要使用 MultiDex 解决,随之而来的是性能问题,尤其在 Lollipop 之前的版本上更为严重。

A note about benchmarks

我也不是谦虚,我这个测试写的不具有普遍适用性,balabala,就不放出来了。
除了运行速度,内存消耗什么的也是要考虑一个的,你们要测自己测

Higher-order functions and Lambda expressions

我们 kotlin 支持高阶函数,想用 lambda 什么的随便,同时也是在 Java 6/7 JVMs and Android 环境下使用 lambda 最好的方式。

看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun transaction(db: Database, body: (Database) -> Int): Int {
db.beginTransaction()
try {
val result = body(db)
db.setTransactionSuccessful()
return result
} finally {
db.endTransaction()
}
}

// 实际使用
val deletedRows = transaction(db) {
it.delete("Customers", null, null)
}

Java 6 JVMs 不能直接使用 Lambda 表达式,所以在字节码里 lambda 和匿名函数被编译为 Function 对象。

Function objects

上面的 lambda 表达式编译后转换成 Java 的形式就是下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
class MyClass$myMethod$1 implements Function1 {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1) {
return Integer.valueOf(this.invoke((Database)var1));
}

public final int invoke(@NotNull Database it) {
Intrinsics.checkParameterIsNotNull(it, "it");
return db.delete("Customers", null, null);
}
}

Android 的 dex 文件里,每个 lambda 表达式编译为 Function 对象后都会给总方法数增加 3 到 4 个方法
幸好 Function 对象只在必要时才创建,通常情况下,这意味着:

  • 对于 capture 表达式 ,每次使用时都会创建,并在执行后被 GC 回收。
  • 对于 non-capturing 表达式(纯函数),会创建一个单例 Function 对象,并复用。

上面的示例是 non-capturing lambda ,所以被编译为单例,而不是内部类。

1
this.transaction(db, (Function1)MyClass$myMethod$1.INSTANCE);

尽量不要重复调用内部执行 capturing lambdas 的标准(非内联的)高阶函数,减轻 GC 的压力。

Boxing overhead

Java8 创建了 43 个特定的函数接口来尽可能的避免拆箱装箱上的性能损耗,Kotlin 没有这么做, Function 对象只实现了一个泛型的接口,实际上每个输入输出对象都被视为 Object

1
2
3
4
5
/** 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
}

也就是说以高阶函数的形式传递参数,如果涉及到原始类型(比如 IntLong)输入或输出 ,会引发 systematic boxing and unboxing 。可能会导致不可忽视的性能问题,尤其是在 Android 设备上。

上面的例子里,返回值被封装为 Integer 对象,调用函数会立即将其拆箱。

在写一个涉及原始类型做输入输出的标准(非内联)高阶函数得十分小心,因为重复调用函数时,对这些值做拆箱装箱会给 GC 带来很大的压力。

Inline functions to the rescue 内联函数拯救世界

幸好 Kotlin 提供了一个方法可以避免这些消耗:将高阶函数声明为 inline。编译器会将函数体直接内联到调用代码里。对高阶函数益处更明显,以参数形式传入的 lambda 函数体也将被内联,这意味着:

  • 不会产生 Function 对象
  • 在 lambda 使用原始类型也不会有拆箱装箱的消耗
  • 不会增加方法数
  • 实际执行中没有方法调用,当方法重复调用时对 CPU 的消耗更低

    当我们把 transaction() 声明为 inline 后,对应的 Java 形式为:

1
2
3
4
5
6
7
db.beginTransaction();
try {
int result$iv = db.delete("Customers", null, null);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}

很有用的特性,当然使用也要遵守基本法:

  • 内联函数不能调用自己
  • 在一个类中,声明为 public 的内联函数,只能访问这个类的 public 方法和字段
  • The code will grow in size,多次引用一个很长的内联函数会导致最终生成的代码巨长无比,如果这个内联函数还引用其他巨长无比的内联函数画面一定美不胜收。

声明内联函数时应尽可能克制其长度,必要时把过长的代码移到非内联函数中。可以在性能至关重要的部分使用 inline。


Companion objects 伴生对象

Kotlin 没有静态的字段和方法。和对象无关的方法或字段可以声明在 companion object 中。

Accessing private class fields from its companion object

伴生对象访问类的私有字段
看这个例子:

1
2
3
4
5
6
7
8
class MyClass private constructor() {

private var hello = 0

companion object {
fun newInstance() = MyClass()
}
}

编译时,伴生对象被编译为一个单例。就是说和 Java 里其他类访问私有字段一样,伴生对象访问外部类的私有字段和方法时会生成额外的方法以供调用。 每当伴生对象进行读写操作时都是执行一个静态方法调用。

1
2
3
ALOAD 1
INVOKESTATIC be/myapplication/MyClass.access$getHello$p (Lbe/myapplication/MyClass;)I
ISTORE 2

Java 可以用 package 访问权限来避免生成这些方法,不过 kotlin 没有。
使用 publicinternal 的话会生成默认的 getter 和 setter 方法供外界调用,技术上来说,调用实例方法比静态方法更 expensive 。所有不必因为考虑性能的原因修改可见性。

如果你需要伴生类去反复的修改类的字段,也许可以考虑把值 cache 到 local variable 来避免重复额外的隐式方法调用。

Accessing constants declared in a companion object

访问伴生对象中的常量
在 kotlin 中,我们常常把 static 常量声明到伴生对象里。

1
2
3
4
5
6
7
8
9
class MyClass {
companion object {
private val TAG = "TAG"
}

fun helloWorld() {
println(TAG)
}
}

代码看起来整洁简单,但是背后的实现却不像代码看起来那样。
和上面提到的一样,访问伴生对象中的 private 常量会导致伴生类的实现额外生成 synthetic getter 方法

1
2
3
GETSTATIC be/myapplication/MyClass.Companion : Lbe/myapplication/MyClass$Companion;
INVOKESTATIC be/myapplication/MyClass$Companion.access$getTAG$p (Lbe/myapplication/MyClass$Companion;)Ljava/lang/String;
ASTORE 1

事实上更糟,这个生成的方法并不直接返回值,而是调用一个 kotlin 生成的实例方法 getter。

1
2
3
ALOAD 0
INVOKESPECIAL be/myapplication/MyClass$Companion.getTAG ()Ljava/lang/String;
ARETURN

声明为 public 的话,就会直接调用 getter 方法,不用生成额外的方法。

这还没完,结果显示储存一个常量时,kotlin 编译器会在 main class 生成一个 private static final 字段,而不是伴生对象。有因为它时私有的,所以想要伴生对象可以访问它,还得生成 access 方法。

1
2
INVOKESTATIC be/myapplication/MyClass.access$getTAG$cp ()Ljava/lang/String;
ARETURN

最终由这个方法返回值:

1
2
GETSTATIC be/myapplication/MyClass.TAG : Ljava/lang/String;
ARETURN

所以,当你访问一个伴生对象里的 private constant field 时,实际上是:

  • 调用类 A 伴生对象 B 的一个静态方法
  • 这个方法调用伴生对象 B 的实例方法
  • 这个实例方法再调用这个类 A 的静态方法
  • 最终返回类 A 的静态字段值

等价的 Java 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final class MyClass {
private static final String TAG = "TAG";
public static final Companion companion = new Companion();

// synthetic
public static final String access$getTAG$cp() {
return TAG;
}

public static final class Companion {
private final String getTAG() {
return MyClass.access$getTAG$cp();
}

// synthetic
public static final String access$getTAG$p(Companion c) {
return c.getTAG();
}
}

public final void helloWorld() {
System.out.println(Companion.access$getTAG$p(companion));
}
}

我们有什么方法可以让字节码变少吗?有,但不保证适用于所有情况。

第一个,使用 const 关键字修饰字段为编译时常量。会把值直接内联到调用位置,仅适用于原始类型和 String 对象。

1
2
3
4
5
6
7
8
9
10
class MyClass {

companion object {
private const val TAG = "TAG"
}

fun helloWorld() {
println(TAG)
}
}

第二个,用 @JvmField注解修饰伴生对象的 public 字段,告诉编译器像纯 Java 一样处理这个常量。这个注解实际上只是为了兼容 Java 创建的,如果不是为了用于 Java 代码的话,作者不建议使用。而且它仅可用于 public 字段。至于 Android 方面,你可能只会在实现 Parcelable 对象时使用到:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyClass() : Parcelable {

companion object {
@JvmField
val CREATOR = creator { MyClass(it) }
}

private constructor(parcel: Parcel) : this()

override fun writeToParcel(dest: Parcel, flags: Int) {}

override fun describeContents() = 0
}

最后一个,你可以用 Proguard优化字节码,祈祷可以合并些方法,不保证绝对有效。

Reading a “static” constant from a companion object adds two to three additional levels of indirection in Kotlin compared to Java and two to three additional methods will be generated for each of these constants.
Always declare primitive type and String constants using the const keyword to avoid this.
For other types of constants you can’t, so if you need to access the constant repeatedly, you may want to cache the value in a local variable.
Also, prefer storing public global constants in their own object rather than a companion object.
和 Java 相比,访问伴生对象的 static 常量增加了两到三个间接访问,每个常量字段都是。
尽量使用 const 修饰原始类型和 String 来避免
对于其他不能修饰的常量类型,并且还要重复访问的,考虑 cache the value in a local variable。
也可以把 public global constants 储存在自己的对象上,别房放到伴生对象里。


over
part 2:local functions, null safety and varargs.


Ref