Re-enable "Eliminate redundant field loads."

This reverts commit 30ddb75955a9fbeb90c60c4c442857a711f4cf64.

Besides re-enabling the original change this ensures that volatile
loads clears all cached values.

Bug: 111250398
Change-Id: If986845f4cc349785b2b9f0f738fc3b3a9c1150a
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index 8ab2c1a..5bed061 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -6,7 +6,6 @@
 import static com.android.tools.r8.ir.code.IRCode.INSTRUCTION_NUMBER_DELTA;
 
 import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DebugLocalInfo.PrintLevel;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -468,7 +467,7 @@
         return instruction;
       }
     }
-    throw new Unreachable();
+    return null;
   }
 
   public void clearUserInfo() {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index c6ba97d..555ddf0 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -46,6 +46,7 @@
 import com.android.tools.r8.ir.optimize.NonNullTracker;
 import com.android.tools.r8.ir.optimize.Outliner;
 import com.android.tools.r8.ir.optimize.PeepholeOptimizer;
+import com.android.tools.r8.ir.optimize.RedundantFieldLoadElimination;
 import com.android.tools.r8.ir.optimize.classinliner.ClassInliner;
 import com.android.tools.r8.ir.optimize.lambda.LambdaMerger;
 import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
@@ -104,6 +105,8 @@
   private final Devirtualizer devirtualizer;
   private final CovariantReturnTypeAnnotationTransformer covariantReturnTypeAnnotationTransformer;
 
+  private final boolean enableWholeProgramOptimizations;
+
   private final OptimizationFeedback ignoreOptimizationFeedback = new OptimizationFeedbackIgnore();
   private DexString highestSortingString;
 
@@ -134,6 +137,7 @@
         options.processCovariantReturnTypeAnnotations
             ? new CovariantReturnTypeAnnotationTransformer(this, appInfo.dexItemFactory)
             : null;
+    this.enableWholeProgramOptimizations = enableWholeProgramOptimizations;
     if (enableWholeProgramOptimizations) {
       assert appInfo.hasLiveness();
       this.nonNullTracker = new NonNullTracker();
@@ -695,6 +699,7 @@
     codeRewriter.rewriteSwitch(code);
     codeRewriter.processMethodsNeverReturningNormally(code);
     codeRewriter.simplifyIf(code, typeEnvironment);
+    new RedundantFieldLoadElimination(appInfo, code, enableWholeProgramOptimizations).run();
 
     if (options.testing.invertConditionals) {
       invertConditionalsForTesting(code);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java
new file mode 100644
index 0000000..dee935e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java
@@ -0,0 +1,222 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize;
+
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.DominatorTree;
+import com.android.tools.r8.ir.code.FieldInstruction;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.Value;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Eliminate redundant field loads.
+ *
+ * <p>Simple algorithm that goes through all blocks in one pass in dominator order and propagates
+ * active field sets across control-flow edges where the target has only one predecessor.
+ */
+// TODO(ager): Evaluate speed/size for computing active field sets in a fixed-point computation.
+public class RedundantFieldLoadElimination {
+
+  private final AppInfo appInfo;
+  private final DexEncodedMethod method;
+  private final IRCode code;
+  private final boolean enableWholeProgramOptimizations;
+  private final DominatorTree dominatorTree;
+
+  // Maps keeping track of fields that have an already loaded value at basic block entry.
+  private final HashMap<BasicBlock, HashMap<FieldAndObject, Instruction>>
+      activeInstanceFieldsAtEntry = new HashMap<>();
+  private final HashMap<BasicBlock, HashMap<DexField, Instruction>> activeStaticFieldsAtEntry =
+      new HashMap<>();
+
+  // Maps keeping track of fields with already loaded values for the current block during
+  // elimination.
+  private HashMap<FieldAndObject, Instruction> activeInstanceFields;
+  private HashMap<DexField, Instruction> activeStaticFields;
+
+  public RedundantFieldLoadElimination(
+      AppInfo appInfo, IRCode code, boolean enableWholeProgramOptimizations) {
+    this.appInfo = appInfo;
+    this.method = code.method;
+    this.code = code;
+    this.enableWholeProgramOptimizations = enableWholeProgramOptimizations;
+    dominatorTree = new DominatorTree(code);
+  }
+
+  private static class FieldAndObject {
+    private final DexField field;
+    private final Value object;
+
+    private FieldAndObject(DexField field, Value receiver) {
+      this.field = field;
+      this.object = receiver;
+    }
+
+    @Override
+    public int hashCode() {
+      return field.hashCode() * 7 + object.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (!(other instanceof FieldAndObject)) {
+        return false;
+      }
+      FieldAndObject o = (FieldAndObject) other;
+      return o.object == object && o.field == field;
+    }
+  }
+
+  private boolean couldBeVolatile(DexField field) {
+    if (!enableWholeProgramOptimizations && field.getHolder() != method.method.getHolder()) {
+      return true;
+    }
+    DexEncodedField definition = appInfo.definitionFor(field);
+    return definition == null || definition.accessFlags.isVolatile();
+  }
+
+  public void run() {
+    for (BasicBlock block : dominatorTree.getSortedBlocks()) {
+      activeInstanceFields =
+          activeInstanceFieldsAtEntry.containsKey(block)
+              ? activeInstanceFieldsAtEntry.get(block)
+              : new HashMap<>();
+      activeStaticFields =
+          activeStaticFieldsAtEntry.containsKey(block)
+              ? activeStaticFieldsAtEntry.get(block)
+              : new HashMap<>();
+      InstructionListIterator it = block.listIterator();
+      while (it.hasNext()) {
+        Instruction instruction = it.next();
+        if (instruction.isFieldInstruction()) {
+          DexField field = instruction.asFieldInstruction().getField();
+          if (instruction.isInstancePut() || instruction.isStaticPut()) {
+            killActiveFields(instruction.asFieldInstruction());
+          } else if (couldBeVolatile(field)) {
+            assert instruction.isInstanceGet() || instruction.isStaticGet();
+            killAllActiveFields();
+          } else {
+            assert instruction.isInstanceGet() || instruction.isStaticGet();
+            assert !couldBeVolatile(field);
+            if (instruction.isInstanceGet() && !instruction.outValue().hasLocalInfo()) {
+              Value object = instruction.asInstanceGet().object();
+              FieldAndObject fieldAndObject = new FieldAndObject(field, object);
+              if (activeInstanceFields.containsKey(fieldAndObject)) {
+                Instruction active = activeInstanceFields.get(fieldAndObject);
+                eliminateRedundantRead(it, instruction, active);
+              } else {
+                activeInstanceFields.put(fieldAndObject, instruction);
+              }
+            } else if (instruction.isStaticGet() && !instruction.outValue().hasLocalInfo()) {
+              if (activeStaticFields.containsKey(field)) {
+                Instruction active = activeStaticFields.get(field);
+                eliminateRedundantRead(it, instruction, active);
+              } else {
+                // A field get on a different class can cause <clinit> to run and change static
+                // field values.
+                killActiveFields(instruction.asFieldInstruction());
+                activeStaticFields.put(field, instruction);
+              }
+            }
+          }
+        }
+        if (instruction.isMonitor() || instruction.isInvokeMethod()) {
+          killAllActiveFields();
+        }
+      }
+      propagateActiveFieldsFrom(block);
+    }
+    assert code.isConsistentSSA();
+  }
+
+  private void propagateActiveFieldsFrom(BasicBlock block) {
+    for (BasicBlock successor : block.getSuccessors()) {
+      // Allow propagation across exceptional edges, just be careful not to propagate if the
+      // throwing instruction is a field instruction.
+      if (successor.getPredecessors().size() == 1) {
+        if (block.hasCatchSuccessor(successor)) {
+          Instruction exceptionalExit = block.exceptionalExit();
+          if (exceptionalExit != null && exceptionalExit.isFieldInstruction()) {
+            killActiveFieldsForExceptionalExit(exceptionalExit.asFieldInstruction());
+          }
+        }
+        assert !activeInstanceFieldsAtEntry.containsKey(successor);
+        activeInstanceFieldsAtEntry.put(successor, new HashMap<>(activeInstanceFields));
+        assert !activeStaticFieldsAtEntry.containsKey(successor);
+        activeStaticFieldsAtEntry.put(successor, new HashMap<>(activeStaticFields));
+      }
+    }
+  }
+
+  private void killAllActiveFields() {
+    activeInstanceFields.clear();
+    activeStaticFields.clear();
+  }
+
+  private void killActiveFields(FieldInstruction instruction) {
+    DexField field = instruction.getField();
+    if (instruction.isInstancePut()) {
+      // Remove all the field/object pairs that refer to this field to make sure
+      // that we are conservative.
+      List<FieldAndObject> keysToRemove = new ArrayList<>();
+      for (FieldAndObject key : activeInstanceFields.keySet()) {
+        if (key.field == field) {
+          keysToRemove.add(key);
+        }
+      }
+      keysToRemove.forEach((k) -> activeInstanceFields.remove(k));
+    } else if (instruction.isInstanceGet()) {
+      Value object = instruction.asInstanceGet().object();
+      FieldAndObject fieldAndObject = new FieldAndObject(field, object);
+      activeInstanceFields.remove(fieldAndObject);
+    } else if (instruction.isStaticPut()) {
+      if (field.clazz != code.method.method.holder) {
+        // Accessing a static field on a different object could cause <clinit> to run which
+        // could modify any static field on any other object.
+        activeStaticFields.clear();
+      } else {
+        activeStaticFields.remove(field);
+      }
+    } else if (instruction.isStaticGet()) {
+      if (field.clazz != code.method.method.holder) {
+        // Accessing a static field on a different object could cause <clinit> to run which
+        // could modify any static field on any other object.
+        activeStaticFields.clear();
+      }
+    }
+  }
+
+  // If a field get instruction throws an exception it did not have an effect on the
+  // value of the field. Therefore, when propagating across exceptional edges for a
+  // field get instruction we have to exclude that field from the set of known
+  // field values.
+  private void killActiveFieldsForExceptionalExit(FieldInstruction instruction) {
+    DexField field = instruction.getField();
+    if (instruction.isInstanceGet()) {
+      Value object = instruction.asInstanceGet().object();
+      FieldAndObject fieldAndObject = new FieldAndObject(field, object);
+      activeInstanceFields.remove(fieldAndObject);
+    } else if (instruction.isStaticGet()) {
+      activeStaticFields.remove(field);
+    }
+  }
+
+  private void eliminateRedundantRead(
+      InstructionListIterator it, Instruction redundant, Instruction active) {
+    redundant.outValue().replaceUsers(active.outValue());
+    it.removeOrReplaceByDebugLocalRead();
+    active.outValue().uniquePhiUsers().forEach(Phi::removeTrivialPhi);
+  }
+}
diff --git a/src/test/examples/uninitializedfinal/UninitializedFinalFieldLeak.java b/src/test/examples/uninitializedfinal/UninitializedFinalFieldLeak.java
new file mode 100644
index 0000000..781d2b4
--- /dev/null
+++ b/src/test/examples/uninitializedfinal/UninitializedFinalFieldLeak.java
@@ -0,0 +1,57 @@
+// Copyright (c) 2018 the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package uninitializedfinal;
+
+// Test that leaks an instance before its final field has been initialized to a thread that
+// reads that field. This tests that redundant field load elimination does not eliminate
+// field reads (even of final fields) that cross a monitor operation.
+public class UninitializedFinalFieldLeak {
+
+  public static class PollingThread extends Thread {
+    public int result = 0;
+    UninitializedFinalFieldLeak f;
+
+    PollingThread(UninitializedFinalFieldLeak f) {
+      this.f = f;
+    }
+
+    // Read the field a number of times. Then lock on the object to await field initialization.
+    public void run() {
+      result += f.i;
+      result += f.i;
+      result += f.i;
+      f.threadReadsDone = true;
+      synchronized (f) {
+        result += f.i;
+      }
+      // The right result is 42. Reading the uninitialized 0 three times and then
+      // reading the initialized value. It is safe to remove the two redundant loads
+      // before the monitor operation.
+      System.out.println(result);
+    }
+  }
+
+  public final int i;
+  public volatile boolean threadReadsDone = false;
+
+  public UninitializedFinalFieldLeak() throws InterruptedException {
+    // Leak the object to a thread and start the thread with the lock on the object taken.
+    // Then allow the other thread to run and read the uninitialized field.
+    // Finally, initialize the field and release the lock.
+    PollingThread t = new PollingThread(this);
+    synchronized (this) {
+      t.start();
+      while (!threadReadsDone) {
+        Thread.yield();
+      }
+      i = 42;
+    }
+    t.join();
+  }
+
+  public static void main(String[] args) throws InterruptedException {
+    new UninitializedFinalFieldLeak();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
index 36f7594..221d427 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesTest.java
@@ -45,9 +45,11 @@
         "instancevariable.InstanceVariable",
         "instanceofstring.InstanceofString",
         "invoke.Invoke",
+        "invokeempty.InvokeEmpty",
         "jumbostring.JumboString",
         "loadconst.LoadConst",
         "loop.UdpServer",
+        "nestedtrycatches.NestedTryCatches",
         "newarray.NewArray",
         "regalloc.RegAlloc",
         "returns.Returns",
@@ -58,9 +60,7 @@
         "throwing.Throwing",
         "trivial.Trivial",
         "trycatch.TryCatch",
-        "nestedtrycatches.NestedTryCatches",
         "trycatchmany.TryCatchMany",
-        "invokeempty.InvokeEmpty",
         "regress.Regress",
         "regress2.Regress2",
         "regress_37726195.Regress",
@@ -82,6 +82,7 @@
         "enclosingmethod_proguarded.Main",
         "interfaceinlining.Main",
         "switchmaps.Switches",
+        "uninitializedfinal.UninitializedFinalFieldLeak",
     };
 
     List<String[]> fullTestList = new ArrayList<>(tests.length * 2);
diff --git a/src/test/java/com/android/tools/r8/regress/b111250398/B111250398.java b/src/test/java/com/android/tools/r8/regress/b111250398/B111250398.java
new file mode 100644
index 0000000..90fba5f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/regress/b111250398/B111250398.java
@@ -0,0 +1,342 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.regress.b111250398;
+
+import static com.android.tools.r8.utils.DexInspectorMatchers.isPresent;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.code.Iget;
+import com.android.tools.r8.code.IgetObject;
+import com.android.tools.r8.code.Sget;
+import com.android.tools.r8.graph.DexCode;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.android.tools.r8.utils.DexInspector.FieldSubject;
+import com.android.tools.r8.utils.DexInspector.MethodSubject;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.ImmutableList;
+import java.util.Arrays;
+import org.junit.Test;
+
+// Copy of javax.inject.Provider.
+interface Provider<T> {
+  T get();
+}
+
+// Copy of dagger.internal.SingleClass.
+final class SingleCheck<T> implements Provider<T> {
+  private static final Object UNINITIALIZED = new Object();
+
+  private volatile Provider<T> provider;
+  private volatile Object instance = UNINITIALIZED;
+
+  private SingleCheck(Provider<T> provider) {
+    assert provider != null;
+    this.provider = provider;
+  }
+
+  @SuppressWarnings("unchecked") // cast only happens when result comes from the delegate provider
+  @Override
+  public T get() {
+    Object local = instance;
+    if (local == UNINITIALIZED) {
+      // provider is volatile and might become null after the check, so retrieve the provider first
+      Provider<T> providerReference = provider;
+      if (providerReference == null) {
+        // The provider was null, so the instance must already be set
+        local = instance;
+      } else {
+        local = providerReference.get();
+        instance = local;
+
+        // Null out the reference to the provider. We are never going to need it again, so we can
+        // make it eligible for GC.
+        provider = null;
+      }
+    }
+    return (T) local;
+  }
+
+  // This method is not relevant for the test.
+  /*
+  public static <P extends Provider<T>, T> Provider<T> provider(P provider) {
+    // If a scoped @Binds delegates to a scoped binding, don't cache the value again.
+    if (provider instanceof SingleCheck || provider instanceof DoubleCheck) {
+      return provider;
+    }
+    return new SingleCheck<T>(checkNotNull(provider));
+  }
+  */
+}
+
+// Several field gets on non-volatile and volatile fields on the same class.
+class A {
+  int t;
+  int f;
+  static int sf;
+  volatile int v;
+  static volatile int sv;
+
+  public void mf() {
+    t = f;
+    t = f;
+    t = f;
+    t = f;
+    t = f;
+  }
+
+  public void msf() {
+    t = sf;
+    t = sf;
+    t = sf;
+    t = sf;
+    t = sf;
+  }
+
+  public void mv() {
+    t = v;
+    t = v;
+    t = v;
+    t = v;
+    t = v;
+  }
+
+  public void msv() {
+    t = sv;
+    t = sv;
+    t = sv;
+    t = sv;
+    t = sv;
+  }
+}
+
+// Several field gets on non-volatile and volatile fields on different class.
+class B {
+  int t;
+
+  public void mf(A a) {
+    t = a.f;
+    t = a.f;
+    t = a.f;
+    t = a.f;
+    t = a.f;
+  }
+
+  public void msf() {
+    t = A.sf;
+    t = A.sf;
+    t = A.sf;
+    t = A.sf;
+    t = A.sf;
+  }
+
+  public void mv(A a) {
+    t = a.v;
+    t = a.v;
+    t = a.v;
+    t = a.v;
+    t = a.v;
+  }
+
+  public void msv() {
+    t = A.sv;
+    t = A.sv;
+    t = A.sv;
+    t = A.sv;
+    t = A.sv;
+  }
+}
+
+// Modified sample from http://tutorials.jenkov.com/java-concurrency/volatile.html.
+class C {
+  private int years;
+  private int months;
+  private volatile int days;
+
+  public int totalDays() {
+    int total = this.days;
+    total += months * 30;
+    total += years * 365;
+    return total;
+  }
+
+  public int totalDaysTimes2() {
+    int total = this.days;
+    total += months * 30;
+    total += years * 365;
+    total += this.days;
+    total += months * 30;
+    total += years * 365;
+    return total;
+  }
+
+  public int totalDaysTimes3() {
+    int total = this.days;
+    total += months * 30;
+    total += years * 365;
+    total += this.days;
+    total += months * 30;
+    total += years * 365;
+    total += this.days;
+    total += months * 30;
+    total += years * 365;
+    return total;
+  }
+
+  public void update(int years, int months, int days){
+    this.years  = years;
+    this.months = months;
+    this.days   = days;
+  }
+}
+
+public class B111250398 extends TestBase {
+
+  private void releaseMode(InternalOptions options) {
+    options.debug = false;
+  }
+
+  private long countIget(DexCode code, DexField field) {
+    return Arrays.stream(code.instructions)
+        .filter(instruction -> instruction instanceof Iget)
+        .map(instruction -> (Iget) instruction)
+        .filter(get -> get.getField() == field)
+        .count();
+  }
+
+  private long countSget(DexCode code, DexField field) {
+    return Arrays.stream(code.instructions)
+        .filter(instruction -> instruction instanceof Sget)
+        .map(instruction -> (Sget) instruction)
+        .filter(get -> get.getField() == field)
+        .count();
+  }
+
+  private long countIgetObject(MethodSubject method, FieldSubject field) {
+    return Arrays.stream(method.getMethod().getCode().asDexCode().instructions)
+        .filter(instruction -> instruction instanceof IgetObject)
+        .map(instruction -> (IgetObject) instruction)
+        .filter(get -> get.getField() == field.getField().field)
+        .count();
+  }
+
+  private void check(DexInspector inspector, int mfOnBGets, int msfOnBGets) {
+    ClassSubject classA = inspector.clazz(A.class);
+    assertThat(classA, isPresent());
+    MethodSubject mfOnA = classA.method("void", "mf", ImmutableList.of());
+    assertThat(mfOnA, isPresent());
+    MethodSubject msfOnA = classA.method("void", "msf", ImmutableList.of());
+    assertThat(msfOnA, isPresent());
+    MethodSubject mvOnA = classA.method("void", "mv", ImmutableList.of());
+    assertThat(mvOnA, isPresent());
+    MethodSubject msvOnA = classA.method("void", "msv", ImmutableList.of());
+    assertThat(msvOnA, isPresent());
+    FieldSubject fOnA = classA.field("int", "f");
+    assertThat(fOnA, isPresent());
+    FieldSubject sfOnA = classA.field("int", "sf");
+    assertThat(sfOnA, isPresent());
+    FieldSubject vOnA = classA.field("int", "v");
+    assertThat(vOnA, isPresent());
+    FieldSubject svOnA = classA.field("int", "sv");
+    assertThat(svOnA, isPresent());
+    ClassSubject classB = inspector.clazz(B.class);
+    assertThat(classB, isPresent());
+    MethodSubject mfOnB = classB.method("void", "mf", ImmutableList.of(classA.getOriginalName()));
+    assertThat(mfOnB, isPresent());
+    MethodSubject msfOnB = classB.method("void", "msf", ImmutableList.of());
+    assertThat(msfOnB, isPresent());
+    MethodSubject mvOnB = classB.method("void", "mv", ImmutableList.of(classA.getOriginalName()));
+    assertThat(mvOnB, isPresent());
+    MethodSubject msvOnB = classB.method("void", "msv", ImmutableList.of());
+    assertThat(msvOnB, isPresent());
+    // Field load of volatile fields are never eliminated.
+    assertEquals(5, countIget(mvOnA.getMethod().getCode().asDexCode(), vOnA.getField().field));
+    assertEquals(5, countSget(msvOnA.getMethod().getCode().asDexCode(), svOnA.getField().field));
+    assertEquals(5, countIget(mvOnB.getMethod().getCode().asDexCode(), vOnA.getField().field));
+    assertEquals(5, countSget(msvOnB.getMethod().getCode().asDexCode(), svOnA.getField().field));
+    // For fields on the same class both separate compilation (D8) and whole program
+    // compilation (R8) will eliminate field loads on non-volatile fields.
+    assertEquals(1, countIget(mfOnA.getMethod().getCode().asDexCode(), fOnA.getField().field));
+    assertEquals(1, countSget(msfOnA.getMethod().getCode().asDexCode(), sfOnA.getField().field));
+    // For fields on other class both separate compilation (D8) and whole program
+    // compilation (R8) will differ in the eliminated field loads of non-volatile fields.
+    assertEquals(mfOnBGets,
+        countIget(mfOnB.getMethod().getCode().asDexCode(), fOnA.getField().field));
+    assertEquals(msfOnBGets,
+        countSget(msfOnB.getMethod().getCode().asDexCode(), sfOnA.getField().field));
+  }
+
+  @Test
+  public void testSeparateCompilation() throws Exception {
+    DexInspector inspector =
+        new DexInspector(compileWithD8(readClasses(A.class, B.class), this::releaseMode));
+    check(inspector, 5, 5);
+  }
+
+  @Test
+  public void testWholeProgram() throws Exception {
+    DexInspector inspector =
+        new DexInspector(compileWithR8(readClasses(A.class, B.class), this::releaseMode));
+    // The reason for getting two Igets in B.mf is that the first Iget inserts a NonNull
+    // instruction which creates a new value for the remaining Igets.
+    check(inspector, 2, 1);
+  }
+
+  private void checkMixed(AndroidApp app) throws Exception{
+    DexInspector inspector = new DexInspector(app);
+    ClassSubject classC = inspector.clazz(C.class);
+    assertThat(classC, isPresent());
+    MethodSubject totalDays = classC.method("int", "totalDays", ImmutableList.of());
+    assertThat(totalDays, isPresent());
+    MethodSubject totalDaysTimes2 = classC.method("int", "totalDaysTimes2", ImmutableList.of());
+    assertThat(totalDaysTimes2, isPresent());
+    MethodSubject totalDaysTimes3 = classC.method("int", "totalDaysTimes3", ImmutableList.of());
+    assertThat(totalDaysTimes3, isPresent());
+    FieldSubject years = classC.field("int", "years");
+    assertThat(years, isPresent());
+    FieldSubject months = classC.field("int", "months");
+    assertThat(months, isPresent());
+    FieldSubject days = classC.field("int", "days");
+    assertThat(days, isPresent());
+
+
+    for (FieldSubject field : new FieldSubject[]{years, months, days}) {
+      assertEquals(1,
+          countIget(totalDays.getMethod().getCode().asDexCode(), field.getField().field));
+      assertEquals(2,
+          countIget(totalDaysTimes2.getMethod().getCode().asDexCode(), field.getField().field));
+      assertEquals(3,
+          countIget(totalDaysTimes3.getMethod().getCode().asDexCode(), field.getField().field));
+    }
+  }
+
+  @Test
+  public void testMixedVolatileNonVolatile() throws Exception {
+    AndroidApp app = readClasses(C.class);
+    checkMixed(compileWithD8(app, this::releaseMode));
+    checkMixed(compileWithR8(app, this::releaseMode));
+  }
+
+  private void checkDaggerSingleProviderGet(AndroidApp app) throws Exception {
+    DexInspector inspector = new DexInspector(app);
+    MethodSubject get =
+        inspector.clazz(SingleCheck.class).method("java.lang.Object", "get", ImmutableList.of());
+    assertThat(get, isPresent());
+    FieldSubject instance =
+        inspector.clazz(SingleCheck.class).field("java.lang.Object", "instance");
+    assertEquals(2, countIgetObject(get, instance));
+  }
+
+  @Test
+  public void testDaggerSingleProvider() throws Exception {
+    AndroidApp app = readClasses(Provider.class, SingleCheck.class);
+    checkDaggerSingleProviderGet(compileWithD8(app, this::releaseMode));
+    checkDaggerSingleProviderGet(compileWithR8(app, this::releaseMode));
+  }
+}