掌握聚合最新动态了解行业最新趋势
API接口,开发服务,免费咨询服务

探索 Kotlin 的隐性成本(Part 1)

2016年,杰克·沃顿(Jake Wharton)就 Java 的隐性成本进行了一系列有趣的演讲。 在同一时期,他也开始倡导使用 Kotlin 语言进行 Android 开发,但除了推荐大家使用这门语言的内联函数特性之外,他几乎没有提到说使用该语言会产生什么隐藏成本。 现在 Kotlin 在 Android Studio 3 中得到了 Google 的正式支持,而我想着通过研究它所生成的字节码来写写这个方面的东西也许是个好主意。

Kotlin 是一种现代编程语言,与 Java 相比,它拥有更多的语法糖,而因此也会拥有更多藏起来的“黑魔法”,其中一些要用的话就会有不可忽略的成本,特别是对于那些年头比较久还有版本比较低的 Android 终端设备。

这里并不是要抵制 Kotlin: 我非常喜欢这门语言,它提高了生产效率,而我也相信作为一个好的开发也需要去了解语言特性内部是如何运作的,以此才能更加好的利用它们。Kotlin 功能强大,而正所谓:

“能力越强责任越大。”

这些文章只会关注 Kotlin 1.1 在 JVM/Android 的实现,不会涉及其 Javascript 的实现。

Kotlin 字节码查看器

这是了解 Kotlin 如何被翻译成字节码的首选工具。只要在 Android Studio 安装好 Kotlin 插件,然后选择 “Show Kotlin Bytecode” 操作,来打开一个面板显示出当前 class 文件的字节码。然后你就能按下 “Decompile” 按钮来读取出对应的 Java 代码。

这里要特别提一下,我每次提到一个 Kotlin 特性都要涉及如下几个方面的内容:

  • 装箱的原生类型,他们会分配给生存时间较短的对象。

  • 实例化附加的对象不会直接在代码中显示出来。

  • 生成附加方法。你也许会了解,在 Android 应用程序中,一个 dex 文件中方法的数量是有限制的,而且在上述情况下,考虑到多个 dex 会带来限制于性能损失的叠加影响,特别是在 Lollipop 之前版本的 Android 系统之上。

关于基准的说明

我特意选择不发布任何微基准,因为大部分都是没有意义的,或者有缺陷,或者两者皆有,并且不能应用于所有的代码变化和运行环境。 在循环或嵌套循环中使用相关代码时,通常会放大负面性能影响。

此外,执行时间并不是衡量的唯一事项:也必须把新增内存使用纳入考量,因为最终所有分配的内存都要回收,而垃圾收集的成本取决于诸多因素,如可用内存和平台所使用的 GC(Garbage Collection,垃圾收集)算法。

简而言之,如果你想知道一个 Kotlin 构造是否具有一些显著的速度或内存影响,那就要在你自己的目标平台上测量你自己的代码。

高阶函数与 Lambda 表达式

Kotlin 支持把函数赋值给变量并传递变量作为其他函数的参数。接受其他函数作为参数的函数称为高阶函数。一个 Kotlin 函数可以由它的名字加前缀 :: 而引用,或直接在代码块中声明一个匿名函数,或使用 lambda 表达式语法。此中第三种是描述一个函数的最紧凑的方式。

Kotlin 是给 Java 6/7 JVM 和 Android 提供 lambda 表达式支持的最佳方式之一。

考虑下面的实用函数,它在数据库事务中执行任意操作,并返回受影响行的数量:

transaction(db: Database, body: (Database) -> Int): Int {
    db.beginTransaction()
    {
        result = body(db)
        db.setTransactionSuccessful()
        result
    } {
        db.endTransaction()
    }
}

我们可以通过使用类似于 Groovy 的语法传递一个 lambda 表达式作为最后的参数:

deletedRows = transaction(db) .delete(, , )

但 Java 6 JVM 不直接支持 lambda 表达式。那么它们是如何转换成字节码的呢?也许正如你所期望的, lambda 表达式和匿名函数被编译为函数对象。

函数对象

这是 Lambda 表达式编译后对应的 Java 呈现。

MyClass$myMethod$1Function1 {
   Object invoke(Object var1) {
      Integer.(.invoke((Database)var1));
   }

   invoke(@NotNull Database it) {
      Intrinsics.(it, );
      db.delete(, , );
   }
}

在 Android dex 文件中,每个 Lambda 表达式都被编译成函数,实际上它会导致增加 3 到 4 个方法

