原文出自 jakewharton 关于 D8
和 R8
系列文章第 13 篇。
在上篇文章介绍枚举 ordinals 的文章中,我们知道 R8
会优化 switch
表达式。在那篇文章中,枚举上 switch
的字节码被省略了,因为实际上还有更多的优化工作要做。
让我们从一个简单的枚举开始,在两个独立的源文件中切换它的内容(这在后面很重要)。
enum Greeting {
FORMAL, INFORMAL
}
复制代码
class Main {
static String greetingType(Greeting greeting) {
switch (greeting) {
case FORMAL: return "formal";
case INFORMAL: return "informal";
default: throw new AssertionError();
}
}
public static void main(String... args) {
System.out.println(greetingType(Greeting.INFORMAL));
}
}
复制代码
如果我们编译并运行这些文件,输出就如预期的那样。
$ javac Greeting.java Main.java
$ java -cp . Main
informal
复制代码
在上篇文章的讲解中,在 switch
表达式中字节码中显示了对 ordinal()
的调用,那么重新排序 greeting
的常量将中断 Main
的输出。
enum Greeting {
- FORMAL, INFORMAL
+ INFORMAL, FORMAL
}
复制代码
更改常量顺序后,我们只能重新编译 Greeting.java
,但应用程序仍会生成正确的输出。
$ javac Greeting.java
$ java -cp . Main
informal
复制代码
所以如果字节码仅依赖于 ordinal()
的值,则此代码将生成 formal
。
1. 深入字节码(Into The Bytecode)
为了更好的理解,我们来看一下 greetingType
方法的字节码。
$ javap -c Main.class
class Main {
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
}
复制代码
让我们把字节码分解一下。此方法的第一个字节码有很多信息要解包:
0: getstatic #2 // Field Main$1.$SwitchMap$Greeting:[I
复制代码
这行字节码的意思是:在类 Main$1
中查找名为 $SwitchMap$Greeting
、类型为 int[]
的静态字段。但是我们并没有在类中定义这个字段,所以它一定是由 javac
生成的。
接下来的两个字节码是调用方法参数中的 ordinal()
方法。
3: aload_0
4: invokevirtual #3 // Method Greeting.ordinal:()I`java
复制代码
Java
字节码是基于堆栈的,因此 getstatic
的 int[]
值和 ordinal()
的 int
值都保留在堆栈上(如果您不了解基于堆栈的机器是如何工作的,您可以看下面的介绍。)下一条指令使用int[]
和 int
作为其操作数。
iaload
指令在 int[]
中的 ordinal()
返回的索引处查找值。该方法的其余字节码是一个normal switch
语句,它使用数组中的值作为输入。
2. Switch Maps
很明显,$SwitchMap$Greeting
数组是一种机制,它允许我们的代码继续工作,尽管序数改变了它们的值。那么它是如何工作的呢?
编译后,switch
的每个 case
语句都对应一个下标位置,default
分支默认是 0
。
switch (greeting) {
case FORMAL: ... // <-- index 1
case INFORMAL: ... // <-- index 2
default: ... // <-- index 0
}
复制代码
$SwitchMap$Greeting
数组在运行时填充在 Main$1
的静态初始值设定项中。首先创建空 int[]
并将其分配给 $SwitchMap$Greeting
字段。
0: invokestatic #1 // Method Greeting.values:()[LGreeting;
3: arraylength
4: newarray int
6: putstatic #2 // Field $SwitchMap$Greeting:[I
复制代码
此数组的长度与常量的数量相同(可能与 case
块的数量不匹配)。这一点很重要,因为序数用作此数组的索引。
下一步字节码指令是准备 swtich
语句中的常量。
9: getstatic #2 // Field $SwitchMap$Greeting:[I
12: getstatic #3 // Field Greeting.FORMAL:LGreeting;
15: invokevirtual #4 // Method Greeting.ordinal:()I
18: iconst_1
19: iastore
复制代码
第一个 case
的 FORMAL
序号用作数组中的偏移量,数组中存储了对应的开关索引值 1
。对于非正式的序号和值 2 也是如此。这个 int[]
有效地创建了一个从序号到固定整数值集的映射,该整数值集可能会改变,但不会改变。
通过使用这个映射,switch
语句可以保持稳定,即使我们重新排列 Greeting
的常量。
3. 优化
当枚举可以与调用方分开重新编译时,javac
创建的开关映射间接寻址非常有用。Android
应用程序被打包为一个单元,因此间接寻址只不过是浪费了二进制大小和运行时开销。
通过 D8
运行上面的示例类问价表明间接寻址得到了维护。
$ java -jar $R8_HOME/build/libs/d8.jar \
--lib $ANDROID_HOME/platforms/android-29/android.jar \
--release \
--output . \
*.class
$ $ANDROID_HOME/build-tools/29.0.2/dexdump -d classes.dex
⋮
[00040c] 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 v1, v0, v1
0008: packed-switch v1, 00000024
复制代码
然而,R8
执行整个程序分析和优化。它没有必要保留这个间接寻址,因为枚举不能独立于 switch
。
[00040c] Main.greetingType:(LGreeting;)Ljava/lang/String;
-0000: sget-object v0, LMain$1;.$SwitchMap$Greeting:[I
0000: invoke-virtual {v1}, LGreeting;.ordinal:()I
0003: move-result v1
-0006: aget v1, v0, v1
0004: packed-switch v1, 00000024
复制代码
switch
的分支将被重写,说明了输入现在直接使用基于零的序数,而不是开关映射中基于一的值。由于 Main$1
及其数组不再被引用,它就像普通的死代码一样被消除。
只有删除此间接寻址,才能从枚举 ordinal() 优化,从而消除开关。否则,序数值将作为索引流入int[]
,这在一般情况下是不安全的。
4. Kotlin
在 Kotlin
中使用的枚举也会出于相同的原因生成类似的间接寻址。
val Greeting.type get() = when (this) {
Greeting.FORMAL -> "formal"
Greeting.INFORMAL -> "informal"
}
复制代码
编译时,Java
字节码显示了类似的机制,但名称不同。
$ javap -c MainKt
public final class MainKt {
public static final java.lang.String getType(Greeting);
Code:
0: aload_0
1: getstatic #21 // Field MainKt$WhenMappings.$EnumSwitchMapping$0:[I
4: swap
5: invokevirtual #27 // Method Greeting.ordinal:()I
8: iaload
9: tableswitch {
1: 36
2: 41
default: 46
}
⋮
复制代码
生成的类的后缀是 $WhenMappings
,而不是名为 $EnumSwitchMapping$0
的 int[]
。
R8
最初没有检测到 Kotlin
映射,因为这些名称略有不同。R8
的 1.6
版(包含在 AGP 3.6
中)将正确检测并消除它们。
switch map
映射消除对于二进制大小和运行时性能来说是一个很好的胜利。更重要的是,通过删除 switch
与其分支逻辑之间的间接寻址,其他优化(如将对 ordinal()
的调用转换为常量)可以消除分支。
更多的 R8
优化帖子即将发布。敬请期待!
评论