This is the 19th day of my participation in the Genwen Challenge

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

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

In the previous article, we demonstrated that R8 optimised the null judgment code for inline methods, due to the fact that R8 (and D8) implemented null judgments during the IR era. When the argument passed to the method is null or non-null constant, R8 evaluates the null judgment directly at compile time.

The examples in the previous two articles were mostly written in Kotlin, and I sometimes pasted only parts of the bytecode to make it more readable. In the previous article, we started by calling the coalesce function from the main function.

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

In the previous article, we use different versions of the compiler to compile the simulation optimization example, after their bytecode are sget – object v1, Ljava/lang/System and out: Ljava/IO/PrintStream; At the beginning, this bytecode means looking for the bytecode of the static System.out field that eventually calls the println method.

If we compile, package the demo above, and then look at the packed bytecode, we can see that the first line of bytecode is a little different.

$ kotlinc *.kt

$ java -jar d8.jar \
    --lib $ANDROID_HOME28 / android/platforms/android - jar \ - release \ - the output \ * class kotlin - stdlib - 1.3.11. Jar $$ANDROID_HOMEDex [00023c] nullskt. main:([Ljava/lang/String;)V 0000: const-string v0,"args"0002: invoke-static {v2, v0}, Lkotlin/jvm/internal/Intrinsics; .checkParameterIsNotNull:(Ljava/lang/Object; Ljava/lang/String;) V 0005: sget-object v1, Ljava/lang/System; .out:Ljava/io/PrintStream;Copy the code

Bytecode is not to replace we write functions to the body, but the first call Intrinstrics. CheckParameterIsNotNull function, the function is the back of the runtime validation at compile time.

Kotlin’s type system models empty references. By taking Array

as an argument to main, that is, it is non-null. But it’s a public API, so anyone can call it. For null constraints, the Kotlin compiler inserts a defense check for non-null arguments in each public API function.

Let’s compile the above source code using D8 to see what changes have been made.

$ 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_HOMEDex [000314] nullskt. main:([Ljava/lang/String;)V 0000: if-eqz v1, 0011 0002: Sget - object v1, Ljava/lang/System;. Out: Ljava/IO/PrintStream;... 0010: return void - 0011: const - string v1,"args"0013: invoke-static {v1}, Lkotlin/jvm/internal/Intrinsics; .throwParameterIsNullException:(Ljava/lang/String;) VCopy the code

In the above d8-compiled bytecode we can see that the loading of string constants and the invocation of Intrinsics methods have been replaced by the standard null check if-eqz. If the null check is true, it jumps to the last exception thrown by the bytecode. Under normal conditions, args is non-empty, and the program will normally execute from 0000 to 0010.

We might be thinking “because it’s an inline function, the bytecode above looks very similar to what R8 handles”. In the previous article, coalesce function is inline. So just can have Instrinsics checkParameterIsNotNull bytecode implementation, we can quickly see Instrinsics. CheckParameterIsNotNull implementation.

public static void checkParameterIsNotNull(Object value, String paramName) {
  if (value == null) { throwParameterIsNullException(paramName); }}Copy the code

The actual R8 is not handled inline as we would expect; if it were, the above if would definitely appear at the top of the function. In addition, although the above method is small, it is outside the threshold range for defining inline in R8. There are several ways to do this.

The first technique is to increase the threshold range for R8 inline. Because checkParameterNotNull is only used for non-empty checking of call parameters, the method’s inline threshold increases and the method body is empty, so it becomes eligible and inline.

The second trick is that R8 recognizes the two bytecodes that perform null checks on the parameters and then throws an exception. When this pattern is recognized, R8 assumes that it is an unusual path for method execution. To optimize the common path, the null check is reversed so that non-null case follows. The exception throwing code is pushed to the bottom of the method.

However, the if check of checkParameterNotNull does not match the sequence of bytecode R8, and the parameter check pattern needs to be identified. The if body contains static method calls rather than throwing exceptions. The last skill is the R8 has a built-in, it identified the intrinsics. ThrowParameterIsNullException ` call equivalent to throw an exception. This makes the subject match the pattern correctly.

These three techniques combine to explain why R8 produces the bytecode we saw above.

Remember that a non-Kotlin caller might have this code for every non-empty argument for every visible method. In complex programs, there will be a lot of such situations!

By replacing static method calls with standard null checks with R8 and moving exceptions to the end of the method, the code preserves the security of the checks while minimizing the performance impact.

1. Combining Null Information

In the previous article, R8 used null information to remove unwanted null checks. In the first half of this article, we describe how R8 ignores null checking by raising the inline threshold and uses the standard IF-Eqz instruction to perform null checking instead of the Intrinsic method provided by Kotlin. It seems that these two features can be combined in some ways.

In the following example, we have added the string. double function,

fun String.double(a): String = this + this

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

Before compiling D8, let’s list the following non-null checks:

  1. argsParameter check becausemainMethod ispublic;
  2. coalesceFunction return value check as will be calleddoubleFunctions;
  3. coalesceA non-null check for the first argument of the function, since the decision is to returnfirstsecond;
  4. doubleThe receiver of the function is checked as it is apublicMethods.

Let’s verify this by D8 compilation.

[000310] NullsKt.main:([Ljava/lang/String;)V
0000: if-eqz v1, 0019
0002: const-string v1, "two"0004: new-instance v0, Ljava/lang/StringBuilder; 0006: invoke-direct {v0}, Ljava/lang/StringBuilder; .<init>:()V 0009: invoke-virtual {v0, v1}, Ljava/lang/StringBuilder; .append:(Ljava/lang/String;) Ljava/lang/StringBuilder; 000c: invoke-virtual {v0, v1}, Ljava/lang/StringBuilder; .append:(Ljava/lang/String;) Ljava/lang/StringBuilder; 000f: invoke-virtual {v0}, Ljava/lang/StringBuilder; .toString:()Ljava/lang/String; 0012: move-result-object v1 0013: sget-object v0, Ljava/lang/System; .out:Ljava/io/PrintStream; 0015: invoke-virtual {v0, v1}, Ljava/io/PrintStream; .println:(Ljava/lang/Object;) V 0018: return-void 0019: const-string v1,"args"001b: invoke-static {v1}, Lkotlin/jvm/internal/Intrinsics; .throwParameterIsNullException:(Ljava/lang/String;) VCopy the code

All null checks are cleared except for protection parameter checks.

Because R8 can prove that coalesce returns a non-null reference, all downstream null-checking can be eliminated. This means that the security call is not required, but is replaced with a normal method call. Empty checks on dual – function receivers have also been eliminated.

2. No Inlining Required

Examples so far include embedding to help reduce production, but inlining doesn’t actually happen as it does in these small examples. This does not prevent all null checks from being cleared.

While I find Kotlin’s example compelling here because of the mandatory, zero-check, Java optimization is interesting because it behaves the other way around. Java does not set defensive null-checking on public method parameters, so data flow analysis can use other modes for null-value signals, even without inline.

final class Nulls {
  public static void main(String[] args) {
    System.out.println(first(args));
    if (args == null) {
      System.out.println("null!"); }}public static String first(String[] values) {
    if (values == null) throw new NullPointerException("values == null");
    return values[0]; }}Copy the code

In Java, every reference can be empty. Therefore, it is not uncommon to see active null-checking in methods like first (even when annotating @nonNULL). Library methods can be large or invoked all over the application, so they are usually not inline. To simulate this, we can explicitly tell R8 to be kept as a method in rules.txt.

 -keepclasseswithmembers class * {
   public static void main(java.lang.String[]);
 }
 -dontobfuscate
+-keep class Nulls {
+   public static java.lang.String first(java.lang.String[]);
+}
Copy the code

We see that the actual results are acceptable even without inline optimization.

[000144] Nulls.first:([Ljava/lang/String;)Ljava/lang/String;
0000: if-eqz v1, 0006
0002: const/4 v0, #int 0
0003: aget-object v1, v1, v0
0005: return-object v1
0006: new-instance v1, Ljava/lang/NullPointerException;
0008: const-string v0, "values == null"000a: invoke-direct {v1, v0}, Ljava/lang/NullPointerException; .<init>:(Ljava/lang/String;) V 000d: throw v1 [000170] Nulls.main:([Ljava/lang/String;)V 0000: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream; 0002: invoke-static {v1}, LNulls;.first:([Ljava/lang/String;)Ljava/lang/String; 0005: move-result-object v1 0006: invoke-virtual {v0, v1}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V 0009: return-voidCopy the code

First, R8 reverses the null check again, so that the exception that raises the exception is at the bottom of the method at index 0006. Normal execution of this method will return from index 0000 to 0005.

In short, arGS and its printed explicit null-checking have disappeared. This is because R8 detects that args is an entry, where it cannot be null after a call. Therefore, any null checks that occur after the call to FIRST need not occur.

3. Summary

All of these examples are small and somewhat artificial, but they demonstrate part of the data flow analysis that R8 does in terms of nullability and null-checking. Across the entire application, whether Java, Kotlin, or hybrid, unnecessary null validations and unused branches can be eliminated without sacrificing the security they provide.

Next week’s R8 article will cover my favorite tool features. It was also one of the demos that I think produced the best and resonated with every Android developer. Stay tuned!