好消息是这些函数对象的新实例只会在必要的时候创建,这在实践中就意味着:

  • 对于捕获(capturing)表达式,每次将 Lambda 作为参数传递的时候都会创建新的函数实现,它会在执行后被垃圾回收器回收;

  • 对于非捕获(non-capturing)表达式(纯函数),则会创建一个单例函数实例,以便后面用到的时候可以复用。

[译者注:捕获 Lambda 表达式指在表达式内部访问了表达式外的非静态变量或者对象的表达式,非捕获 Lambda 表达式反之]

既然我们示例中的调用代码使用了非捕获 Lambda,它会被编译成单例而不是一个内部类:

this.transaction(db, (Function1)MyClass$myMethod$1.INSTANCE);
如果标准(非内联)的高阶函数调用捕获 Lambda,应该避免对其进行重复调用以减少垃圾回收器的压力。

装箱的开销

与 Java 8 不同,Java 8 大约有43 个不同的特定函数接口用于尽可能地避免装箱和拆箱。由 Kotlin 编译出来的函数对象只实现完全通用的接口,高效地将 Object 类型用于输入或输出值。

Function1<P1, R> : Function<R> {
    invoke(p1: P1): R
}

这表示在高阶函数中调用作为参数传入的函数,如果函数涉及基本类型(比如 Int 或 Long),实际上会对输入值和返回值有计划地装箱和拆箱。这可能会对性能产生不可忽视的影响,尤其是在 Android 中。

  在我们上面编译的 Lambda 中,你可以看到结果是装箱成 Integer 对象的。调用者代码随后会对其进行拆箱操作。

   在写一个函数作为参考的标准(非内联)高阶函数时,如果这个作为参数的函数使用的是基本类型的输入或输出值,那就要小心了。调用这个作为参数的函数会因为装箱和拆箱操作给垃圾收集器带来更大的压力。

用内联函数解决相关问题

幸好,在使用 Lambda 表达式的时候,Kotlin 有一个神奇的技巧来避免这些开销:将高级函数声明为 inline。编译器会将函数体直接内联到调用代码内部,完全避免了调用。对于高阶函数来说,更大的好处在于,作为参数的 Lambda 表达式也会被内联。实际的影响包括:

  • 声明 Lambda 不会产生函数对象实例;

  • Lambda 的输入或输出有基本类型时,不会产生装箱或拆箱动作;

  • 不会造成方法数量的增加;

  • 没有实际调用函数。这对于需要多次调用而且重度使用 CPU 的代码来说可以提升性能。

我们在将 transaction() 函数声明为内联函数之后,调用代码的 Java 呈现就变成了:

db.beginTransaction();
var5;
{
   result$iv = db.delete(, , );
   db.setTransactionSuccessful();
   var5 = result$iv;
} {
   db.endTransaction();
}

这个杀手锏有一些需要注意的地方:

  • 内部函数不能调用自身,也不能通过其它内联函数调用自身。[译者注:简单地说就是不能用于内联的递归];

  • 类中声明为 public 的内联函数只能访问这个类的公有函数和字段;

  • 代码大小会有所增长。内联一个被使用多次的长函数会使生成的代码相当大,如果这个长函数引用了其它内联的长函数,就更不得了。

如果可能,就将高阶函数内联。保持简短,若有必要,可将大块代码搬到非内联函数中。 
对于要求性能的代码所调用的函数,也可以声明内联。

我们会在以后的文章中讨论内联函数其它性能方面的优点。

原文来自:开源中国社区

声明:所有来源为“聚合数据”的内容信息,未经本网许可,不得转载!如对内容有异议或投诉,请与我们联系。邮箱:marketing@think-land.com

  • 营运车判定查询

    输入车牌号码或车架号,判定是否属于营运车辆。

    输入车牌号码或车架号,判定是否属于营运车辆。

  • 名下车辆数量查询

    根据身份证号码/统一社会信用代码查询名下车辆数量。

    根据身份证号码/统一社会信用代码查询名下车辆数量。

  • 车辆理赔情况查询

    根据身份证号码/社会统一信用代码/车架号/车牌号,查询车辆是否有理赔情况。

    根据身份证号码/社会统一信用代码/车架号/车牌号,查询车辆是否有理赔情况。

  • 车辆过户次数查询

    根据身份证号码/社会统一信用代码/车牌号/车架号,查询车辆的过户次数信息。

    根据身份证号码/社会统一信用代码/车牌号/车架号,查询车辆的过户次数信息。

  • 风险人员分值

    根据姓名和身份证查询风险人员分值。

    根据姓名和身份证查询风险人员分值。

0512-88869195
数 据 驱 动 未 来
Data Drives The Future