写点什么

[译] D8 优化

用户头像
Antway
关注
发布于: 3 小时前

原文出自 jakewharton 关于 D8R8 系列文章第 14 篇。



不,那不是打字错误!虽然到目前为止本系列中的优化都是由 R8 在整个程序优化期间完成的,但 D8 也可以执行一些简单的优化。


D8 是在这篇文章中介绍:作为新的 Java-to-Dalvik 字节码编译器引入 Android 的。它处理 Java8 (以及 Java9 和更高版本)语言特性的移植,以便在 Android上运行。它还可以解决平台中特定于供应商和版本的错误


到目前为止,我们已经从 D8 系列中看到了这一点,但它还有另外两个职责,我们将在本期和下一期文章中介绍:


  • 在不存在旧 API 级别的地方使用 Backporting 方法。

  • 执行局部优化以减小字节码大小和/或提高性能。我们将在本系列的下一篇文章中讨论 API 的适配。现在,让我们看看 D8 可能执行的一些局部优化。

1. 重写 Switch

在过去的两篇文章中,我们介绍了 switch 的优化。对于 D8R8 为某些 switch 语句生成的字节码,两者都略带谎言。让我们再看一次其中的一个例子。


enum Greeting {  FORMAL, INFORMAL;    static String greetingType(Greeting greeting) {    switch (greeting) {      case FORMAL: return "formal";      case INFORMAL: return "informal";      default: throw new AssertionError();    }  }}
复制代码


在上面的 Java 字节码中为 greetingType 方法显示的使用了 lookupswitch 字节码,该字节码在匹配值时具有跳转位置的偏移量。


static java.lang.String greetingType(Greeting);  Code:     0: getstatic     #2      // Field Main$1.$SwitchMap$Greeting:[I     3: aload_0     4: invokevirtual #3      // Method Greeting.ordinal:()I     7: iaload     8: lookupswitch  {                   1: 36                   2: 39             default: 42        }    36: ldc           #4      // String formal    38: areturn    39: ldc           #5      // String informal    41: areturn    42: new           #6      // class java/lang/AssertionError    45: dup    46: invokespecial #7      // Method java/lang/AssertionError."<init>":()V    49: athrow
复制代码


tableswitch Java 字节码在转换为 Dalvik 字节码时被重写为 packed-switch 字节码。


