原文出自 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:()I
0003: move-result v1
⋮
复制代码
如果我们用其中一个常量调用这个方法,就会出现一个优化的机会。
public static void main(String... args) {
System.out.println(Greeting.type(Greeting.INFORMAL));
}
复制代码
由于这是整个应用程序中类型的唯一引用处,所以 R8
将方法内联起来。
[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
⋮
0047: invoke-virtual {v1, v2}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V
0050: 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;)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
⋮
0022: invoke-virtual {v1, v2}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V
0025: 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
语句远比看上去复杂。敬请期待!
评论