Kotlin在Android上的性能

今天让我们谈谈Kotlin在生产中的Android性能。 让我们来看一下,实施棘手的优化,比较字节码。 最后,我们将认真进行比较并衡量基准。

本文基于亚历山大·斯米尔诺夫Alexander Smirnov)在AppsConf 2017上的一份报告,将有助于弄清是否有可能用Kotlin编写代码,这在速度上不会逊于Java。


演讲者简介: PapaJobs的CTO Alexander Smirnov, 在Faces中运行视频博客Android,也是Mosdroid社区的组织者之一。

让我们从您的期望开始。

您认为Kotlin在运行时是否比Java慢? 还是更快? 或者也许没有太大区别? 毕竟,两者都可以使用虚拟机为我们提供的字节码。

让我们做对。 传统上,当出现比较性能的问题时,每个人都希望看到基准和特定数字。 不幸的是,对于Android,没有JMH( Java Microbenchmark Harness ),所以我们不能仅仅衡量在Java中可以完成多酷。 那么,我们可以做如下所述的测量吗?

fun measure() : Long { val startTime = System.nanoTime() work() return System.nanoTime() - startTime } adb shell dumpsys gfxinfo %package_name% 

如果您曾经尝试过以这种方式来衡量代码,那么一位JMH开发人员将伤心,哭泣,并在梦里来到您身边-永远不要那样做。

在Android上,您可以进行基准测试,尤其是Google在去年的I / O上演示了这一点。 他们说,他们极大地改进了虚拟机,在本例中为ART,如果在Android 4.1上分配一个对象大约需要600-700纳秒,那么在第八个版本中,大约需要60纳秒。 即 他们能够在虚拟机中如此精确地对其进行测量。 为什么我们都不做-我们没有这样的工具。

如果我们查看所有文档,那么唯一可以发现的就是上面的建议,即如何测量UI:

亚行shell dumpsys gfxinfo%package_name%

实际上,让我们以这种方式进行操作,最后看看它会带来什么。 但是首先,我们将确定我们将测量的内容以及我们还能做的其他事情。

下一个问题。 创建一流的应用程序时,您认为性能在哪里重要?

  1. 绝对无处不在。
  2. UI线程。
  3. 自定义视图+动画。




我最喜欢第一个选项,但是最有可能相信不可能使所有代码都非常非常快地工作,并且至少没有UiThread或自定义视图很重要。 我也同意这一点-这非常非常重要。 没有人会注意到在您自己的JSON流中将反序列化10毫秒以上的事实。

格式塔心理学说,当我们眨眼大约150-300毫秒时,人眼无法聚焦,看不到那里实际发生的情况。 然后这10毫秒的天气就没有了。 但是,如果我们回到格式塔心理学,那么重要的不是我真正看到的和发生的事情,而是我作为用户所理解的很重要。

即 如果我们让用户认为他拥有非常非常快的所有东西,但是实际上,例如在漂亮的动画的帮助下,它会被完美地击败,那么即使实际上不是这样,他也会感到满意。

iOS中的格式塔心理学主题已经移动了很长时间。 因此,如果您将两个具有相同处理时间但在不同平台上的应用程序并排放置,则似乎在iOS上一切都更快。 iOS中的动画处理要快一些,较早的动画会在启动时启动,并会启动许多其他动画,因此它很漂亮。

因此, 第一个规则是考虑用户。

对于第二条规则,您需要将自己沉浸在铁杆中。

小提琴风格


为了诚实地评估Kotlin的性能,我们将其与Java进行比较。 因此,事实证明,无法衡量仅在Kotlin中存在的某些事物,例如:

  • 集合Api。
  • 方法默认参数。
  • 数据类。
  • 化类型。
  • 协程。

Kotlin提供给我们的Collection API非常酷,非常快。 在Java中,这根本不存在,只有不同的实现。 例如,Liteweight Stream API库会变慢,因为它所做的一切与Kotlin相同,但是会为操作分配一两个额外的分配,因为一切都会变成一个附加的对象。

如果我们采用Java 8的Stream API,它的运行速度将比Kotlin Collection API慢,但有一个条件-如果我们对大量Stream API数据启用并行,则Collection API中不会出现这种瘫痪, Java将绕过Kotlin Collection API。 因此,我们无法比较这些事情,因为我们正是从Android的角度进行比较。

