Keep track of static-puts in clinit and remove them if necessary:
if there is another static-put with constant value, all the other
static-puts up to that point are unnecessary. We should remove them
all together, otherwise remaining static-put with non-constant may
change program behaviors.
Bug: 138912149
Change-Id: Iaf095ac0b2a38ce4ccae07fd9a3f1fa0ccee1c91
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java b/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java
index ae8ec3d..1f7a504 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java
@@ -116,7 +116,7 @@
// the field initial value.
Set<StaticPut> unnecessaryStaticPuts = Sets.newIdentityHashSet();
Collection<StaticPut> finalFieldPuts =
- computeUnnecessaryStaticPuts(code, clazz, unnecessaryStaticPuts);
+ findFinalFieldPutsWhileCollectingUnnecessaryStaticPuts(code, clazz, unnecessaryStaticPuts);
// Return eagerly if there is nothing to optimize.
if (unnecessaryStaticPuts.isEmpty()) {
@@ -315,9 +315,10 @@
return null;
}
- private Collection<StaticPut> computeUnnecessaryStaticPuts(
- IRCode code, DexClass clazz, Set<StaticPut> puts) {
- Map<DexField, StaticPut> finalFieldPut = Maps.newIdentityHashMap();
+ private Collection<StaticPut> findFinalFieldPutsWhileCollectingUnnecessaryStaticPuts(
+ IRCode code, DexClass clazz, Set<StaticPut> unnecessaryStaticPuts) {
+ Map<DexField, StaticPut> finalFieldPuts = Maps.newIdentityHashMap();
+ Map<DexField, Set<StaticPut>> isWrittenBefore = Maps.newIdentityHashMap();
Set<DexField> isReadBefore = Sets.newIdentityHashSet();
final int color = code.reserveMarkingColor();
try {
@@ -330,7 +331,7 @@
// as long as the instructions do not throw.
ArrayPut arrayPut = instruction.asArrayPut();
if (arrayPut.instructionInstanceCanThrow(appView, clazz.type).isThrowing()) {
- return finalFieldPut.values();
+ return validateFinalFieldPuts(finalFieldPuts, isWrittenBefore);
}
} else if (instruction.isStaticGet()) {
StaticGet get = instruction.asStaticGet();
@@ -339,14 +340,14 @@
isReadBefore.add(field.field);
} else if (instruction.instructionMayHaveSideEffects(appView, clazz.type)) {
// Reading another field is only OK if the read does not have side-effects.
- return finalFieldPut.values();
+ return validateFinalFieldPuts(finalFieldPuts, isWrittenBefore);
}
} else if (instruction.isStaticPut()) {
StaticPut put = instruction.asStaticPut();
if (put.getField().holder != clazz.type) {
// Can cause clinit on another class which can read uninitialized static fields
// of this class.
- return finalFieldPut.values();
+ return validateFinalFieldPuts(finalFieldPuts, isWrittenBefore);
}
DexField field = put.getField();
if (clazz.definesStaticField(field)) {
@@ -359,27 +360,54 @@
continue;
}
if (put.value().isConstant()) {
- if ((field.type.isClassType() || field.type.isArrayType())
- && put.value().isZero()) {
- finalFieldPut.put(put.getField(), put);
- puts.add(put);
+ if (field.type.isReferenceType() && put.value().isZero()) {
+ finalFieldPuts.put(field, put);
+ unnecessaryStaticPuts.add(put);
+ // If this field has been written before, those static-put's up to this point are
+ // redundant. We should remove them all together; otherwise, remaining static-put
+ // that is not constant can change the program semantics. See b/138912149.
+ if (isWrittenBefore.containsKey(field)) {
+ unnecessaryStaticPuts.addAll(isWrittenBefore.get(field));
+ isWrittenBefore.remove(field);
+ }
+ continue;
} else if (field.type.isPrimitiveType()
|| field.type == dexItemFactory.stringType) {
- finalFieldPut.put(put.getField(), put);
- puts.add(put);
+ finalFieldPuts.put(field, put);
+ unnecessaryStaticPuts.add(put);
+ if (isWrittenBefore.containsKey(field)) {
+ unnecessaryStaticPuts.addAll(isWrittenBefore.get(field));
+ isWrittenBefore.remove(field);
+ }
+ continue;
}
+ // Still constant, but not able to represent it as static encoded values, e.g.,
+ // const-class, const-method-handle, etc. This static-put can be redundant if the
+ // field is rewritten with another constant. Will fall through and track static-put.
} else if (isClassNameConstantOf(clazz, put)) {
// Collect put of class name constant as a potential default value.
- finalFieldPut.put(put.getField(), put);
- puts.add(put);
+ finalFieldPuts.put(field, put);
+ unnecessaryStaticPuts.add(put);
+ if (isWrittenBefore.containsKey(field)) {
+ unnecessaryStaticPuts.addAll(isWrittenBefore.get(field));
+ isWrittenBefore.remove(field);
+ }
+ continue;
}
+ // static-put that is reaching here can be redundant if the corresponding field is
+ // rewritten with another constant (of course before being read).
+ // However, if static-put is still remaining in `isWrittenBefore`, that indicates
+ // the previous candidate as final field put is no longer valid.
+ isWrittenBefore
+ .computeIfAbsent(field, ignore -> Sets.newIdentityHashSet())
+ .add(put);
} else {
// Writing another field is not OK.
- return finalFieldPut.values();
+ return validateFinalFieldPuts(finalFieldPuts, isWrittenBefore);
}
} else if (instruction.instructionMayHaveSideEffects(appView, clazz.type)) {
// Some other instruction that has side-effects. Stop here.
- return finalFieldPut.values();
+ return validateFinalFieldPuts(finalFieldPuts, isWrittenBefore);
} else {
// TODO(b/120138731): This check should be removed when the Class.get*Name()
// optimizations become enabled.
@@ -397,7 +425,7 @@
// OK, this value is technically a constant.
continue;
}
- return finalFieldPut.values();
+ return validateFinalFieldPuts(finalFieldPuts, isWrittenBefore);
}
}
}
@@ -409,7 +437,33 @@
} finally {
code.returnMarkingColor(color);
}
- return finalFieldPut.values();
+ return validateFinalFieldPuts(finalFieldPuts, isWrittenBefore);
+ }
+
+ private Collection<StaticPut> validateFinalFieldPuts(
+ Map<DexField, StaticPut> finalFieldPuts,
+ Map<DexField, Set<StaticPut>> isWrittenBefore) {
+ // If a field is rewritten again with other values that we can't represent as static encoded
+ // values, that would be recorded at `isWrittenBefore`, which is used to collect and remove
+ // redundant static-puts. The remnant indicates that the candidate for final field put is not
+ // valid anymore, so remove it.
+ //
+ // For example,
+ // static String x;
+ //
+ // static {
+ // x = "constant"; // will be in finalFieldPut
+ // x = <non-constant> // will be added to isWrittenBefore
+ // // x = "another-constant"
+ // }
+ // If there is another static-put with a constant, static-put stored in `isWrittenBefore` is
+ // used to remove redundant static-put together. (And the previous constant is overwritten.)
+ // If not, `x` has "constant" as a default value, whereas static-put with non-constant is
+ // remaining. If other optimizations (most likely member value propagation) rely on encoded
+ // values, leaving it can cause incorrect optimizations. Thus, we invalidate candidates of
+ // final field puts at all.
+ isWrittenBefore.keySet().forEach(finalFieldPuts::remove);
+ return finalFieldPuts.values();
}
// Check if the static put is a constant derived from the class holding the method.
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/B138912149.java b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/B138912149.java
index 280853e..bee06af 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/B138912149.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/B138912149.java
@@ -4,6 +4,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.Assume.assumeTrue;
import com.android.tools.r8.TestBase;
@@ -34,10 +37,16 @@
.addInnerClasses(B138912149.class)
.addKeepMainRule(TestClass.class)
.setMinApi(parameters.getRuntime())
+ .noMinification()
.compile()
.run(parameters.getRuntime(), TestClass.class)
- // TODO(b/138912149): Should be "The end".
- .assertSuccessWithOutputLines("Dead code 2", "The end");
+ .assertSuccessWithOutputLines("The end")
+ .inspect(codeInspector -> {
+ // All <clinit>s are simplified and shrunk.
+ codeInspector.forAllClasses(classSubject -> {
+ assertThat(classSubject.clinit(), not(isPresent()));
+ });
+ });
}
@Test
@@ -58,6 +67,12 @@
if (B.alwaysFalse || !B.alwaysTrue) {
System.out.println("Dead code 2");
}
+ if (C.alwaysNull != null) {
+ System.out.println("Dead code 3");
+ }
+ if (!N.name.endsWith("N")) {
+ System.out.println("Dead code: " + N.name);
+ }
System.out.println("The end");
}
}
@@ -79,4 +94,27 @@
alwaysTrue = true;
}
}
+
+ static class C {
+ static Object alwaysNull;
+
+ static {
+ alwaysNull = C.class;
+ // Either keep this static-put or remove both static-put's and put `null` to `alwaysNull`.
+ alwaysNull = null;
+ }
+ }
+
+ static class N {
+ static String name;
+
+ static {
+ name = "dummy";
+ // Either keep this static-put or remove both static-put's and put the name, ...$N, to `name`.
+ name = N.class.getName();
+ // Note that we can't use other kinds of names, e.g., simple name, because the enclosing class
+ // is not provided as a program class, hence all attributes related to inner-name computation
+ // is gone in this test setting.
+ }
+ }
}