blob: 1179c6d855eec5502a3a35bb39bc92940b5fc518 [file] [log] [blame]
// Copyright (c) 2020, 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.graph.DexMethod;
import com.android.tools.r8.ir.analysis.framework.intraprocedural.AbstractState;
import com.android.tools.r8.ir.analysis.framework.intraprocedural.AbstractTransferFunction;
import com.android.tools.r8.ir.analysis.framework.intraprocedural.DataflowAnalysisResult;
import com.android.tools.r8.ir.analysis.framework.intraprocedural.FailedTransferFunctionResult;
import com.android.tools.r8.ir.analysis.framework.intraprocedural.IntraproceduralDataflowAnalysis;
import com.android.tools.r8.ir.analysis.framework.intraprocedural.TransferFunctionResult;
import com.android.tools.r8.ir.code.BasicBlock;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InvokeMethod;
import com.android.tools.r8.ir.code.InvokeVirtual;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.utils.SetUtils;
import com.google.common.collect.Sets;
import java.util.Set;
/**
* This defines a simple program analysis that determines if there is a path from a call to append()
* on a StringBuilder back to itself in the control flow graph.
*
* <p>The analysis explicitly allows paths from a call to append() back to itself that go through a
* call to toString() on the builder. This ensures that we can still optimize builders that are
* fully enclosed in a loop: <code>
* while (true) {
* System.out.println(new StringBuilder().append("42").toString());
* }
* </code>
*/
class StringBuilderAppendFlowAnalysis {
/**
* Returns true if there is a call to {@code append()} on {@param builder}, which is inside a
* loop.
*/
static boolean hasAppendInstructionInLoop(
IRCode code, Value builder, StringBuilderOptimizationConfiguration configuration) {
IntraproceduralDataflowAnalysis<AbstractStateImpl> analysis =
new IntraproceduralDataflowAnalysis<>(
AbstractStateImpl.bottom(), code, new TransferFunction(builder, configuration));
DataflowAnalysisResult result = analysis.run(builder.definition.getBlock());
return result.isFailedAnalysisResult();
}
/** This defines the state that we keep track of for each {@link BasicBlock}. */
private static class AbstractStateImpl extends AbstractState<AbstractStateImpl> {
private static final AbstractStateImpl BOTTOM = new AbstractStateImpl();
// The set of invoke instructions that call append(), which is on a path to the current program
// point.
private final Set<InvokeVirtual> liveAppendInstructions;
private AbstractStateImpl() {
this(Sets.newIdentityHashSet());
}
private AbstractStateImpl(Set<InvokeVirtual> liveAppendInstructions) {
this.liveAppendInstructions = liveAppendInstructions;
}
public static AbstractStateImpl bottom() {
return BOTTOM;
}
private AbstractStateImpl addLiveAppendInstruction(InvokeVirtual invoke) {
Set<InvokeVirtual> newLiveAppendInstructions =
SetUtils.newIdentityHashSet(liveAppendInstructions);
newLiveAppendInstructions.add(invoke);
return new AbstractStateImpl(newLiveAppendInstructions);
}
private boolean isAppendInstructionLive(InvokeVirtual invoke) {
return liveAppendInstructions.contains(invoke);
}
@Override
public AbstractStateImpl asAbstractState() {
return this;
}
@Override
public AbstractStateImpl join(AbstractStateImpl state) {
if (liveAppendInstructions.isEmpty()) {
return state;
}
if (state.liveAppendInstructions.isEmpty()) {
return this;
}
Set<InvokeVirtual> newLiveAppendInstructions =
SetUtils.newIdentityHashSet(liveAppendInstructions, state.liveAppendInstructions);
return new AbstractStateImpl(newLiveAppendInstructions);
}
@Override
public boolean equals(Object other) {
if (other == null || getClass() != other.getClass()) {
return false;
}
AbstractStateImpl state = (AbstractStateImpl) other;
return liveAppendInstructions.equals(state.liveAppendInstructions);
}
@Override
public int hashCode() {
return liveAppendInstructions.hashCode();
}
}
/**
* This defines the transfer function for the analysis.
*
* <p>If a call to {@code append()} on the builder is seen, then that invoke instruction is added
* to the abstract state.
*
* <p>If a call to {@code toString()} on the builder i seen, then the abstract state is reset to
* bottom.
*/
private static class TransferFunction
implements AbstractTransferFunction<BasicBlock, Instruction, AbstractStateImpl> {
private final Value builder;
private final StringBuilderOptimizationConfiguration configuration;
private TransferFunction(Value builder, StringBuilderOptimizationConfiguration configuration) {
this.builder = builder;
this.configuration = configuration;
}
@Override
public TransferFunctionResult<AbstractStateImpl> apply(
Instruction instruction, AbstractStateImpl state) {
if (instruction.isInvokeMethod()) {
return apply(state, instruction.asInvokeMethod());
}
return state;
}
private TransferFunctionResult<AbstractStateImpl> apply(
AbstractStateImpl state, InvokeMethod invoke) {
if (isAppendOnBuilder(invoke)) {
assert invoke.isInvokeVirtual();
InvokeVirtual appendInvoke = invoke.asInvokeVirtual();
if (state.isAppendInstructionLive(appendInvoke)) {
return new FailedTransferFunctionResult<>();
}
return state.addLiveAppendInstruction(appendInvoke);
}
if (isToStringOnBuilder(invoke)) {
return AbstractStateImpl.bottom();
}
return state;
}
private boolean isAppendOnBuilder(InvokeMethod invoke) {
DexMethod invokedMethod = invoke.getInvokedMethod();
return configuration.isAppendMethod(invokedMethod)
&& invoke.getArgument(0).getAliasedValue() == builder;
}
private boolean isToStringOnBuilder(InvokeMethod invoke) {
DexMethod invokedMethod = invoke.getInvokedMethod();
return configuration.isToStringMethod(invokedMethod)
&& invoke.getArgument(0).getAliasedValue() == builder;
}
}
}