这是我参与更文挑战的第 27 天,活动详情查看: 更文挑战
原文出自 jakewharton 关于 D8 和 R8 系列文章第 14 篇。
- 原文链接 : D8 Optimizations
- 原文作者 : jakewharton
- 译者 : Antway
不,那不是打字错误!虽然到目前为止本系列中的优化都是由 R8 在整个程序优化期间完成的,但 D8 也可以执行一些简单的优化。
D8 是在这篇文章中介绍:作为新的 Java-to-Dalvik 字节码编译器引入 Android 的。它处理 Java8 (以及 Java9 和更高版本)语言特性的移植,以便在 Android上运行。它还可以解决平台中特定于供应商和版本的错误。
到目前为止,我们已经从 D8 系列中看到了这一点,但它还有另外两个职责,我们将在本期和下一期文章中介绍:
- 在不存在旧
API级别的地方使用Backporting方法。 - 执行局部优化以减小字节码大小和/或提高性能。
我们将在本系列的下一篇文章中讨论 API 的适配。现在,让我们看看 D8 可能执行的一些局部优化。
1. 重写 Switch
在过去的两篇文章中,我们介绍了 switch 的优化。对于 D8 和 R8 为某些 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:[I
0002: invoke-virtual {v2}, LGreeting;.ordinal:()I
0005: move-result v1
0006: aget v0, v0, v1
0008: packed-switch v0, 00000017
000b: new-instance v0, Ljava/lang/AssertionError;
000d: invoke-direct {v0}, Ljava/lang/AssertionError;.<init>:()V
0010: throw v0
0011: const-string v0, "formal"
0013: return-object v0
0014: const-string v0, "informal"
0016: return-object v0
0017: 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;)Z
0005: move-result v1
0006: if-eqz v1, 0010
0008: invoke-virtual {v0}, Ljava/lang/String;.length:()I
0011: move-result v1
0012: invoke-virtual {v2, v1}, Ljava/lang/String;.substring:(I)Ljava/lang/String;
000f: move-result-object v2
0010: 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 是框架中具有良好定义行为的最后一个类。
在第一篇文章发布后的九个月里,字符串上可以优化的方法的数量大幅增长。D8 和 R8 都将计算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 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
⋮
复制代码
在 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 级别上工作。



















![[02/27][官改] Simplicity@MIX2 ROM更新-一一网](https://www.proyy.com/wp-content/uploads/2020/02/3168457341.jpg)


![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)