blob: 5cdcc6b1e86e7689354742f5336ad318a34bd70b [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.ReplaceArgumentByExistingString;
import com.android.tools.r8.ir.optimize.string.StringBuilderAction.ReplaceArgumentByStringConcat;
import com.android.tools.r8.ir.optimize.string.StringBuilderAction.ReplaceByConstantString;
import com.android.tools.r8.ir.optimize.string.StringBuilderAction.ReplaceByExistingString;
import com.android.tools.r8.ir.optimize.string.StringBuilderAction.ReplaceByStringConcat;
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.InitOrAppendNode;
import com.android.tools.r8.ir.optimize.string.StringBuilderNode.NewInstanceNode;
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 com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
/**
* 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<StringBuilderNode, NewInstanceNode> newInstances;
private final Map<Value, String> optimizedStrings = new IdentityHashMap<>();
private final Supplier<Value> newValueSupplier;
MunchingState(
Map<Instruction, StringBuilderAction> actions,
Set<StringBuilderNode> escaping,
Set<StringBuilderNode> inspectingCapacity,
Set<StringBuilderNode> looping,
Map<StringBuilderNode, Set<StringBuilderNode>> materializingInstructions,
Map<StringBuilderNode, NewInstanceNode> newInstances,
StringBuilderOracle oracle,
Supplier<Value> newValueSupplier) {
this.actions = actions;
this.escaping = escaping;
this.inspectingCapacity = inspectingCapacity;
this.looping = looping;
this.materializingInstructions = materializingInstructions;
this.newInstances = newInstances;
this.oracle = oracle;
this.newValueSupplier = newValueSupplier;
}
public NewInstanceNode getNewInstanceNode(StringBuilderNode root) {
return newInstances.get(root);
}
public boolean isLooping(StringBuilderNode root) {
return looping.contains(root);
}
public boolean isEscaping(StringBuilderNode root) {
return escaping.contains(root);
}
public boolean isInspecting(StringBuilderNode root) {
return inspectingCapacity.contains(root);
}
public Value getNewOutValue() {
return newValueSupplier.get();
}
}
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) {
AppendNode appendNode = currentNode.asAppendNode();
if (appendNode == null || !appendNode.hasSinglePredecessor()) {
return false;
}
InitOrAppendNode previous = currentNode.getSinglePredecessor().asInitOrAppend();
if (previous == 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;
}
String currentConstantArgument = getConstantArgumentForNode(appendNode, munchingState);
if (currentConstantArgument == null) {
return false;
}
String previousConstantArgument = getConstantArgumentForNode(previous, munchingState);
if (previousConstantArgument == null) {
return false;
}
String newConstant = previousConstantArgument + currentConstantArgument;
previous.setConstantArgument(newConstant);
munchingState.actions.put(
previous.getInstruction(), new AppendWithNewConstantString(newConstant));
munchingState.actions.put(
appendNode.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") -> toString() => newInstance -> init("foo") -> append("bar")
* actions: [toString() => ReplaceByConstantString("foo")]
* </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() being 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 (munchingState.isEscaping(originalRoot) || munchingState.isInspecting(originalRoot)) {
return false;
}
if (!currentNode.isToStringNode() && !currentNode.isImplicitToStringNode()) {
return false;
}
NewInstanceNode newInstanceNode = munchingState.getNewInstanceNode(originalRoot);
if (newInstanceNode == null || !newInstanceNode.hasSingleSuccessor()) {
return false;
}
InitNode init = newInstanceNode.getSingleSuccessor().asInitNode();
if (init == null || !init.hasSingleSuccessor()) {
return false;
}
if (!currentNode.hasSinglePredecessor() || currentNode.getSinglePredecessor() != init) {
return false;
}
String initConstantArgument = getConstantArgumentForNode(init, munchingState);
if (initConstantArgument == null) {
return false;
}
// 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.isToStringNode()) {
ToStringNode toStringNode = currentNode.asToStringNode();
munchingState.actions.put(
toStringNode.getInstruction(), new ReplaceByConstantString(initConstantArgument));
String oldValue =
munchingState.optimizedStrings.put(
toStringNode.getInstruction().outValue(), initConstantArgument);
assert oldValue == null;
} else {
assert currentNode.isImplicitToStringNode();
ImplicitToStringNode implicitToStringNode = currentNode.asImplicitToStringNode();
InitOrAppendNode initOrAppend = implicitToStringNode.getInitOrAppend();
initOrAppend.setConstantArgument(initConstantArgument);
munchingState.actions.put(
initOrAppend.getInstruction(), new AppendWithNewConstantString(initConstantArgument));
}
munchingState.materializingInstructions.get(originalRoot).remove(currentNode);
currentNode.removeNode();
return true;
}
}
/**
* This peephole will try to remove toString nodes and replace by an invoke to String.concat:
*
* <pre>
* newInstance -> init(notNull(string)) -> append(notNull(otherString)) -> toString() =>
* newInstance -> init(notNull(string)) -> append(otherString)
* actions: [toString() => string.concat(otherString)]
* </pre>
*
* <p>This pattern only triggers when a constant munching of toString could happen.
*/
private static class MunchToStringIntoStringConcat implements PeepholePattern {
@Override
public boolean optimize(
StringBuilderNode originalRoot,
StringBuilderNode currentNode,
MunchingState munchingState) {
if (munchingState.isEscaping(originalRoot)
|| munchingState.isInspecting(originalRoot)
|| !currentNode.hasSinglePredecessor()) {
return false;
}
if (!currentNode.isToStringNode() && !currentNode.isImplicitToStringNode()) {
return false;
}
NewInstanceNode newInstanceNode = munchingState.getNewInstanceNode(originalRoot);
if (newInstanceNode == null || !newInstanceNode.hasSingleSuccessor()) {
return false;
}
InitOrAppendNode firstNode = newInstanceNode.getSingleSuccessor().asInitNode();
if (firstNode == null || !firstNode.hasSingleSuccessor()) {
return false;
}
if (firstNode.asInitNode().isConstructorInvokeSideEffectFree(munchingState.oracle)
&& "".equals(firstNode.getConstantArgument())
&& firstNode.hasSingleSuccessor()) {
firstNode = firstNode.getSingleSuccessor().asAppendNode();
if (firstNode == null
|| !firstNode.hasSinglePredecessor()
|| !firstNode.hasSingleSuccessor()) {
return false;
}
}
// We cannot String.concat or return the string safely when it is not constant and maybe null.
if (!firstNode.hasConstantOrNonConstantArgument()) {
return false;
}
List<InitOrAppendNode> initOrAppends = Lists.newArrayList(firstNode);
if (currentNode.getSinglePredecessor() != firstNode) {
AppendNode appendAfterFirstNode = firstNode.getSingleSuccessor().asAppendNode();
AppendNode appendBeforeToString = currentNode.getSinglePredecessor().asAppendNode();
if (appendAfterFirstNode == null
|| appendAfterFirstNode != appendBeforeToString
|| !appendAfterFirstNode.hasConstantOrNonConstantArgument()) {
return false;
}
initOrAppends.add(appendAfterFirstNode);
}
// Check that all values are not constant otherwise we can compute the constant value and
// replace all entirely.
if (Iterables.all(initOrAppends, InitOrAppendNode::hasConstantArgument)) {
return false;
}
InitOrAppendNode first = initOrAppends.get(0);
// 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.isToStringNode()) {
Instruction currentInstruction = currentNode.asToStringNode().getInstruction();
if (initOrAppends.size() == 1) {
// Replace with the string itself.
munchingState.actions.put(
currentInstruction, new ReplaceByExistingString(first.getNonConstantArgument()));
} else {
InitOrAppendNode second = initOrAppends.get(1);
ReplaceByStringConcat concatAction;
if (first.hasConstantArgument()) {
concatAction =
ReplaceByStringConcat.replaceByNewConstantConcatValue(
first.getConstantArgument(), second.getNonConstantArgument());
} else if (second.hasConstantArgument()) {
concatAction =
ReplaceByStringConcat.replaceByValueConcatNewConstant(
first.getNonConstantArgument(), second.getConstantArgument());
} else {
concatAction =
ReplaceByStringConcat.replaceByValues(
first.getNonConstantArgument(), second.getNonConstantArgument());
}
munchingState.actions.put(currentInstruction, concatAction);
}
} else {
assert currentNode.isImplicitToStringNode();
ImplicitToStringNode implicitToStringNode = currentNode.asImplicitToStringNode();
InitOrAppendNode initOrAppend = implicitToStringNode.getInitOrAppend();
if (initOrAppends.size() == 1) {
initOrAppend.setNonConstantArgument(first.getNonConstantArgument());
munchingState.actions.put(
initOrAppend.getInstruction(),
new ReplaceArgumentByExistingString(first.getNonConstantArgument()));
} else {
// Changing append to String.concat require us to calculate a new string value that will
// be the result. We allocate it here such that we can use it repeatedly in munching.
InitOrAppendNode second = initOrAppends.get(1);
Value newOutValue = munchingState.getNewOutValue();
initOrAppend.setNonConstantArgument(newOutValue);
ReplaceArgumentByStringConcat concatAction;
if (first.hasConstantArgument()) {
concatAction =
ReplaceArgumentByStringConcat.replaceByNewConstantConcatValue(
first.getConstantArgument(), second.getNonConstantArgument(), newOutValue);
} else if (second.hasConstantArgument()) {
concatAction =
ReplaceArgumentByStringConcat.replaceByValueConcatNewConstant(
first.getNonConstantArgument(), second.getConstantArgument(), newOutValue);
} else {
concatAction =
ReplaceArgumentByStringConcat.replaceByValues(
first.getNonConstantArgument(), second.getNonConstantArgument(), newOutValue);
}
munchingState.actions.put(initOrAppend.getInstruction(), concatAction);
}
}
munchingState.materializingInstructions.get(originalRoot).remove(currentNode);
currentNode.removeNode();
return true;
}
}
private static String getConstantArgumentForNode(
InitOrAppendNode node, MunchingState munchingState) {
if (node.hasConstantArgument()) {
return node.getConstantArgument();
}
return getOptimizedConstantArgument(node, munchingState);
}
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() && !munchingState.isLooping(root)) {
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();
boolean hasKnownArgumentOrCannotBeObserved =
appendNode.hasConstantOrNonConstantArgument()
|| !munchingState.oracle.canObserveStringBuilderCall(
currentNode.asAppendNode().getInstruction());
if (canRemoveIfNoInspectionOrMaterializing
&& canRemoveIfLastAndNoLoop
&& hasKnownArgumentOrCannotBeObserved) {
removeNode = true;
}
} else if (currentNode.isInitNode()
&& 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()) {
Instruction currentInstruction =
currentNode.asStringBuilderInstructionNode().getInstruction();
StringBuilderAction currentAction = munchingState.actions.get(currentInstruction);
if (currentAction != null
&& !currentAction.isAllowedToBeOverwrittenByRemoveStringBuilderAction()) {
assert currentAction.isReplaceArgumentByStringConcat();
currentAction.asReplaceArgumentByStringConcat().setRemoveInstruction();
} else {
munchingState.actions.put(
currentInstruction, 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 MunchToStringIntoStringConcat(),
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;
}
}