This is the 18th day of my participation in Gwen Challenge

This article is part 5 of jakewharton’s series on D8 and R8.

  • R8 Optimization: Null Data Flow Analysis (Part 1)
  • Originally written by jakewharton
  • Translator: Antway

This article will introduce the optimization of R8 for Null Data. Let’s get started!

1. R8

fun <T : Any> coalesce(a: T? , b:T?).: T? = a ? : bfun main(vararg args: String) {
 println(coalesce("one"."two"))
 println(coalesce(null."two"))}Copy the code

In the preceding example, the coalesce function returns the coalesce parameter based on whether a is null. If a is not null, a is returned; otherwise, B is returned. The output above is as follows:

one
two
Copy the code

At compile time, if the body of a function is short, R8 and ProGuard will put the body of the function into the calling function at the point of the call. Because Coalesce is short, its functions are inline nested wherever it is called.

fun main(vararg args: String) {
  println("one"? :"two")
  println(null? :"two")}Copy the code

In fact, the Kotlin editor can verify at compile time? We compile the above code and look at the compiled and packaged bytecode to verify.

[000180] NullsKt.main:([Ljava/lang/String;)V
0000: sget-object v1, Ljava/lang/System; .out:Ljava/io/PrintStream;0002: const-string v0, "one"
0004: invoke-virtual {v1, v0}, Ljava/io/PrintStream; .println:(Ljava/lang/Object;) V0007: sget-object v1, Ljava/lang/System; .out:Ljava/io/PrintStream; 0009:const-string v0, "two"000b: invoke-virtual {v1, v0}, Ljava/io/PrintStream; .println:(Ljava/lang/Object;) V 000e:return-void
Copy the code

As expected, there is no conditional judgment in the bytecode; instead, the println method is called directly to print one and two. However, because optimization occurs within R8, not before the Kotlin compiler, the actual Dalvik bytecode contains conditions.

[000144] NullsKt.main:([Ljava/lang/String;)V
0000: sget-object v1, Ljava/lang/System; .out:Ljava/io/PrintStream;0002: const-string v0, "one"
0004: if-nez v0, 0006
0006: const-string v0, "two"0008: invoke-virtual {v1, v0}, Ljava/io/PrintStream; .println:(Ljava/lang/Object;) V 000b: sget-object v1, Ljava/lang/System; .out:Ljava/io/PrintStream;000d: const/4 v0, #int 0
000f: if-nez v0, 0010
0010: const-string v0, "two"
0012: invoke-virtual {v1, v0}, Ljava/io/PrintStream; .println:(Ljava/lang/Object;) V0015: return-void
Copy the code

Load constant one at 0002, then 0004 to determine whether it is empty or not, so it is not empty all the time, which results in dead code at 0006, unable to load two. The same goes for loading 0 at 000d (for null) and then doing a non-null check at 000F, which will always fail because it will always be empty, and then directly executing code at 0010.

In the last article, we talked about how R8 optimizes code at the IR level, and IR uses SSA to enforce some optimizations. With SSA, R8 can determine the flow of data in the program. The inline data processing flow for the first println of the above code can be briefly described as follows.

The basic property of SSA is that each variable is assigned only once. This is why the string two is assigned to y and not to x. Z by [Φ (euler function)] (https://baike.baidu.com/item/%E6%AC%A7%E6%8B%89%E5%87%BD%E6%95%B0) to select the value of x or y. Combined with the previous bytecode, you can see that x, y, and z all end up assigned to register address v0, which is overwritten. Note that the single assignment is only for the IR layer!

If we add some empty information to the above flowchart, since both x and y are initialized with constants, they must not be empty. And then z is not empty,

When x is not null, R8 knows that if-nez bytecode checks that x is not null are always true, so this judgment is useless. Similarly, assignment to y is useless.

From the above analysis, we know that bytecode judged by false branch is useless dead-code, so we optimize the branch and delete the useless judgment branch.

We can see that the value of z is the result of phi operating on a single variable x, so we can replace z with x.

As you can see from the diagram, the rest are: a w variable pointing to System.out, assigning one to x, and w calling println to print x.

The introduction above is for the first println function in the sample code. The second println function is initialized to null, so the process is reversed. X is initialized to null, and non-null checks are always false, so y is always two.

By using SSA IR, R8 can be optimized for conditions to remove useless code.

$ kotlinc *.kt

$ cat rules.txt
-keepclasseswithmembers class * { 
  public static void main(java.lang.String[]); 
} 
-dontobfuscate

$ java -jar r8.jar \
    --lib $ANDROID_HOME28 / android/platforms/android - jar \ - release \ -- the output. \ - pg - conf rules. TXT \ * class kotlin - stdlib - 1.3.11. Jar $$ANDROID_HOME/build-tools/28.0.3/dexdump -d classes.dex [000340] nullskt. main:([Ljava/lang/String;)V 0000: sget-object v1, Ljava/lang/System;.out:Ljava/io/PrintStream; 0002: const-string v0,"one"0004: invoke-virtual {v1, v0}, Ljava/io/PrintStream; .println:(Ljava/lang/Object;) V 0007: sget-object v1, Ljava/lang/System; .out:Ljava/io/PrintStream; 0009: const-string v0,"two"000b: invoke-virtual {v1, v0}, Ljava/io/PrintStream; .println:(Ljava/lang/Object;) V 000e: return-voidCopy the code

As you can see from the bytecode above, this is exactly the same as our initial sample analysis.

2. Analysis Inside D8

For the above example, I try to use Java code implementation to analyze it.

class Nulls {
  public static void main(String... args) {
    Object first = "one";
    if (first == null) {
      first = "two";
    }
    System.out.println(first);
    Object second = null;
    if (second == null) {
      second = "two"; } System.out.println(second); }}Copy the code

We compiled the above example, then packaged it with D8, and finally looked at the dex bytecode with Dumpdex, and found that the judgment condition was still removed.

$ javac *.java

$ java -jar d8.jar \
    --lib $ANDROID_HOME/platforms/android-28/android.jar \
    --release \
    --output . \
    *.class

$ $ANDROID_HOME/build-tools/28.03./dexdump -d classes.dex
[000224] Nulls.main:([Ljava/lang/String;)V
0000: sget-object v1, Ljava/lang/System; .out:Ljava/io/PrintStream;0002: const-string v0, "one"
0004: invoke-virtual {v1, v0}, Ljava/io/PrintStream; .println:(Ljava/lang/Object;) V0007: sget-object v1, Ljava/lang/System; .out:Ljava/io/PrintStream; 0009:const-string v0, "two"000b: invoke-virtual {v1, v0}, Ljava/io/PrintStream; .println:(Ljava/lang/Object;) V 000e:return-void
Copy the code

This happens because D8 is also optimized using IR, and there is still empty information. Even if no R8 optimizations are made, D8 will also clean the relevant code optimizations if the judgment condition in the IR is always true or false.

If we compiled with the dx tool without IR, we would still find conditional judgments and useless code in the bytecode.

$ $ANDROID_HOME/build-tools/28.03./dx --dex --output=classes.dex *.class

$ $ANDROID_HOME/build-tools/28.03./dexdump -d classes.dex
[000204] Nulls.main:([Ljava/lang/String;)V
0000: const-string v0, "one"
0002: if-nez v0, 0006
0004: const-string v0, "two"
0006: sget-object v1, Ljava/lang/System; .out:Ljava/io/PrintStream; 0008: invoke-virtual {v1, v0}, Ljava/io/PrintStream; .println:(Ljava/lang/Object;) V 000b:const/4 v0, #int 0
000c: if-nez v0, 0010
000e: const-string v0, "two"
0010: sget-object v1, Ljava/lang/System; .out:Ljava/io/PrintStream;0012: invoke-virtual {v1, v0}, Ljava/io/PrintStream; .println:(Ljava/lang/Object;) V0015: return-void
Copy the code

Therefore, R8 does have a good job of optimizing inline nesting, but if constant conditions and dead code exist in the source code, D8 will also eliminate them at compile time.

3. Summary

This article has only scratched the surface of R8’s internal data flow analysis. The next article will continue to expand the analysis of nullity by discussing how Kotlin enforces nullity constraints at run time.