This is the 25th day of my participation in Gwen Challenge
This article is part 12 of jakewharton’s series on D8 and R8.
- R8 Optimization: Enum Ordinals and Names
- Originally written by jakewharton
- Translator: Antway
Enumerations are (and always are!) One recommended way to create constants. In general, enumerations provide only one set of possible constants and no others. But as full classes, enumerations can also carry helper methods and fields (instance and static) or implementation interfaces.
The optimization for enumerations is usually to process them where they appear. A common optimization is to replace simple references with integer values (that is, for enumerations that have no fields, methods, or interfaces). However, there are other optimizations that apply to all enumerations that are still available.
1. The Ordinal method
Each enumerated constant has an ordinal() method that returns its position in the list of all constants, ranging from [0, N], which can be used to index to other zero-based data structures, such as arrays or even bits. The most common use is the switch statement in the Java compiler.
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 newAssertionError(); }}}Copy the code
Viewing the compiled bytecode shows a hidden call to ordinal().
[000a34] Greeting.type:(LGreeting;) Ljava/lang/String;0000: invoke-virtual {v1}, LGreeting; .ordinal:()I0003: move - the result of v1 ⋮Copy the code
If we call this method with one of the constants, there is an opportunity for optimization.
public static void main(String... args) {
System.out.println(Greeting.type(Greeting.INFORMAL));
}
Copy the code
Since this is the only reference to a type in the entire application, R8 inlines the methods.
[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:()I0007: move - result where v0 ⋮0047: invoke-virtual {v1, v2}, Ljava/io/PrintStream; .println:(Ljava/lang/String;) V0050: return-void
Copy the code
The bytecode index 0002 looks for the INFORMAL enumeration constant, and then calls its oridinal() method 0004-0007. This is a redundant operation because the ordinal of the constant is known at compile time.
R8 detects the call to ordinal() in the constant reference stream and calls the correct integer value replacement that will result.
[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;) V0045: return-void
Copy the code
This constant value now flows into the Switch statement, leaving only the required branches to eliminate it.
[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;) V0007: return-void
Copy the code
Although the language provides toggling of enumerations, its implementation is based on integers in sequential values. Replacing a call to ordinal() over a fixed constant is a simple optimization, but it allows more advanced optimizations (such as branch elimination) to be applied where they would not otherwise be applied.
2. The method Names
In addition to ordinal(), each enumerated constant also exposes its declared name through the name() method. By default, toString() will also return the name of the declaration, but since this method can be overridden, it must have a different name, name().
enum Greeting {
FORMAL { / *... * / },
INFORMAL { / *... * / };
abstract String greet(String name);
@Override public String toString(a) {
return "Greeting(" + name().toLowercase(US) + ') '; }}Copy the code
The value of name() is typically used for display, logging, or 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");
}
Copy the code
Running the example above will print “FORMAL: Hello, Jake”, and R8 will inline the printGreeting into the main method because it is referenced in only one place.
[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; 000 c: move - result - object v2 ⋮0022: invoke-virtual {v1, v2}, Ljava/io/PrintStream; .println:(Ljava/lang/String;) V0025: return-void
Copy the code
The bytecode index at 0000 looks for the FORMAL enumeration constant, and 0009-000C calls its name() method. Like ordinal(), this is a redundant operation because the name of the constant is known at compile time.
R8 also detects where name() is used in the enumeration constant call stream and replaces it with a string constant. If you’ve read The economics of Generated Code article, you know the cost of creating a string constant, but thankfully, enumeration constants can be shared with string constants so you don’t have to.
[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;) V0023: return-void
Copy the code
The lookup at the bytecode index 0000 still occurs because the code needs to call the greet method, but the call to name() is cancelled.
This optimization will not apply to other large optimizations, such as branch elimination. However, because it generates a string, any string operations performed on the result of the name() call can also be performed at compile time.
For enumerations that do not override toString(), this optimization also applies to calls to toString(), which by default are the same as name().
Both of these enumeration optimizations are small and really only useful in other R8-optimized scenarios. However, if it hasn’t been clear from this series so far, this is how most optimizations are made.
So far in this series, I’ve chosen to emphasize optimization because I’ve found bugs in it, and sometimes even made suggestions myself through the R8 problem tracker. But the two optimizations in this article are a bit special because I’ve done them myself! I don’t think we’ll see any other contributions from me in the series, but it’s nice to have at least a small role.
In the next article, we’ll return to enum order optimization, because switch statements on enUms are far more complex than they seem. Stay tuned!