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

这是我参与更文挑战的第 25 天,活动详情查看: 更文挑战

原文出自 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:()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 10042: 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;)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 语句远比看上去复杂。敬请期待!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享