写点什么

[译] R8 优化: 枚举的 Ordinals 和 Names

用户头像
Antway
关注
发布于: 1 小时前

原文出自 jakewharton 关于 D8R8 系列文章第 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 v00047: 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() 的值通常用于 displayloggingserialization


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 v20022: 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 语句远比看上去复杂。敬请期待!

用户头像

Antway

关注

持续精进,尽管很慢 2019.05.27 加入

专注开源库

评论

发布
暂无评论
[译] R8 优化: 枚举的 Ordinals 和 Names