原文出自 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 . Maininformal
复制代码
在上篇文章的讲解中,在 switch 表达式中字节码中显示了对 ordinal() 的调用,那么重新排序 greeting 的常量将中断 Main 的输出。
enum Greeting {- FORMAL, INFORMAL+ INFORMAL, FORMAL }
复制代码
更改常量顺序后,我们只能重新编译 Greeting.java,但应用程序仍会生成正确的输出。
$ javac Greeting.java$ java -cp . Maininformal
复制代码
所以如果字节码仅依赖于 ordinal() 的值,则此代码将生成 formal。
1. 深入字节码(Into The Bytecode)
为了更好的理解,我们来看一下 greetingType 方法的字节码。
$ javap -c Main.classclass 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_04: 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: arraylength4: newarray int6: putstatic #2 // Field $SwitchMap$Greeting:[I
复制代码
此数组的长度与常量的数量相同(可能与 case 块的数量不匹配)。这一点很重要,因为序数用作此数组的索引。
下一步字节码指令是准备 swtich 语句中的常量。
9: getstatic #2 // Field $SwitchMap$Greeting:[I12: getstatic #3 // Field Greeting.FORMAL:LGreeting;15: invokevirtual #4 // Method Greeting.ordinal:()I18: iconst_119: 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:[I0002: invoke-virtual {v1}, LGreeting;.ordinal:()I0005: move-result v10006: aget v1, v0, v10008: 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 MainKtpublic 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 优化帖子即将发布。敬请期待!
评论