在我看来,第二件事是无法比较的,它是Method的默认参数 -一项很酷的功能,顺便说一下,它在Dart中。 当您调用某个方法时,它可能具有一些参数,这些参数可能需要一些值,但可能为NULL。 因此,您不会使用10种不同的方法,而是使用一种方法并说其中一个参数可以为NULL,并且将来使用时不带任何参数。 即 他会看,参数已经来了,还是他还没有来。 这很方便,因为您可以编写更少的代码,但是不便之处在于您必须为此付费。 这是语法糖:作为开发人员,您认为这是一个API方法,但实际上,在幕后,该方法的每个带有缺失参数的变体都是在字节码中生成的。 这些方法中的每一个都还逐位检查此参数是否到达。 如果成功,那么就创建一个位掩码,并根据该位掩码实际调用您编写的原始方法。 按位运算,所有的一切花了一点钱,但是却很少,而且为了方便起见,这是正常的。 在我看来,这绝对是正常的。

下一个无法比较的项目是数据类

每个人都在哭泣,在Java中有一些参数,这些参数有一些模型类。 即 您可以获取参数并为所有这些参数执行更多方法,获取器和设置器。 事实证明,对于具有十个参数的类,您仍然需要一整套吸气剂,二传手和更多的东西。 此外,如果您不使用生成器,则必须用手书写,这通常很糟糕。

Kotlin让您摆脱一切。 首先,由于Kotlin中有属性,因此您无需编写getter和setter。 它没有类参数,所有属性 。 无论如何,我们是这样认为的。 其次,如果您将其写为Data类,则将生成其他所有内容。 例如equals(),toStrung()/ hasCode()等。

当然,这也有缺点。 例如,我不需要在equals()中一次比较数据类的所有20个参数,而只需要比较3。有人不喜欢这一切,因为这样做会降低性能,并且还会产生很多东西服务功能,并且编译后的代码非常庞大。 也就是说,如果您手动编写所有内容,则与使用数据类相比,代码将更少。

由于其他原因,我不使用数据类。 以前,此类类的扩展受到限制。 现在每个人对此都比较好,但是习惯仍然存在。

在Kotlin中,什么是非常非常酷的?它将比Java上的更快? 这是Reified类型 ,顺便说一句,在Dart中也是如此。

您知道在使用泛型时,类型擦除将在编译阶段删除,并且在运行时您不再知道该泛型的实际对象是什么。

使用Reified类型时,在Java中不需要反射时,您不需要在很多地方使用反射,因为使用内联方法时,您可以通过Reified知道类型,因此事实证明您不使用反射,并且代码运行更快。 魔术。

还有协程 。 它们非常酷,我非常喜欢它们,但是在演奏时,它们仅包含在alpha版本中,因此无法与它们进行正确的比较。

领域


因此,让我们继续前进,继续我们可以与Java进行比较的内容以及我们可以总体上影响的内容。

 class Test { var a = 5 var b = 6 val c = B() fun work () { val d = a + b val e = ca + cb } } class B (@JvmField var a: Int = 5,var b: Int = 6) 

就像我说的,我们没有该类的参数,而是有属性。

我们有var,有val,有一个外部类,其一个属性是@JvmField,我们将研究work()函数实际发生的情况:我们将自己类的字段a和字段b的值相加,外部类的字段a和字段b的值,该值写在不可变字段c中。

问题是,实际上将在d = a + b中调用什么。 我们都知道,一旦有此属性,就将为此参数调用此类的getter。

  L0 LINENUMBER 10 L0 ALOAD 0 GETFIELD kotlin/Test.a : I ALOAD 0 GETFIELD kotlin/Test.b : I IADD ISTORE 1 

但是,如果我们查看字节码,我们将看到实际上正在访问getfield。 也就是说,字节码中的这个不是对InvokeVirtual函数的调用,而是对字段的直接访问。 最初没有任何承诺要我们拥有所有属性,而不是字段。 事实证明,科特林在欺骗我们,这是直接的吸引力。

