Fix repeated analysis of values in dead code removal
Bug: b/267990059
Change-Id: I9deebf700e936ad81a8fb25d067acc04637d980d
diff --git a/src/main/java/com/android/tools/r8/ir/code/ArrayPut.java b/src/main/java/com/android/tools/r8/ir/code/ArrayPut.java
index 13f8213..c521bf5 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ArrayPut.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ArrayPut.java
@@ -23,6 +23,7 @@
import com.android.tools.r8.ir.conversion.DexBuilder;
import com.android.tools.r8.ir.conversion.MethodConversionOptions;
import com.android.tools.r8.ir.conversion.TypeConstraintResolver;
+import com.android.tools.r8.ir.optimize.DeadCodeRemover.DeadInstructionResult;
import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
import com.android.tools.r8.ir.optimize.InliningConstraints;
import com.android.tools.r8.ir.regalloc.RegisterAllocator;
@@ -117,8 +118,7 @@
}
@Override
- public boolean instructionMayHaveSideEffects(
- AppView<?> appView, ProgramMethod context, SideEffectAssumption assumption) {
+ public boolean instructionInstanceCanThrow(AppView<?> appView, ProgramMethod context) {
// In debug mode, ArrayPut has a side-effect on the locals.
if (appView.options().debug) {
return true;
@@ -158,29 +158,24 @@
if (!valueType.lessThanOrEqualUpToNullability(memberType, appView)) {
return true;
}
-
- if (array.hasPhiUsers()) {
- // The array could be used indirectly.
- return true;
- }
-
- // Check that all usages of the array are array stores.
- for (Instruction user : array.aliasedUsers()) {
- if (user.isAssume()) {
- if (user.outValue().hasPhiUsers()) {
- return true;
- }
- continue;
- }
- if (!user.isArrayPut() || user.asArrayPut().array().getAliasedValue() != array) {
- return true;
- }
- }
-
return false;
}
@Override
+ public boolean instructionMayHaveSideEffects(
+ AppView<?> appView, ProgramMethod context, SideEffectAssumption assumption) {
+ // This modifies the array (or throws).
+ return true;
+ }
+
+ @Override
+ public DeadInstructionResult canBeDeadCode(AppView<?> appView, IRCode code) {
+ return instructionInstanceCanThrow(appView, code.context())
+ ? DeadInstructionResult.notDead()
+ : DeadInstructionResult.deadIfInValueIsDead(array());
+ }
+
+ @Override
public boolean identicalAfterRegisterAllocation(
Instruction other, RegisterAllocator allocator, MethodConversionOptions conversionOptions) {
// We cannot share ArrayPut instructions without knowledge of the type of the array input.
diff --git a/src/main/java/com/android/tools/r8/ir/code/ValueIsDeadAnalysis.java b/src/main/java/com/android/tools/r8/ir/code/ValueIsDeadAnalysis.java
index 14124c1..2bcbae6 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ValueIsDeadAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ValueIsDeadAnalysis.java
@@ -4,19 +4,41 @@
package com.android.tools.r8.ir.code;
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.ir.optimize.DeadCodeRemover.DeadInstructionResult;
+import com.android.tools.r8.utils.MapUtils;
import com.android.tools.r8.utils.WorkList;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import java.util.Collections;
+import java.util.IdentityHashMap;
import java.util.LinkedHashSet;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
public class ValueIsDeadAnalysis {
+ private enum ValueIsDeadResult {
+ DEAD,
+ NOT_DEAD;
+
+ boolean isDead() {
+ return this == DEAD;
+ }
+
+ boolean isNotDead() {
+ return this == NOT_DEAD;
+ }
+ }
+
private final AppView<?> appView;
private final IRCode code;
+ private final Map<Value, ValueIsDeadResult> analysisCache = new IdentityHashMap<>();
+
public ValueIsDeadAnalysis(AppView<?> appView, IRCode code) {
this.appView = appView;
this.code = code;
@@ -27,9 +49,24 @@
if (value.isUnused()) {
return true;
}
+ // Create an is-dead dependence graph. If the deadness of value v depends on value u being dead,
+ // a directed edge [v -> u] is added to the graph.
+ //
+ // This graph serves two purposes:
+ // 1) If the analysis finds that `u` is *not* dead, then using the graph we can find all the
+ // values whose deadness depend (directly or indirectly) on `u` being dead, and mark them as
+ // being *not* dead in the analysis cache.
+ // 2) If the analysis finds that `u` *is* dead, we can remove the node from the dependence graph
+ // (as it is necessarily a leaf), and repeatedly mark direct and indirect predecessors of `u`
+ // that have now become leaves as being dead in the analysis cache.
WorkList<Value> worklist = WorkList.newIdentityWorkList(value);
Value notDeadWitness = findNotDeadWitness(worklist);
boolean isDead = Objects.isNull(notDeadWitness);
+ if (isDead) {
+ for (Value deadValue : worklist.getSeenSet()) {
+ recordValueIsDead(deadValue);
+ }
+ }
return isDead;
}
@@ -38,20 +75,31 @@
}
private Value findNotDeadWitness(WorkList<Value> worklist) {
+ DependenceGraph dependenceGraph = new DependenceGraph();
while (worklist.hasNext()) {
Value value = worklist.next();
- // Give up when the dependent set of values reach a given threshold (otherwise this fails with
- // a StackOverflowError on Art003_omnibus_opcodesTest).
- // TODO(b/267990059): Remove this bail-out when the analysis is linear in the size of the code
- // object.
- if (worklist.getSeenSet().size() > 100) {
- return value;
+ // The first time we visit a value we have not yet added any outgoing edges to the dependence
+ // graph.
+ assert dependenceGraph.isLeaf(value);
+
+ // Lookup if we have already analyzed the deadness of this value.
+ ValueIsDeadResult cacheResult = analysisCache.get(value);
+ if (cacheResult != null) {
+ // If it is dead, then continue the search for a non-dead dependent. Otherwise this value is
+ // a witness that the analysis failed.
+ if (cacheResult.isDead()) {
+ continue;
+ } else {
+ recordDependentsAreNotDead(value, dependenceGraph);
+ return value;
+ }
}
// If the value has debug users we cannot eliminate it since it represents a value in a local
// variable that should be visible in the debugger.
if (value.hasDebugUsers()) {
+ recordValueAndDependentsAreNotDead(value, dependenceGraph);
return value;
}
@@ -59,6 +107,7 @@
for (Instruction instruction : value.uniqueUsers()) {
DeadInstructionResult result = instruction.canBeDeadCode(appView, code);
if (result.isNotDead()) {
+ recordValueAndDependentsAreNotDead(value, dependenceGraph);
return value;
}
if (result.isMaybeDead()) {
@@ -69,9 +118,123 @@
}
}
- // Continue the analysis of the dependents.
- worklist.addIfNotSeen(valuesRequiredToBeDead);
+ // No need to record that the deadness of a values relies on its own removal, nor known to be
+ // dead values.
+ valuesRequiredToBeDead.remove(value);
+ valuesRequiredToBeDead.removeIf(
+ valueRequiredToBeDead -> analysisCache.get(value) == ValueIsDeadResult.DEAD);
+
+ if (valuesRequiredToBeDead.isEmpty()) {
+ // We have now proven that this value is dead.
+ recordValueIsDeadAndPropagateToDependents(value, dependenceGraph);
+ } else {
+ // Record the current value as a dependent of each value required to be dead.
+ for (Value valueRequiredToBeDead : valuesRequiredToBeDead) {
+ dependenceGraph.addDependenceEdge(value, valueRequiredToBeDead);
+ }
+
+ // Continue the analysis of the dependents.
+ worklist.addIfNotSeen(valuesRequiredToBeDead);
+ }
}
return null;
}
+
+ private void recordValueIsDeadAndPropagateToDependents(
+ Value value, DependenceGraph dependenceGraph) {
+ WorkList<Value> worklist = WorkList.newIdentityWorkList(value);
+ while (worklist.hasNext()) {
+ Value current = worklist.next();
+ recordValueIsDead(current);
+
+ // This value is now proven to be dead, thus there is no need to keep track of its successors.
+ dependenceGraph.unlinkSuccessors(current);
+
+ // Continue processing of new leaves.
+ for (Value dependent : dependenceGraph.removeLeaf(current)) {
+ if (dependenceGraph.isLeaf(dependent)) {
+ worklist.addIfNotSeen(dependent);
+ }
+ }
+ }
+ }
+
+ private void recordValueIsDead(Value value) {
+ ValueIsDeadResult existingResult = analysisCache.put(value, ValueIsDeadResult.DEAD);
+ assert existingResult == null || existingResult == ValueIsDeadResult.DEAD;
+ }
+
+ private void recordValueAndDependentsAreNotDead(Value value, DependenceGraph dependenceGraph) {
+ recordValueIsNotDead(value, dependenceGraph);
+ recordDependentsAreNotDead(value, dependenceGraph);
+ }
+
+ private void recordValueIsNotDead(Value value, DependenceGraph dependenceGraph) {
+ // This value is now proven to be dead, thus there is no need to keep track of its successors.
+ dependenceGraph.unlinkSuccessors(value);
+ ValueIsDeadResult existingResult = analysisCache.put(value, ValueIsDeadResult.NOT_DEAD);
+ assert existingResult == null || existingResult.isNotDead();
+ }
+
+ private void recordDependentsAreNotDead(Value value, DependenceGraph dependenceGraph) {
+ WorkList<Value> worklist = WorkList.newIdentityWorkList(value);
+ while (worklist.hasNext()) {
+ Value current = worklist.next();
+ for (Value dependent : dependenceGraph.removeLeaf(current)) {
+ recordValueIsNotDead(dependent, dependenceGraph);
+ worklist.addIfNotSeen(dependent);
+ }
+ }
+ }
+
+ private static class DependenceGraph {
+
+ private final Map<Value, Set<Value>> successors = new IdentityHashMap<>();
+ private final Map<Value, Set<Value>> predecessors = new IdentityHashMap<>();
+
+ /**
+ * Records that the removal of {@param value} depends on the removal of {@param
+ * valueRequiredToBeDead} by adding an edge from {@param value} to {@param
+ * valueRequiredToBeDead} in this graph.
+ */
+ void addDependenceEdge(Value value, Value valueRequiredToBeDead) {
+ successors
+ .computeIfAbsent(value, ignoreKey(Sets::newIdentityHashSet))
+ .add(valueRequiredToBeDead);
+ predecessors
+ .computeIfAbsent(valueRequiredToBeDead, ignoreKey(Sets::newIdentityHashSet))
+ .add(value);
+ }
+
+ Set<Value> removeLeaf(Value value) {
+ assert isLeaf(value);
+ Set<Value> dependents = MapUtils.removeOrDefault(predecessors, value, Collections.emptySet());
+ for (Value dependent : dependents) {
+ Set<Value> dependentSuccessors = successors.get(dependent);
+ boolean removed = dependentSuccessors.remove(value);
+ assert removed;
+ if (dependentSuccessors.isEmpty()) {
+ successors.remove(dependent);
+ }
+ }
+ return dependents;
+ }
+
+ void unlinkSuccessors(Value value) {
+ Set<Value> valueSuccessors =
+ MapUtils.removeOrDefault(successors, value, Collections.emptySet());
+ for (Value successor : valueSuccessors) {
+ Set<Value> successorPredecessors = predecessors.get(successor);
+ boolean removed = successorPredecessors.remove(value);
+ assert removed;
+ if (successorPredecessors.isEmpty()) {
+ predecessors.remove(successor);
+ }
+ }
+ }
+
+ boolean isLeaf(Value value) {
+ return !successors.containsKey(value);
+ }
+ }
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java b/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
index 16cd748..dce99b1 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
@@ -22,9 +22,9 @@
import com.android.tools.r8.ir.code.ValueIsDeadAnalysis;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.utils.Box;
+import com.android.tools.r8.utils.IterableUtils;
import com.android.tools.r8.utils.Timing;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterators;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
@@ -299,21 +299,12 @@
}
@Override
- public boolean isDeadIfInValueIsDead() {
- return true;
- }
-
- @Override
public Iterable<Value> getValuesRequiredToBeDead() {
- return () -> Iterators.singletonIterator(inValueRequiredToBeDead);
+ return IterableUtils.singleton(inValueRequiredToBeDead);
}
};
}
- public boolean isDeadIfInValueIsDead() {
- return false;
- }
-
public boolean isDeadIfOutValueIsDead() {
return false;
}
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 94b9bfe..e0a915f 100644
--- a/src/main/java/com/android/tools/r8/utils/MapUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/MapUtils.java
@@ -61,6 +61,11 @@
map.entrySet().removeIf(entry -> predicate.test(entry.getKey(), entry.getValue()));
}
+ public static <K, V> V removeOrDefault(Map<K, V> map, K key, V defaultValue) {
+ V value = map.remove(key);
+ return value != null ? value : defaultValue;
+ }
+
public static String toString(Map<?, ?> map) {
return StringUtils.join(
",", map.entrySet(), entry -> entry.getKey() + ":" + entry.getValue(), BraceType.TUBORG);