原文出自 jakewharton 关于 D8 和 R8 系列文章第 12 篇。
枚举是(并且一直是!)一种推荐的方法来创建常量。通常情况下,枚举只提供一组可能的常量,而不是其他的。但是作为完整类,枚举还可以携带辅助方法和字段(实例和静态)或实现接口。
对于枚举的优化通常就是在它出现的位置进行处理,一种常见的优化是用整数值替换简单的引用(即对于那些没有字段、方法或接口的枚举)。但是,还有其他一些优化适用于所有仍然可用的枚举。
1. Ordinal 方法
每个枚举常量都有一个 ordinal() 方法,该方法返回它在所有常量列表中的位置,范围在[0,N],它可以用于索引到其他基于零的数据结构,如数组甚至位。最常见的用法就是在 Java 编译器中的 switch 语句。
enum Greeting { FORMAL { @Override String greet(String name) { return "Hello, " + name; } }, INFORMAL { @Override String greet(String name) { return "Hey " + name + '!'; } };
abstract String greet(String name);
static String type(Greeting greeting) { switch (greeting) { case FORMAL: return "formal"; case INFORMAL: return "informal"; default: throw new AssertionError(); } }}
复制代码
查看编译的字节码显示了对 ordinal() 的隐藏调用。
[000a34] Greeting.type:(LGreeting;)Ljava/lang/String;0000: invoke-virtual {v1}, LGreeting;.ordinal:()I0003: move-result v1 ⋮
复制代码
如果我们用其中一个常量调用这个方法,就会出现一个优化的机会。
public static void main(String... args) { System.out.println(Greeting.type(Greeting.INFORMAL));}
复制代码
由于这是整个应用程序中类型的唯一引用处,所以 R8 将方法内联起来。
[000b60] Greeter.main:([Ljava/lang/String;)V0000: sget-object v1, Ljava/lang/System;.out:Ljava/io/PrintStream;0002: sget-object v0, LGreeting;.INFORMAL:LGreeting;0004: invoke-virtual {v0}, LGreeting;.ordinal:()I0007: move-result v0 ⋮0047: invoke-virtual {v1, v2}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V0050: return-void
复制代码
字节码索引 0002 查找 INFORMAL 枚举常量,然后 0004-0007 调用其 oridinal() 方法。这是一个冗余的操作,因为常量的序号在编译时是已知的。
R8 检测常量引用流中对 ordinal() 的调用,并调用将产生的正确整数值替换。
[000b60] Greeter.main:([Ljava/lang/String;)V 0000: sget-object v1, Ljava/lang/System;.out:Ljava/io/PrintStream;-0002: sget-object v0, LGreeting;.INFORMAL:LGreeting;-0004: invoke-virtual {v0}, LGreeting;.ordinal:()I-0007: move-result v0+0002: const/4 v0, #int 1 ⋮ 0042: invoke-virtual {v1, v2}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V 0045: return-void
复制代码
这个常量值现在流入 switch 语句,只留下所需的分支就可以消除它。
[000b60] Greeter.main:([Ljava/lang/String;)V 0000: sget-object v1, Ljava/lang/System;.out:Ljava/io/PrintStream;-0002: const/4 v0, #int 1- ⋮+0002: const-string v0, "informal" 0004: invoke-virtual {v1, v0}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V 0007: return-void
复制代码
尽管该语言提供了枚举的切换,但它的实现都是基于序数值中的整数。在固定常量上替换对 ordinal() 的调用是一种简单的优化,但它允许更高级的优化(如分支消除)应用于否则无法应用的地方。
2. Names 方法
除了 ordinal() 之外,每个枚举常量还通过 name() 方法公开其声明的名称。默认情况下, toString() 还将返回声明的名称,但由于该方法可以被重写,因此必须有一个不同的名称 name()。
enum Greeting { FORMAL { /* … */ }, INFORMAL { /* … */ }; abstract String greet(String name); @Override public String toString() { return "Greeting(" + name().toLowercase(US) + ')'; }}
复制代码
name() 的值通常用于 display、logging 或 serialization。
static void printGreeting(Greeting greeting, String name) { System.out.println(greeting.name() + ": " + greeting.greet(name));}
public static void main(String... args) { printGreeting(Greeting.FORMAL, "Jake");}
复制代码
运行上面的示例,将会打印出 “FORMAL: Hello, Jake”,同样由于只被一个位置引用,R8 会将 printGreeting 内联到 main 方法中。
[000474] Greeting.main:([Ljava/lang/String;)V0000: sget-object v3, LGreeting;.FORMAL:LGreeting;0002: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream;0004: new-instance v1, Ljava/lang/StringBuilder;0006: invoke-direct {v1}, Ljava/lang/StringBuilder;.<init>:()V0009: invoke-virtual {v3}, LGreeting;.name:()Ljava/lang/String;000c: move-result-object v2 ⋮0022: invoke-virtual {v1, v2}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V0025: return-void
复制代码
字节码索引 0000 处查找 FORMAL 枚举常量,然后 0009-000c 调用其 name() 方法。就像 ordinal() 一样,这是一个冗余的操作,因为常量的名称在编译时是已知的。
R8 同样会检测枚举常量调用流中使用 name() 的位置,然后替换为字符串常量。如果你读过 economics of generated code 这篇文章,你就能明白创建一个字符串常量的代价,但是谢天谢地,枚举常量可以跟字符串常量共用,这样就不用创建了。
[000474] Greeting.main:([Ljava/lang/String;)V 0000: sget-object v3, LGreeting;.FORMAL:LGreeting; 0002: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream; 0004: new-instance v1, Ljava/lang/StringBuilder; 0006: invoke-direct {v1}, Ljava/lang/StringBuilder;.<init>:()V-0009: invoke-virtual {v3}, LGreeting;.name:()Ljava/lang/String;-000c: move-result-object v2+0009: const-string v2, "FORMAL" ⋮ 0020: invoke-virtual {v1, v2}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V 0023: return-void
复制代码
字节码索引 0000 处的查找仍然会发生,因为代码需要调用 greet 方法,但是取消了对 name() 的调用。
这种优化将无法应用其他大型优化,如分支消除。但是,由于它生成一个字符串,因此对 name() 调用的结果执行的任何字符串操作也可以在编译时执行。
对于没有重写 toString() 的枚举,此优化还将应用于对 toString() 的调用,该调用默认与 name() 相同。
这两种枚举优化都很小,实际上只在其他 R8 优化的场景中有作用。不过,如果到目前为止在本系列中还不清楚的话,那么这就是大多数优化表现的方式。
到目前为止,在这个系列中,我选择了强调优化,因为我发现了其中的错误,有时甚至通过 R8 问题跟踪程序自己提出了建议。但是这篇文章中的两个优化有点特别,因为我自己也做到了!我想我们不会在这个系列中看到我的其他贡献,但至少扮演了一个小角色感觉很好。
在下一篇文章中,我们将回到 enum 顺序优化,因为 enum 上的 switch 语句远比看上去复杂。敬请期待!
评论