如果我们确实看到另一行生成了什么字节码,那么会发生什么情况:val e = ca + cb?

  L1 LINENUMBER 11 L1 ALOAD 0 GETFIELD kotlin/Test.c : Lkotlin/B; GETFIELD kotlin/Ba : I ALOAD 0 GETFIELD kotlin/Test.c : Lkotlin/B; INVOKEVIRTUAL kotlin/B.getB ()I IADD ISTORE 2 

以前,如果您要访问私有财产,那么您总是会收到一个InvokeVirtual调用。 如果这是私有财产,则可以通过GetField对其进行访问。 GetField比InvokeVirtual快得多,Android的规范声称直接访问字段要快3到7倍。 因此,建议您始终引用Field,而不是通过getter或setter来引用。 现在,尤其是在第八个ART虚拟机中,已经有不同的数字,但是如果您仍然支持4.1,这将是正确的。

因此,事实证明,拥有GetField而不是InvokeVirtual对我们仍然是有益的。

现在,如果要访问自己的类的属性,或者如果这是公共属性,则必须设置@JvmField,就可以实现GetField。 然后,GetField调用将在字节码中完全相同,这将提高3-7倍。

显然,我们在这里以纳秒为单位讲话,而且只有一个宝座,它很小,非常小。 但是,另一方面,如果您在UI线程中执行此操作(例如,在ondraw方法中访问某种视图),则这将影响每个帧的呈现,并且您可以更快地执行此操作。

如果我们将所有优化加起来,那么总而言之,它可以提供一些帮助。

静态!


静电呢? 我们都知道,在Kotlin中,static是一个伴随对象。 以前,您可能添加了某种标记,例如public static,final static等,如果将其转换为Kotlin代码,则将获得一个伴随对象,该对象将编写如下内容:

  companion object { var k = 5 fun work2() : Int = 42 } 

您认为该条目与Java中的标准静态最终声明相同吗? 完全是静态的吗?

是的,确实,Kotlin在这里声明它在Kotlin中-静态,该对象表示它是静态的。 实际上,这不是静态的。

如果查看生成的字节码,将看到以下内容:

  L2 LINENUMBER 21 L2 GETSTATIC kotlin/Test.Companion : Lkotlin/Test$Companion; INVOKEVIRTUAL kotlin/Test$Companion.getK ()I GETSTATIC kotlin/Test.Companion : Lkotlin/Test$Companion; INVOKEVIRTUAL kotlin/Test$Companion.work2 ()I IADD ISTORE 3 

生成一个Test.Companion;为其创建实例的单例对象,该实例被写入其自己的字段。 之后,将通过此对象访问伴随对象之一。 他采用getstatic,即此类的静态实例,并在其上调用getK函数invokevirtual,与work2函数完全相同。 因此我们得到它不是静态的。

这很重要,因为在较早的JVM上,invokestatic比invokevirtual快30%。 当然,现在在HotSpot,优化的虚拟化真的很酷,而且几乎是看不见的。 但是,您需要牢记这一点,尤其是因为有一个额外的分配,并且4ST1上的额外位置太700纳秒。

