blob: 01be10c7c4b2fc08e7fd06ab774c446712a631eb [file] [log] [blame]
// Copyright (c) 2022, 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.string;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.optimize.string.StringBuilderAction.AppendWithNewConstantString;
import com.android.tools.r8.ir.optimize.string.StringBuilderAction.RemoveStringBuilderAction;
import com.android.tools.r8.ir.optimize.string.StringBuilderAction.ReplaceByConstantString;
import com.android.tools.r8.ir.optimize.string.StringBuilderNode.AppendNode;
import com.android.tools.r8.ir.optimize.string.StringBuilderNode.ImplicitToStringNode;
import com.android.tools.r8.ir.optimize.string.StringBuilderNode.InitNode;
import com.android.tools.r8.ir.optimize.string.StringBuilderNode.InitOrAppend;
import com.android.tools.r8.ir.optimize.string.StringBuilderNode.StringBuilderInstruction;
import com.android.tools.r8.ir.optimize.string.StringBuilderNode.ToStringNode;
import com.android.tools.r8.utils.WorkList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* StringBuilderNodeMuncher is a classic munching algorithm that will try to remove nodes on string
* builders by looking at small view of it {@link PeepholePattern}. If a pattern can be optimized
* the graph will be updated in place and if the IR needs to be updated, an action will be added.
*/
class StringBuilderNodeMuncher {
static class MunchingState {
private final Map<Instruction, StringBuilderAction> actions;
private final StringBuilderOracle oracle;
// The below state can all be computed by searching the graph but are tracked here to improve
// performance.
private final Set<StringBuilderNode> escaping;
private final Set<StringBuilderNode> inspectingCapacity;
private final Set<StringBuilderNode> looping;
private final Map<StringBuilderNode, Set<StringBuilderNode>> materializingInstructions;
private final Map<Value, String> optimizedStrings = new IdentityHashMap<>();
MunchingState(
Map<Instruction, StringBuilderAction> actions,
Set<StringBuilderNode> escaping,
Set<StringBuilderNode> inspectingCapacity,
Set<StringBuilderNode> looping,
Map<StringBuilderNode, Set<StringBuilderNode>> materializingInstructions,
StringBuilderOracle oracle) {
this.actions = actions;
this.escaping = escaping;
this.inspectingCapacity = inspectingCapacity;
this.looping = looping;
this.materializingInstructions = materializingInstructions;
this.oracle = oracle;
}
}
private interface PeepholePattern {
boolean optimize(
StringBuilderNode root, StringBuilderNode currentNode, MunchingState munchingState);
}
/**
* This peephole will try to optimize two sequential appends:
*
* <pre>
* append("foo") -> append("bar") =>
* append("foobar")
*
* or
*
* init("foo") -> append("bar") =>
* init("foobar")
* </pre>
*/
private static class MunchAppends implements PeepholePattern {
@Override
public boolean optimize(
StringBuilderNode root, StringBuilderNode currentNode, MunchingState munchingState) {
if (!currentNode.isAppendNode()) {
return false;
}
String currentConstantArgument = getConstantArgumentForNode(currentNode, munchingState);
if (currentConstantArgument == null || !currentNode.hasSinglePredecessor()) {
return false;
}
StringBuilderNode previous = currentNode.getSinglePredecessor();
String previousConstantArgument = getConstantArgumentForNode(previous, munchingState);
if (previousConstantArgument == null || !previous.hasSingleSuccessor()) {
return false;
}
// The capacity changes based on the init call (on JVM it adds 16 to length of input).
if (previous.isInitNode() && munchingState.inspectingCapacity.contains(root)) {
return false;
}
assert previous.isInitOrAppend();
String newConstant = previousConstantArgument + currentConstantArgument;
InitOrAppend initOrAppend = previous.asInitOrAppend();
initOrAppend.setConstantArgument(newConstant);
munchingState.actions.put(
initOrAppend.getInstruction(), new AppendWithNewConstantString(newConstant));
munchingState.actions.put(
currentNode.asAppendNode().getInstruction(), RemoveStringBuilderAction.getInstance());
currentNode.removeNode();
return true;
}
}
/**
* This peephole will try to remove toString nodes and replace by a constant string:
*
* <pre>
* newInstance -> init("foo") -> append("bar") -> toString() =>
* newInstance -> init("foo") -> append("bar")
* </pre>
*
* <p>If the node is an implicitToString, we update the append of another builder to have the new
* constant value directly. If not, we keep track of the outValue toString() had is replaced by a
* constant, by updating {@code MunchingState.optimizedStrings}
*/
private static class MunchToString implements PeepholePattern {
@Override
public boolean optimize(
StringBuilderNode originalRoot,
StringBuilderNode currentNode,
MunchingState munchingState) {
if (!currentNode.isToStringNode() && !currentNode.isImplicitToStringNode()) {
return false;
}
StringBuilderNode root = findFirstNonSentinelRoot(originalRoot);
if (!root.isNewInstanceNode() || !root.hasSingleSuccessor()) {
return false;
}
StringBuilderNode init = root.getSingleSuccessor();
String rootConstantArgument = getConstantArgumentForNode(init, munchingState);
if (rootConstantArgument == null || !init.isInitNode()) {
return false;
}
// This is either <init>(str) -> toString() or <init>(str) -> append(str) -> toString()
// If the string builder dependency is directly given to another string builder, there is
// no toString() but an append with this string builder as argument.
if (!currentNode.hasSinglePredecessor() || !init.hasSingleSuccessor()) {
return false;
}
String constantArgument = null;
if (currentNode.getSinglePredecessor() == init) {
constantArgument = rootConstantArgument;
} else {
StringBuilderNode expectedAppend = init.getSingleSuccessor();
StringBuilderNode expectedSameAppend = currentNode.getSinglePredecessor();
String appendConstantArgument = getConstantArgumentForNode(expectedAppend, munchingState);
if (expectedAppend == expectedSameAppend && appendConstantArgument != null) {
// TODO(b/190489514): See if this larger pattern is necessary.
assert false : "See why this larger pattern is necessary";
constantArgument = rootConstantArgument + appendConstantArgument;
}
}
if (constantArgument == null) {
return false;
}
if (currentNode.isToStringNode()) {
ToStringNode toStringNode = currentNode.asToStringNode();
munchingState.actions.put(
toStringNode.getInstruction(), new ReplaceByConstantString(constantArgument));
munchingState.materializingInstructions.get(originalRoot).remove(currentNode);
String oldValue =
munchingState.optimizedStrings.put(
toStringNode.getInstruction().outValue(), constantArgument);
assert oldValue == null;
} else {
assert currentNode.isImplicitToStringNode();
ImplicitToStringNode implicitToStringNode = currentNode.asImplicitToStringNode();
InitOrAppend initOrAppend = implicitToStringNode.getInitOrAppend();
initOrAppend.setConstantArgument(constantArgument);
munchingState.actions.put(
initOrAppend.getInstruction(), new AppendWithNewConstantString(constantArgument));
}
currentNode.removeNode();
return true;
}
}
/**
* Find the first non split reference node or loop-node, which are nodes inserted to track
* control-flow.
*/
private static StringBuilderNode findFirstNonSentinelRoot(StringBuilderNode root) {
WorkList<StringBuilderNode> workList = WorkList.newIdentityWorkList(root);
while (workList.hasNext()) {
StringBuilderNode node = workList.next();
if (!node.isSplitReferenceNode() && !node.isLoopNode()) {
return node;
}
if (node.hasSingleSuccessor()) {
workList.addIfNotSeen(node.getSingleSuccessor());
}
}
return root;
}
private static String getConstantArgumentForNode(
StringBuilderNode node, MunchingState munchingState) {
if (node.isAppendNode()) {
AppendNode appendNode = node.asAppendNode();
if (appendNode.hasConstantArgument()) {
return appendNode.getConstantArgument();
}
return getOptimizedConstantArgument(appendNode, munchingState);
} else if (node.isInitNode()) {
InitNode initNode = node.asInitNode();
if (initNode.hasConstantArgument()) {
return initNode.getConstantArgument();
}
return getOptimizedConstantArgument(initNode, munchingState);
}
return null;
}
private static String getOptimizedConstantArgument(
StringBuilderInstruction node, MunchingState munchingState) {
List<Value> inValues = node.getInstruction().inValues();
if (inValues.size() != 2) {
return null;
}
return munchingState.optimizedStrings.get(inValues.get(1).getAliasedValue());
}
/**
* This pattern tries to remove nodes that are no longer needed. An example is when a toString is
* materialized, and append may now not be observable and can be removed. This pattern tries to
* remove as many nodes as possibly by continuing to remove predecessor nodes. This is not
* necessary for correctness but since the overall munching algorithm runs over successors,
* removing predecessors directly here saves cycles.
*/
private static class MunchNonMaterializing implements PeepholePattern {
@Override
public boolean optimize(
StringBuilderNode root, StringBuilderNode currentNode, MunchingState munchingState) {
boolean removedAnyNodes = false;
boolean removeNode;
boolean isEscaping = munchingState.escaping.contains(root);
while (currentNode != null) {
// Remove appends if the string builder do not escape, is not inspected or materialized
// and if it is not part of a loop.
removeNode = false;
if (currentNode.isSplitReferenceNode()) {
removeNode = currentNode.getSuccessors().isEmpty() || currentNode.hasSinglePredecessor();
} else if (currentNode.isAppendNode() && !isEscaping) {
AppendNode appendNode = currentNode.asAppendNode();
boolean canRemoveIfNoInspectionOrMaterializing =
!munchingState.inspectingCapacity.contains(root)
&& munchingState.materializingInstructions.get(root).isEmpty();
boolean canRemoveIfLastAndNoLoop =
!isLoopingOnPath(root, currentNode, munchingState)
&& currentNode.getSuccessors().isEmpty();
if (canRemoveIfNoInspectionOrMaterializing
&& canRemoveIfLastAndNoLoop
&& munchingState.oracle.canObserveStringBuilderCall(
currentNode.asAppendNode().getInstruction())) {
munchingState.actions.put(
appendNode.getInstruction(), RemoveStringBuilderAction.getInstance());
removeNode = true;
}
} else if (currentNode.isInitNode()
&& currentNode.asInitNode().hasConstantArgument()
&& currentNode.hasSinglePredecessor()
&& currentNode.getSinglePredecessor().isNewInstanceNode()
&& currentNode.getSuccessors().isEmpty()
&& !isEscaping
&& !munchingState.oracle.canObserveStringBuilderCall(
currentNode.asInitNode().getInstruction())) {
removeNode = true;
}
if (!removeNode) {
return false;
} else {
removedAnyNodes = true;
currentNode.removeNode();
if (currentNode.isStringBuilderInstructionNode()) {
munchingState.actions.put(
currentNode.asStringBuilderInstructionNode().getInstruction(),
RemoveStringBuilderAction.getInstance());
}
currentNode =
currentNode.hasSinglePredecessor() ? currentNode.getSinglePredecessor() : null;
}
}
return removedAnyNodes;
}
private boolean isLoopingOnPath(
StringBuilderNode root, StringBuilderNode currentNode, MunchingState munchingState) {
if (!munchingState.looping.contains(root)) {
return false;
}
WorkList<StringBuilderNode> workList = WorkList.newIdentityWorkList(currentNode);
boolean seenNewInstance = false;
while (workList.hasNext()) {
StringBuilderNode next = workList.next();
if (next.isNewInstanceNode()) {
seenNewInstance = true;
}
if (next.isLoopNode()) {
// If we have seen a new instance and there is only a single successor for the loop
// the instance is always created inside the body.
return !seenNewInstance || !next.hasSingleSuccessor();
}
workList.addIfNotSeen(next.getPredecessors());
}
return false;
}
}
private static final PeepholePattern[] peepholePatterns =
new PeepholePattern[] {new MunchAppends(), new MunchToString(), new MunchNonMaterializing()};
static boolean optimize(
StringBuilderNode root, StringBuilderNode currentNode, MunchingState munchingState) {
if (currentNode.isDead()) {
return false;
}
for (PeepholePattern peepholePattern : peepholePatterns) {
if (peepholePattern.optimize(root, currentNode, munchingState)) {
return true;
}
}
return false;
}
}