Account for classes with kept classes in default field value analysis
Fixes: b/379034741
Change-Id: I230714181f351334d97ee0f60cd0252dcb5710e1
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPropagator.java
index 800b277..c403ea1 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPropagator.java
@@ -84,6 +84,7 @@
converter,
fieldStates,
methodStates,
+ immediateSubtypingInfo,
inFlowComparator)
.run(executorService);
timing.end();
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java
index 28d0ead..4c9489a 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/DefaultFieldValueJoiner.java
@@ -11,6 +11,7 @@
import com.android.tools.r8.graph.DefaultUseRegistryWithResult;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
import com.android.tools.r8.graph.ProgramField;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.ir.analysis.type.DynamicType;
@@ -36,6 +37,8 @@
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.Timing;
import com.android.tools.r8.utils.collections.ProgramFieldSet;
+import it.unimi.dsi.fastutil.objects.Reference2BooleanMap;
+import it.unimi.dsi.fastutil.objects.Reference2BooleanOpenHashMap;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
@@ -57,17 +60,20 @@
private final Set<DexProgramClass> classesWithSingleCallerInlinedInstanceInitializers;
private final FieldStateCollection fieldStates;
private final List<FlowGraph> flowGraphs;
+ private final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
public DefaultFieldValueJoiner(
AppView<AppInfoWithLiveness> appView,
Set<DexProgramClass> classesWithSingleCallerInlinedInstanceInitializers,
FieldStateCollection fieldStates,
- List<FlowGraph> flowGraphs) {
+ List<FlowGraph> flowGraphs,
+ ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
this.appView = appView;
this.classesWithSingleCallerInlinedInstanceInitializers =
classesWithSingleCallerInlinedInstanceInitializers;
this.fieldStates = fieldStates;
this.flowGraphs = flowGraphs;
+ this.immediateSubtypingInfo = immediateSubtypingInfo;
}
public Map<FlowGraph, Deque<FlowGraphNode>> joinDefaultFieldValuesForFieldsWithReadBeforeWrite(
@@ -79,12 +85,35 @@
return Collections.emptyMap();
}
+ // Classes that are kept or have a kept subclass can be instantiated in a way that does not use
+ // any instance initializers. For all fields on such classes, we therefore include the default
+ // field value.
+ Map<DexProgramClass, Boolean> classesWithKeptSubclasses =
+ computeClassesWithKeptSubclasses(fieldsOfInterest);
+ ProgramFieldSet fieldsWithLiveDefaultValue = ProgramFieldSet.createConcurrent();
+ MapUtils.removeIf(
+ fieldsOfInterest,
+ (clazz, fields) -> {
+ Boolean isKeptOrHasKeptSubclass = classesWithKeptSubclasses.get(clazz);
+ assert isKeptOrHasKeptSubclass != null;
+ if (isKeptOrHasKeptSubclass) {
+ fields.removeIf(
+ field -> {
+ if (field.getDefinition().isInstance()) {
+ fieldsWithLiveDefaultValue.add(field);
+ return true;
+ }
+ return false;
+ });
+ }
+ return fields.isEmpty();
+ });
+
// If constructor inlining is disabled, then we focus on whether each instance initializer
// definitely assigns the given field before it is read. We do the same for final and static
// fields.
Map<DexType, ProgramFieldSet> fieldsNotSubjectToInitializerAnalysis =
removeFieldsNotSubjectToInitializerAnalysis(fieldsOfInterest);
- ProgramFieldSet fieldsWithLiveDefaultValue = ProgramFieldSet.createConcurrent();
analyzeInitializers(
fieldsOfInterest,
field -> {
@@ -108,7 +137,46 @@
return updateFlowGraphs(fieldsWithLiveDefaultValue, executorService);
}
- protected Map<DexProgramClass, List<ProgramField>> getFieldsOfInterest() {
+ /**
+ * For each of the classes in the key set of the given {@param fieldsOfInterest}, this computes
+ * whether the class is kept or has a kept subclass.
+ */
+ private Map<DexProgramClass, Boolean> computeClassesWithKeptSubclasses(
+ Map<DexProgramClass, List<ProgramField>> fieldsOfInterest) {
+ Reference2BooleanMap<DexProgramClass> classesWithKeptSubclasses =
+ new Reference2BooleanOpenHashMap<>();
+ for (DexProgramClass clazz : fieldsOfInterest.keySet()) {
+ computeClassHasKeptSubclass(clazz, classesWithKeptSubclasses);
+ }
+ return classesWithKeptSubclasses;
+ }
+
+ private boolean computeClassHasKeptSubclass(
+ DexProgramClass clazz, Reference2BooleanMap<DexProgramClass> classesWithKeptSubclasses) {
+ if (classesWithKeptSubclasses.containsKey(clazz)) {
+ return classesWithKeptSubclasses.getBoolean(clazz);
+ }
+ if (isKeptDirectly(clazz)) {
+ classesWithKeptSubclasses.put(clazz, true);
+ return true;
+ }
+ for (DexProgramClass subclass : immediateSubtypingInfo.getSubclasses(clazz)) {
+ if (computeClassHasKeptSubclass(subclass, classesWithKeptSubclasses)) {
+ classesWithKeptSubclasses.put(clazz, true);
+ return true;
+ }
+ }
+ assert !classesWithKeptSubclasses.containsKey(clazz)
+ || !classesWithKeptSubclasses.getBoolean(clazz);
+ classesWithKeptSubclasses.put(clazz, false);
+ return false;
+ }
+
+ private boolean isKeptDirectly(DexProgramClass clazz) {
+ return appView.getKeepInfo(clazz).isPinned(appView.options());
+ }
+
+ private Map<DexProgramClass, List<ProgramField>> getFieldsOfInterest() {
Map<DexProgramClass, List<ProgramField>> fieldsOfInterest = new IdentityHashMap<>();
for (DexProgramClass clazz : appView.appInfo().classes()) {
clazz.forEachProgramField(
@@ -262,12 +330,6 @@
(holderType, fields) -> {
assert !fields.isEmpty();
DexProgramClass holder = fields.iterator().next().getHolder();
- // If the class is kept it could be instantiated directly, in which case all default field
- // values could be live.
- if (appView.getKeepInfo(holder).isPinned(appView.options())) {
- fields.forEach(liveDefaultValueConsumer);
- return true;
- }
if (holder.isFinal() || !appView.appInfo().isInstantiatedIndirectly(holder)) {
// When the class is not explicitly marked final, the class could in principle have
// injected subclasses if it is pinned. However, none of the fields are pinned, so we
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InFlowPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InFlowPropagator.java
index 752e59d..ee8e703 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InFlowPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InFlowPropagator.java
@@ -9,6 +9,7 @@
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
import com.android.tools.r8.graph.ProgramField;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.ir.analysis.type.DynamicType;
@@ -50,6 +51,7 @@
final IRConverter converter;
protected final FieldStateCollection fieldStates;
final MethodStateCollectionByReference methodStates;
+ final ImmediateProgramSubtypingInfo immediateSubtypingInfo;
final InFlowComparator inFlowComparator;
public InFlowPropagator(
@@ -58,6 +60,7 @@
IRConverter converter,
FieldStateCollection fieldStates,
MethodStateCollectionByReference methodStates,
+ ImmediateProgramSubtypingInfo immediateSubtypingInfo,
InFlowComparator inFlowComparator) {
this.appView = appView;
this.classesWithSingleCallerInlinedInstanceInitializers =
@@ -65,6 +68,7 @@
this.converter = converter;
this.fieldStates = fieldStates;
this.methodStates = methodStates;
+ this.immediateSubtypingInfo = immediateSubtypingInfo;
this.inFlowComparator = inFlowComparator;
}
@@ -124,7 +128,11 @@
protected DefaultFieldValueJoiner createDefaultFieldValueJoiner(List<FlowGraph> flowGraphs) {
return new DefaultFieldValueJoiner(
- appView, classesWithSingleCallerInlinedInstanceInitializers, fieldStates, flowGraphs);
+ appView,
+ classesWithSingleCallerInlinedInstanceInitializers,
+ fieldStates,
+ flowGraphs,
+ immediateSubtypingInfo);
}
private void processFlowGraphs(List<FlowGraph> flowGraphs, ExecutorService executorService)
diff --git a/src/main/java/com/android/tools/r8/utils/DepthFirstSearchWorkListBase.java b/src/main/java/com/android/tools/r8/utils/DepthFirstSearchWorkListBase.java
index a4d1c8f..48ceda2 100644
--- a/src/main/java/com/android/tools/r8/utils/DepthFirstSearchWorkListBase.java
+++ b/src/main/java/com/android/tools/r8/utils/DepthFirstSearchWorkListBase.java
@@ -255,11 +255,12 @@
protected TraversalContinuation<TB, S> internalOnJoin(DFSNodeWithStateImpl<N, S> node) {
return joiner(
node,
- childStateMap.computeIfAbsent(
+ MapUtils.removeOrComputeDefault(
+ childStateMap,
node,
n -> {
assert false : "Unexpected joining of not visited node";
- return new ArrayList<>();
+ return Collections.emptyList();
}));
}
diff --git a/src/main/java/com/android/tools/r8/utils/MapUtils.java b/src/main/java/com/android/tools/r8/utils/MapUtils.java
index ac5d7f6..99fae2e 100644
--- a/src/main/java/com/android/tools/r8/utils/MapUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/MapUtils.java
@@ -96,6 +96,11 @@
return value != null ? value : defaultValue;
}
+ public static <K, V> V removeOrComputeDefault(Map<K, V> map, K key, Function<K, V> defaultValue) {
+ V value = map.remove(key);
+ return value != null ? value : defaultValue.apply(key);
+ }
+
public static String toString(Map<?, ?> map) {
return StringUtils.join(
",", map.entrySet(), entry -> entry.getKey() + ":" + entry.getValue(), BraceType.TUBORG);
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/DefaultFieldValueAnalysisWithKeptSubclassTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/DefaultFieldValueAnalysisWithKeptSubclassTest.java
index d2e241e..dd9a582 100644
--- a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/DefaultFieldValueAnalysisWithKeptSubclassTest.java
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/DefaultFieldValueAnalysisWithKeptSubclassTest.java
@@ -3,7 +3,6 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.optimize.argumentpropagation;
-import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -65,11 +64,10 @@
inspector -> {
ClassSubject aClassSubject = inspector.clazz(A.class);
assertThat(aClassSubject, isPresent());
- assertThat(aClassSubject.uniqueFieldWithOriginalName("f"), isAbsent());
+ assertThat(aClassSubject.uniqueFieldWithOriginalName("f"), isPresent());
})
- // TODO(b/379034741): Should succeed with expected output.
.run(parameters.getRuntime(), Main.class, B.class.getTypeName())
- .assertSuccessWithEmptyOutput();
+ .assertSuccessWithOutputLines("Hello, world!");
}
static class Main {
diff --git a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedFieldTypeTest.java b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedFieldTypeTest.java
index 693a077..181a85a 100644
--- a/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedFieldTypeTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ifrule/verticalclassmerging/MergedFieldTypeTest.java
@@ -118,7 +118,7 @@
ClassSubject testClassSubject = inspector.clazz(TestClass.class);
assertThat(testClassSubject, isPresent());
- if (enableVerticalClassMerging && parameters.canInitNewInstanceUsingSuperclassConstructor()) {
+ if (enableVerticalClassMerging) {
// Verify that TestClass.field has been removed.
assertEquals(1, testClassSubject.allFields().size());