让我们看一下如果反向部署字节码时出现的Java代码:

 private static int k = 5; public static final Test.Companion Companion = new Test.Companion((DefaultConstructorMarker)null); public static final class Companion { public final int getK() { return Test.k;} public final void setK(int var1) { Test.k = var1; } public final int work2() { return 42; } private Companion() { } // $FF: synthetic method public Companion(DefaultConstructorMarker $constructor_marker) { this(); } } 

创建一个静态字段,创建Companion对象的静态最终实现,创建getter和setter,并且正如您所看到的,引用内部的静态字段时,将出现一个附加的static方法。 一切都很难过。

我们怎么办,确保它不是静态的? 我们可以尝试添加@JvmField和@JvmStatic,看看会发生什么。

 val i = k + work2() companion object { @JvmField var k = 5 JvmStatic fun work2() : Int = 42 } 

我马上说,您不会离开@JvmStatic,它将是同一个对象,因为这是一个伴随对象,所以将对该对象进行额外的分配,并且会有一个额外的调用。

 private static int k = 5; public static final Test.Companion Companion = new Test.Companion((DefaultConstructorMarker)null); public static final class Companion { @JvmStatic public final int work2() { return 42; } private Companion() {} // $FF: synthetic method public Companion(DefaultConstructorMarker $constructor_marker) { this(); } } 

但是调用将仅针对k进行更改,因为它将是@JvmField,它将直接作为getstatic使用,不再生成getter和setter。 但是对于work2函数,什么都不会改变。

  L2 LINENUMBER 21 L2 GETSTATIC kotlin/Test.k : I GETSTATIC kotlin/Test.Companion : Lkotlin/Test$Companion; INVOKEVIRTUAL kotlin/Test$Companion.work2 ()I IADD ISTORE 3 

关于如何创建静态的第二种选择是在Kotlin文档中提出的,因此据说我们可以只创建一个对象,这就是静态代码。

 object A { fun test() = 53 } 

实际上,事实并非如此。

 L3 LINENUMBER 23 L3 GETSTATIC kotlin/A.INSTANCE : Lkotlin/A; INVOKEVIRTUAL kotlin/A.test ()I POP 

事实证明,我们从创建的单调中进行了一个getstatic实例调用,并调用了完全相同的虚方法。

我们可以实现invokestatic的唯一方法是高阶函数。 例如,当我们只在类之外编写一些函数时,fun test2实际上将称为静态。

  fun test2() = 99 L4 LINENUMBER 24 L4 INVOKESTATIC kotlin/TestKt.test2 ()I POP 

此外,最有趣的是,将创建一个类,一个对象(在本例中为testKt),它将为其自身生成一个对象,将生成一个将其放入该对象的函数,现在将其称为invokestatic。

为什么这样做是无法理解的。 许多人对此不满意,但是有些人认为这种实现是很正常的。 由于虚拟机,包括。 艺术在进步,但现在并不是那么关键。 在Android的第八版中,就像在HotSpot上一样,所有内容都进行了优化,但是这些小问题仍然会稍微影响整体性能。

可空性


 fun test(first: String, second: String?) : String { second ?: return first return "$first $second" } 

这是下一个有趣的例子。 似乎我们注意到,second可以为空,并且在对其进行任何操作之前必须对其进行检查。 在这种情况下,我希望我们能有一个。 如果第二秒不等于零,则部署此代码时,我认为执行会更进一步,并且仅先输出。

这实际上如何在Java代码中展现出来? 实际上会有支票。

 @NotNull public final String test(@NotNull String first,@Nullable String second) { Intrinsics.checkParameterIsNotNull(first, "first"); return second != null ? (first + " " + second) : first; } 

我们将首先获得本征。 假设我说这个

如果将扩展为三元运算符。 但是除此之外,尽管我们甚至修复了第一个参数不能为空的问题,但仍将通过内部函数对其进行检查。

Intrinsics是Kotlin的内部类,具有一组特定的参数和检查。 每次使方法参数不可为空时,无论如何都会对其进行检查。 怎么了 然后,我们在Interop Java中工作,您可能会期望它在这里不会为空,但是对于Java,它将来自某个地方。

如果选中此选项,它将沿着代码走得更远,然后在调用10-20个方法之后,您将使用一个参数执行某项操作,尽管该参数可能不能为空,但由于某种原因它确实是可以为空的。 一切都会落到您身上,您将无法理解实际发生的情况。 为了避免这种情况,每次传递null参数时,仍然需要检查它。 并且如果它可以为空,那么将会有一个例外。

此检查也很有价值,如果有很多检查,那将不是很好。

但是实际上,如果我们谈论的是HotSpot,则这些Intrinsics的10次调用将花费大约4纳秒。 这非常非常小,您不必为此担心,但这是一个有趣的因素。

基本原则


在Java中,有一种称为原语的东西。 众所周知,在Kotlin中,没有基元,我们始终使用对象进行操作。 在Java中,它们用于在一些次要计算中为对象提供更高的性能。 添加两个对象比添加两个基元要昂贵得多。 考虑一个例子。

  var a = 5 var b = 6 var bOption : Int? = 6 

有三个数字,因为将推导出前两个非空类型,而对于第三个,我们说它可以为空。

  private int a = 5; private int b = 6; @Nullable private Integer bOption = Integer.valueOf(6); 

如果查看字节码并查看生成了哪个Java代码,则前两个数字不为null,因此它们可以是原语。 但是原语不能包含Null,只有一个对象可以执行此操作,因此将为第三个数字生成一个对象。

自动装箱


当使用基元并使用基元和非基元执行操作时,您将需要将其中之一转换为基元或对象。

而且,如果您在Kotlin中使用可空值和不可空值进行操作,那么性能损失会很小,这并不奇怪。 而且,如果有很多这样的操作,那么您将损失很多。

  val a: String? = null var b = a?.isBlank() == true 

看到装箱/拆箱的位置在哪里? 在查看字节码之前,我也没有看到。

 if (a != null && a.isBlank()) true else false 

实际上,我希望会有这样的比较:如果字符串不为null且为空,则设置为true,否则设置为false。 一切似乎都很简单,但是实际上生成了以下代码:

 String a = (String)null; boolean b = Intrinsics.areEqual(a != null ? Boolean.valueOf(StringsKt.isBlank((CharSequence)a)) : null, Boolean.valueOf(true)); 

让我们看看里面。 变量a被采用,将其强制转换为CharSequence,强制转换后也已使用了一段时间,另一个检查称为-StringsKt.isBlank-这是CharSequence扩展函数的编写方式,因此将其强制转换并发送。 由于第一个表达式可以为空,因此将其接受并进行装箱,并将其全部包装在Boolean.valueOf中。 因此,真正的原语也成为对象,并且只有在已经进行验证并调用Intrinsics.areEqual之后,该原语才成为对象。

看来这是一个简单的操作,但结果却出乎意料。 实际上,这种事情很少。 但是,当您可以拥有可为空/不可为空的内容时,就可以生成很多这样的东西,而这是您从未期望的。 因此,我建议您尽快摆脱晦涩难懂的地方。 即 尽早获得值的免疫力,并远离可为空值,以便您尽可能快地不为空。

循环


下一个有趣的事情。

您可以使用Java中的常规方式,但也可以使用新的便捷API-立即在列表中编写元素枚举。 , work, it - .

 list.forEach { work(it * 2) } 

. , . , Google, , ArrayList for 3 , . .

, ArrayList, — foreach.

 inline fun <reified T> List<T>.foreach(crossinline action: (T) -> Unit): Unit { val size = size var i = 0 while (i < size) { action(get(i)) i++ } } list.foreach { } 

API, - . , Kotlin: extension , «», reified, .. , , , crossinline. , , . 3 , Android Google.

RANGES


Ranges.

 inline fun <reified T> List<T>.foreach(crossinline action: (T) -> Unit): Unit { val size = size for(i in 0..size) { work(i * 2) } } 

: Unit -. −1, until , , . , , ranges. 即 , . step. .

INTRINSICS


- Intrinsics, :

 class Test { fun concat(first: String, second: String) = "$first $second" } 

Intrinsics — second, first.

 public final class Test { @NotNull public final String concat(@NotNull String first, @NotNull String second) { Intrinsics.checkParameterIsNotNull(first, "first"); Intrinsics.checkParameterIsNotNull(second, "second"); return first + " " + second; } } 

, gradle. , - 4 , . Kotlin UI, , nullable, Kotlin :

kotlinc -Xno-call-assertions -Xno-param-assertions Test.kt

Intrinsics, , .

, , . — Xno-param-assertions — Intrinsics, .

, , , , , . , , , .

REDEX


, , , Proguard. , 99% , , . Android 8.0 , . , .

, Proguard, Facebook, Redex . -, , . , Jvm Fields , .

, Redex . , , , Proguard, , . Redex 7% APK. , .

BENCHMARKS


. , , . , . , dumpsys gfxinfo , . github github.com/smred .

, Huawei.


. — , . , , 0,04 . , , — , .


Kotlin, . , , . - , Kotlin , Java. , , , , . .


, , , , Kotlin Java. , - , , , , , .


, : - Kotlin , .. . , . - - — 2 , Galaxy S6, .


Google Pixel. , 0,1 .



, , ,

  • UI custom view.
  • onmeasure-onlayout-ondraw. autoboxing, not null ..
  • Kotlin, Java , .
  • — .

, , . , , , , Kotlin, . , Kotlin .

, .

brand new AppsConf , Android . , . , 8 9 .

Source: https://habr.com/ru/post/zh-CN420143/


All Articles