[000584] Main.greetingType:(LGreeting;)Ljava/lang/String;0000: sget-object v0, LMain$1;.$SwitchMap$Greeting:[I0002: invoke-virtual {v2}, LGreeting;.ordinal:()I0005: move-result v10006: aget v0, v0, v10008: packed-switch v0, 00000017000b: new-instance v0, Ljava/lang/AssertionError;000d: invoke-direct {v0}, Ljava/lang/AssertionError;.<init>:()V0010: throw v00011: const-string v0, "formal"0013: return-object v00014: const-string v0, "informal"0016: return-object v00017: packed-switch-data (8 units)
复制代码


如果我们真的用 D8 编译和索引上面的源文件,它的 Dalvik 字节码输出是不同的。


 [0005f0] Main.greetingType:(LGreeting;)Ljava/lang/String; 0000: sget-object v0, LMain$1;.$SwitchMap$Greeting:[I 0002: invoke-virtual {v1}, LGreeting;.ordinal:()I 0005: move-result v1 0006: aget v0, v0, v1-0008: packed-switch v0, 00000017+0008: const/4 v1, #int 1+0009: if-eq v0, v1, 0014+000b: const/4 v1, #int 2+000c: if-eq v0, v1, 0017 000e: new-instance v0, Ljava/lang/AssertionError; 0010: invoke-direct {v0}, Ljava/lang/AssertionError;.<init>:()V 0013: throw v0 0014: const-string v0, "formal" 0016: return-object v0 0017: const-string v0, "informal" 0019: return-object v0-0017: packed-switch-data (8 units)
复制代码


可以看到在 008 位置上的 packed-switch 被替换为一系列的 if/else if 检查。根据索引,你可能会认为这会产生一个更大的二进制,但实际上恰恰相反。原始的 packed-switch 指令会被编译为 packed-switch-data 字节码,并占用 8 个单元长度。 所以整个 packed-switch 总共需要 26 字节指令而 if/else if 只需要 20 个字节指令。


一般只有在字节码节省的情况下,才会重写 switch。这取决于 case 块的数量、是否有fallthrough 以及值是否连续。D8 计算两种形式的成本,然后选择较小的形式。

2. 字符串优化

早在二月份,就写了一篇关于 R8字符串常量操作的文章。它介绍一个 OkHttp 的例子,其中对一个常量进行了 String.length 调用优化。


static String patternHost(String pattern) {  return pattern.startsWith(WILDCARD)      ? pattern.substring(WILDCARD.length())      : pattern;}
复制代码


当使用旧的 dx 编译工具时,输出的就是一个简单的转码。


[0001a8] Test.patternHost:(Ljava/lang/String;)Ljava/lang/String;0000: const-string v0, "*."0002: invoke-virtual {v2, v0}, Ljava/lang/String;.startsWith:(Ljava/lang/String;)Z0005: move-result v10006: if-eqz v1, 00100008: invoke-virtual {v0}, Ljava/lang/String;.length:()I0011: move-result v10012: invoke-virtual {v2, v1}, Ljava/lang/String;.substring:(I)Ljava/lang/String;000f: move-result-object v20010: return-object v2
复制代码


在第 008 行的字节码是 String.length 方法的调用并返回一个值,这个常量对应在 0000 位置。


然而,对于 D8,这个方法调用一个常量,在编译时被检测到并计算为相应的数值。


 [0001a8] Test.patternHost:(Ljava/lang/String;)Ljava/lang/String; 0000: const-string v0, "*." 0002: invoke-virtual {v1, v0}, Ljava/lang/String;.startsWith:(Ljava/lang/String;)Z 0005: move-result v0 0006: if-eqz v0, 000d-0008: invoke-virtual {v0}, Ljava/lang/String;.length:()I-0011: move-result v1+0008: const/4 v0, #int 2 0009: invoke-virtual {v1, v0}, Ljava/lang/String;.substring:(I)Ljava/lang/String; 000c: move-result-object v1 000d: return-object v1
复制代码


删除方法调用不是 D8 甚至 R8 通常会做的事情。应用此优化是安全的,因为 String 是框架中具有良好定义行为的最后一个类。


在第一篇文章发布后的九个月里,字符串上可以优化的方法的数量大幅增长。D8R8 都将计算isEmpty()startsWith(String)endsWith(String)contains(String)equals(String)equalsIgnoreCase(String)contentEquals(String)hashCode()length()indexOf(String)indexOf(int)lastIndexOf(String)lastIndexOf(int)compareTo(String)compareToIgnoreCase(String)substring(int)substring(int, int),以及常量字符串上的 trim()。显然,在没有 R8 内联的情况下,这些方法中的大多数都不太可能应用,但是当它发生时,它们就在那里了。

3. 已知数组长度

就像你对一个常量字符串调用 length() 方法一样,对于一个固定长度的数组调用 lenght() 方法也没什么奇怪的。


让我们在看一下 OkHttp 中的一个示例:


private fun decodeIpv6(input: String, pos: Int, limit: Int): InetAddress? {  val address = ByteArray(16)  var b = 0
var i = pos while (i < limit) { if (b == address.size) return null // Too many groups.
复制代码


address.size (在字节码层面是调用 lenght)的使用可以避免重复的 16 个常量。缺点是这个解析循环的每次迭代都会解析 dx 输出中的数组长度。


[00020c] OkHttpKt.decodeIpv6:(Ljava/lang/String;II)Ljava/net/InetAddress;0000: const/16 v5, #int 160002: new-array v0, v5, [B0004: const/4 v1, #int 00005: const/4 v2, #int 00006: if-ge v2, v8, 00360008: array-length v6, v00009: if-ne v1, v6, 000b
复制代码


0000 处字节码中常量 16 被注册到 v5 中,同时被 0002 出字节码引用,所以这个数组的长度就被保存到 v0 中。从 0006 位置开始的循环 i < limit,在循环内部, 在 0008 位置时 v0 标识的数组长度被加载到 v6 中,并且在 0009 处的 if 中使用。


D8 识别出 lenght 的查找是在一个数组引用上进行的,该数组引用不会改变,并且其大小在编译时是已知的。


 [00020c] OkHttpKt.decodeIpv6:(Ljava/lang/String;II)Ljava/net/InetAddress; 0000: const/16 v5, #int 16 0002: new-array v0, v5, [B 0004: const/4 v1, #int 0 0005: const/4 v2, #int 0 0006: if-ge v2, v8, 0036-0008: array-length v6, v0-0009: if-ne v1, v6, 000b+0009: if-ne v1, v5, 000b
复制代码


array-length 的调用被删除,同时 if 语句被重写为重用 v5 的值。


就其本身而言,这种模式并不太常见。当 R8 内联生效并且检查 array.length 的方法内联到声明新数组的调用程序中时,它再次发挥了良好的作用。




每个优化都很小。D8 只能在没有外部可见效果且不改变程序行为的情况下执行优化。这在很大程度上限制了它在单个方法体中进行的优化。


在运行时,您无法判断 switch 是否重新连接到 if/else 条件。无法判断对常量字符串的length() 调用是否已替换为其等效的常量值。无法判断在同一方法中初始化的数组上对 length 的调用是否已替换为输入大小。D8 能够执行的每一种优化(以及其他一些优化)都会产生更小、更高效的字节码。当然,当你调用 R8 的全部功能时,它们的影响会成倍增加。


在下一篇文章中,我们将开始讨论 D8 如何在现有类型上对新 API 进行兼容,以便在旧的 API 级别上工作。

用户头像

Antway

关注

持续精进,尽管很慢 2019.05.27 加入

专注开源库

评论

发布
暂无评论
[译] D8 优化