Introduce notion of observable side effects for class initializers
Bug: 135918413
Change-Id: Icfa2ae3c345fad5039deb4daa7697f410c9ce0e1
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index 07f9e1c..e47fe4d 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -676,27 +676,6 @@
return getClassInitializer() != null;
}
- public boolean hasTrivialClassInitializer() {
- if (isLibraryClass()) {
- // We don't know for library classes in general but assume that java.lang.Object is safe.
- return superType == null;
- }
- DexEncodedMethod clinit = getClassInitializer();
- return clinit != null && clinit.getCode() != null && clinit.getCode().isEmptyVoidMethod();
- }
-
- public boolean hasNonTrivialClassInitializer() {
- if (isLibraryClass()) {
- // We don't know for library classes in general but assume that java.lang.Object is safe.
- return superType != null;
- }
- DexEncodedMethod clinit = getClassInitializer();
- if (clinit == null || clinit.getCode() == null) {
- return false;
- }
- return !clinit.getCode().isEmptyVoidMethod();
- }
-
public boolean hasDefaultInitializer() {
return getDefaultInitializer() != null;
}
@@ -760,7 +739,7 @@
return false;
}
}
- if (hasNonTrivialClassInitializer()) {
+ if (hasClassInitializerWithObservableSideEffects()) {
return true;
}
if (defaultValuesForStaticFieldsMayTriggerAllocation()) {
@@ -769,6 +748,18 @@
return initializationOfParentTypesMayHaveSideEffects(appView, ignore);
}
+ private boolean hasClassInitializerWithObservableSideEffects() {
+ if (isLibraryClass()) {
+ // We don't know for library classes in general but assume that java.lang.Object is safe.
+ return superType != null;
+ }
+ DexEncodedMethod clinit = getClassInitializer();
+ if (clinit == null || clinit.getCode() == null) {
+ return false;
+ }
+ return clinit.getOptimizationInfo().classInitializerMayHaveObservableSideEffects();
+ }
+
public Iterable<DexType> allImmediateSupertypes() {
Iterator<DexType> iterator =
superType != null
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 234d322..64c5b9b 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -1092,6 +1092,11 @@
private DefaultMethodOptimizationInfoImpl() {}
@Override
+ public boolean classInitializerMayHaveObservableSideEffects() {
+ return true;
+ }
+
+ @Override
public TypeLatticeElement getDynamicReturnType() {
return UNKNOWN_TYPE;
}
@@ -1238,6 +1243,7 @@
public static class MethodOptimizationInfoImpl implements UpdatableMethodOptimizationInfo {
+ private boolean classInitializationMayHaveObservableSideEffects = true;
private boolean hasBeenInlinedIntoSingleCallSite = false;
private Set<DexType> initializedClassesOnNormalExit =
DefaultMethodOptimizationInfoImpl.UNKNOWN_INITIALIZED_CLASSES_ON_NORMAL_EXIT;
@@ -1320,6 +1326,16 @@
}
@Override
+ public boolean classInitializerMayHaveObservableSideEffects() {
+ return classInitializationMayHaveObservableSideEffects;
+ }
+
+ @Override
+ public void unsetClassInitializationMayHaveObservableSideEffects() {
+ classInitializationMayHaveObservableSideEffects = false;
+ }
+
+ @Override
public TypeLatticeElement getDynamicReturnType() {
return returnsObjectOfType;
}
diff --git a/src/main/java/com/android/tools/r8/graph/MethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/graph/MethodOptimizationInfo.java
index 108779c..782ff34 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodOptimizationInfo.java
@@ -19,6 +19,8 @@
Default
}
+ boolean classInitializerMayHaveObservableSideEffects();
+
TypeLatticeElement getDynamicReturnType();
ParameterUsage getParameterUsages(int parameter);
diff --git a/src/main/java/com/android/tools/r8/graph/UpdatableMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/graph/UpdatableMethodOptimizationInfo.java
index da39812..091a5a4 100644
--- a/src/main/java/com/android/tools/r8/graph/UpdatableMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/UpdatableMethodOptimizationInfo.java
@@ -59,4 +59,6 @@
void unsetForceInline();
void markNeverInline();
+
+ void unsetClassInitializationMayHaveObservableSideEffects();
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/Value.java b/src/main/java/com/android/tools/r8/ir/code/Value.java
index 52dcbd1..c170810 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Value.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Value.java
@@ -483,6 +483,17 @@
return false;
}
+ public boolean mayDependOnEnvironment() {
+ Value root = getAliasedValue();
+ if (root.isConstant()) {
+ return false;
+ }
+ if (root.isConstantArray()) {
+ return false;
+ }
+ return true;
+ }
+
public boolean usedInMonitorOperation() {
for (Instruction instruction : uniqueUsers()) {
if (instruction.isMonitor()) {
@@ -793,6 +804,11 @@
return definition.isOutConstant() && !hasLocalInfo();
}
+ public boolean isConstantArray() {
+ Value root = getAliasedValue();
+ return !root.isPhi() && definition.isNewArrayEmpty();
+ }
+
public boolean isPhi() {
return false;
}
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 26ea6c5..fe76855 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
@@ -19,6 +19,7 @@
import com.android.tools.r8.graph.DexApplication.Builder;
import com.android.tools.r8.graph.DexCallSite;
import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
@@ -42,6 +43,7 @@
import com.android.tools.r8.ir.code.InstructionListIterator;
import com.android.tools.r8.ir.code.InvokeStatic;
import com.android.tools.r8.ir.code.NumericType;
+import com.android.tools.r8.ir.code.StaticPut;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.desugar.BackportedMethodRewriter;
import com.android.tools.r8.ir.desugar.CovariantReturnTypeAnnotationTransformer;
@@ -1359,22 +1361,71 @@
private void computeMayHaveSideEffects(
OptimizationFeedback feedback, DexEncodedMethod method, IRCode code) {
- if (options.enableSideEffectAnalysis
- && !appView.appInfo().withLiveness().mayHaveSideEffects.containsKey(method.method)) {
- boolean mayHaveSideEffects =
- // If the method is synchronized then it acquires a lock.
- method.accessFlags.isSynchronized()
- || (appView.dexItemFactory().isConstructor(method.method)
- && hasNonTrivialFinalizeMethod(method.method.holder))
- || Streams.stream(code.instructions())
- .anyMatch(
- instruction ->
- instruction.instructionMayHaveSideEffects(appView, method.method.holder));
- if (!mayHaveSideEffects) {
- // If the method is native, we don't know what could happen.
- assert !method.accessFlags.isNative();
- feedback.methodMayNotHaveSideEffects(method);
+ // If the method is native, we don't know what could happen.
+ assert !method.accessFlags.isNative();
+
+ if (!options.enableSideEffectAnalysis) {
+ return;
+ }
+
+ if (appView.appInfo().withLiveness().mayHaveSideEffects.containsKey(method.method)) {
+ return;
+ }
+
+ DexType context = method.method.holder;
+
+ if (method.isClassInitializer()) {
+ // For class initializers, we also wish to compute if the class initializer has observable
+ // side effects. A class initializer has observable side effects if it writes a static field
+ // of another class, or if any non-static-put instructions may have side effects.
+ boolean hasIgnoredStaticPut = false;
+ boolean mayHaveObservableSideEffects = false;
+ for (Instruction instruction : code.instructions()) {
+ if (instruction.isStaticPut()) {
+ StaticPut staticPut = instruction.asStaticPut();
+ DexEncodedField field = appView.appInfo().resolveField(staticPut.getField());
+ if (field == null
+ || field.field.holder != method.method.holder
+ || staticPut.inValue().mayDependOnEnvironment()
+ || instruction.instructionInstanceCanThrow(appView, context).isThrowing()) {
+ mayHaveObservableSideEffects = true;
+ break;
+ }
+ hasIgnoredStaticPut = true;
+ continue;
+ }
+ if (instruction.instructionMayHaveSideEffects(appView, context)) {
+ mayHaveObservableSideEffects = true;
+ break;
+ }
}
+ if (!mayHaveObservableSideEffects) {
+ feedback.unsetClassInitializerMayHaveObservableSideEffects(method);
+ if (!hasIgnoredStaticPut) {
+ feedback.methodMayNotHaveSideEffects(method);
+ }
+ }
+ return;
+ }
+
+ boolean mayHaveSideEffects;
+ if (method.accessFlags.isSynchronized()) {
+ // If the method is synchronized then it acquires a lock.
+ mayHaveSideEffects = true;
+ } else if (method.isInstanceInitializer() && hasNonTrivialFinalizeMethod(context)) {
+ // If a class T overrides java.lang.Object.finalize(), then treat the constructor as having
+ // side effects. This ensures that we won't remove instructions on the form `new-instance
+ // {v0}, T`.
+ mayHaveSideEffects = true;
+ } else {
+ // Otherwise, check if there is an instruction that has side effects.
+ mayHaveSideEffects =
+ Streams.stream(code.instructions())
+ .anyMatch(instruction -> instruction.instructionMayHaveSideEffects(appView, context));
+ }
+
+ if (!mayHaveSideEffects) {
+ feedback.methodMayNotHaveSideEffects(method);
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
index 752f35e..6fac364 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
@@ -59,4 +59,6 @@
void setNonNullParamOrThrow(DexEncodedMethod method, BitSet facts);
void setNonNullParamOnNormalExits(DexEncodedMethod method, BitSet facts);
+
+ void unsetClassInitializerMayHaveObservableSideEffects(DexEncodedMethod method);
}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDelayed.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDelayed.java
index 82f6391..bcaee39 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDelayed.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDelayed.java
@@ -148,6 +148,12 @@
getOptimizationInfoForUpdating(method).setNonNullParamOnNormalExits(facts);
}
+ @Override
+ public synchronized void unsetClassInitializerMayHaveObservableSideEffects(
+ DexEncodedMethod method) {
+ getOptimizationInfoForUpdating(method).unsetClassInitializationMayHaveObservableSideEffects();
+ }
+
public void updateVisibleOptimizationInfo() {
// Remove methods that have become obsolete. A method may become obsolete, for example, as a
// result of the class staticizer, which aims to transform virtual methods on companion classes
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
index c0655df..964983a 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
@@ -95,4 +95,7 @@
@Override
public void setNonNullParamOnNormalExits(DexEncodedMethod method, BitSet facts) {
}
+
+ @Override
+ public void unsetClassInitializerMayHaveObservableSideEffects(DexEncodedMethod method) {}
}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackSimple.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackSimple.java
index 1fc7a91..b30f830 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackSimple.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackSimple.java
@@ -124,4 +124,9 @@
public void setNonNullParamOnNormalExits(DexEncodedMethod method, BitSet facts) {
// Ignored.
}
+
+ @Override
+ public void unsetClassInitializerMayHaveObservableSideEffects(DexEncodedMethod method) {
+ // Ignored.
+ }
}
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 2c2ef08..090152a 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -898,9 +898,9 @@
// We also need to add the corresponding <clinit> to the set of live methods, as otherwise
// static field initialization (and other class-load-time sideeffects) will not happen.
KeepReason reason = KeepReason.reachableFromLiveType(type);
- if (holder.isProgramClass() && holder.hasNonTrivialClassInitializer()) {
+ if (holder.isProgramClass() && holder.hasClassInitializer()) {
DexEncodedMethod clinit = holder.getClassInitializer();
- if (clinit != null) {
+ if (clinit != null && clinit.getOptimizationInfo().mayHaveSideEffects()) {
assert clinit.method.holder == holder.type;
markDirectStaticOrConstructorMethodAsLive(clinit, reason);
}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/B135918413.java b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/B135918413.java
index c05847c..34bad33 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/B135918413.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/B135918413.java
@@ -5,8 +5,9 @@
package com.android.tools.r8.ir.optimize.membervaluepropagation;
import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import com.android.tools.r8.NeverInline;
import com.android.tools.r8.TestBase;
@@ -14,6 +15,7 @@
import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.utils.codeinspector.ClassSubject;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FieldSubject;
import com.android.tools.r8.utils.codeinspector.InstructionSubject;
import com.android.tools.r8.utils.codeinspector.MethodSubject;
import org.junit.Test;
@@ -52,18 +54,26 @@
ClassSubject classSubject = inspector.clazz(TestClass.class);
assertThat(classSubject, isPresent());
+ ClassSubject configClassSubject = inspector.clazz(Config.class);
+ assertThat(configClassSubject, isPresent());
+
+ FieldSubject alwaysEmptyFieldSubject = configClassSubject.uniqueFieldWithName("alwaysEmpty");
+ assertThat(alwaysEmptyFieldSubject, isPresent());
+
MethodSubject mainMethodSubject = classSubject.mainMethod();
assertThat(mainMethodSubject, isPresent());
- // TODO(b/135918413): Should be true.
- assertFalse(
+ assertTrue(
mainMethodSubject
.streamInstructions()
.filter(InstructionSubject::isStaticGet)
- .allMatch(instruction -> instruction.getField().name.toSourceString().equals("out")));
+ .map(InstructionSubject::getField)
+ .allMatch(
+ field ->
+ field.name.toSourceString().equals(alwaysEmptyFieldSubject.getFinalName())
+ || field.name.toSourceString().equals("out")));
MethodSubject deadMethodSubject = classSubject.uniqueMethodWithName("dead");
- // TODO(b/135918413): Should be absent.
- assertThat(deadMethodSubject, isPresent());
+ assertThat(deadMethodSubject, not(isPresent()));
}
static class TestClass {