StringBuilder optimization, part 1.
This CL is collecting locally built builders and filtering out
non-trivial builder patterns that are out of scope.
Bug: 114002137, 113859361, 129200243
Change-Id: I5b8ba4251b892c9e5f1b190321d51439a086c009
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index c2633d3..85cf68a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -156,6 +156,7 @@
public final DexString valueOfMethodName = createString("valueOf");
public final DexString toStringMethodName = createString("toString");
+ public final DexString internMethodName = createString("intern");
public final DexString getClassMethodName = createString("getClass");
public final DexString finalizeMethodName = createString("finalize");
@@ -767,6 +768,7 @@
public final DexMethod valueOf;
public final DexMethod toString;
+ public final DexMethod intern;
private StringMethods() {
isEmpty = createMethod(
@@ -810,6 +812,8 @@
stringDescriptor, valueOfMethodName, stringDescriptor, needsOneObject);
toString = createMethod(
stringDescriptor, toStringMethodName, stringDescriptor, DexString.EMPTY_ARRAY);
+ intern = createMethod(
+ stringDescriptor, internMethodName, stringDescriptor, DexString.EMPTY_ARRAY);
}
}
@@ -832,12 +836,12 @@
public final DexMethod defaultConstructor;
public final DexMethod intConstructor;
public final DexMethod stringConstructor;
+ public final DexMethod toString;
private final Set<DexMethod> appendMethods;
private final Set<DexMethod> constructorMethods;
private StringBuildingMethods(DexType receiver) {
- DexType sbufType = createType(createString("Ljava/lang/StringBuffer;"));
DexString append = createString("append");
appendBoolean = createMethod(receiver, createProto(receiver, booleanType), append);
@@ -854,7 +858,7 @@
appendLong = createMethod(receiver, createProto(receiver, longType), append);
appendObject = createMethod(receiver, createProto(receiver, objectType), append);
appendString = createMethod(receiver, createProto(receiver, stringType), append);
- appendStringBuffer = createMethod(receiver, createProto(receiver, sbufType), append);
+ appendStringBuffer = createMethod(receiver, createProto(receiver, stringBufferType), append);
charSequenceConstructor =
createMethod(receiver, createProto(voidType, charSequenceType), constructorMethodName);
@@ -863,6 +867,7 @@
createMethod(receiver, createProto(voidType, intType), constructorMethodName);
stringConstructor =
createMethod(receiver, createProto(voidType, stringType), constructorMethodName);
+ toString = createMethod(receiver, createProto(stringType), toStringMethodName);
appendMethods =
ImmutableSet.of(
@@ -1186,7 +1191,9 @@
return new DexCallSite(methodName, methodProto, bootstrapMethod, bootstrapArgs);
}
- public DexMethod createMethod(DexString clazzDescriptor, DexString name,
+ public DexMethod createMethod(
+ DexString clazzDescriptor,
+ DexString name,
DexString returnTypeDescriptor,
DexString[] parameterDescriptors) {
assert !sorted;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index ec6da10..e082fb4 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -68,6 +68,7 @@
import com.android.tools.r8.ir.optimize.classinliner.ClassInliner;
import com.android.tools.r8.ir.optimize.lambda.LambdaMerger;
import com.android.tools.r8.ir.optimize.staticizer.ClassStaticizer;
+import com.android.tools.r8.ir.optimize.string.StringBuilderOptimizer;
import com.android.tools.r8.ir.optimize.string.StringOptimizer;
import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
import com.android.tools.r8.ir.regalloc.RegisterAllocator;
@@ -142,6 +143,7 @@
private final Devirtualizer devirtualizer;
private final CovariantReturnTypeAnnotationTransformer covariantReturnTypeAnnotationTransformer;
private final StringOptimizer stringOptimizer;
+ private final StringBuilderOptimizer stringBuilderOptimizer;
private final UninstantiatedTypeOptimization uninstantiatedTypeOptimization;
private final TypeChecker typeChecker;
private final IdempotentFunctionCallCanonicalizer idempotentFunctionCallCanonicalizer;
@@ -193,6 +195,7 @@
? new CovariantReturnTypeAnnotationTransformer(this, appView.dexItemFactory())
: null;
this.stringOptimizer = new StringOptimizer(appView);
+ this.stringBuilderOptimizer = new StringBuilderOptimizer(appView);
this.nonNullTracker = options.enableNonNullTracking ? new NonNullTracker(appView) : null;
if (appView.enableWholeProgramOptimizations()) {
assert appView.appInfo().hasLiveness();
@@ -624,6 +627,9 @@
if (uninstantiatedTypeOptimization != null) {
uninstantiatedTypeOptimization.logResults();
}
+ if (stringBuilderOptimizer != null) {
+ stringBuilderOptimizer.logResults();
+ }
}
// Check if what we've added to the application builder as synthesized classes are same as
@@ -1004,6 +1010,13 @@
codeRewriter.commonSubexpressionElimination(code);
codeRewriter.simplifyArrayConstruction(code);
codeRewriter.rewriteMoveResult(code);
+ // TODO(b/114002137): for now, string concatenation depends on rewriteMoveResult.
+ if (options.enableStringConcatenationOptimization
+ && !isDebugMode
+ && options.isGeneratingDex()) {
+ stringBuilderOptimizer.computeTrivialStringConcatenation(code);
+ }
+
codeRewriter.splitRangeInvokeConstants(code);
new SparseConditionalConstantPropagation(code).run();
codeRewriter.rewriteSwitch(code);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizationConfiguration.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizationConfiguration.java
new file mode 100644
index 0000000..80dd16f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizationConfiguration.java
@@ -0,0 +1,20 @@
+// Copyright (c) 2019, 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.graph.DexType;
+import com.android.tools.r8.ir.code.InvokeMethod;
+
+interface StringBuilderOptimizationConfiguration {
+ boolean isBuilderType(DexType type);
+
+ boolean isBuilderInit(DexType builderType, DexMethod method);
+
+ boolean isAppendMethod(DexMethod method);
+
+ boolean isSupportedAppendMethod(InvokeMethod invoke);
+
+ boolean isToStringMethod(DexMethod method);
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizer.java
new file mode 100644
index 0000000..5e6f735
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizer.java
@@ -0,0 +1,418 @@
+// Copyright (c) 2019, 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.AppInfo;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.escape.EscapeAnalysis;
+import com.android.tools.r8.ir.analysis.escape.EscapeAnalysisConfiguration;
+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.logging.Log;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import it.unimi.dsi.fastutil.objects.Object2IntArrayMap;
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+// This optimization attempts to replace all builder.toString() calls with a constant string.
+// TODO(b/114002137): for now, the analysis depends on rewriteMoveResult.
+// Consider the following example:
+//
+// StringBuilder builder;
+// if (...) {
+// builder.append("X");
+// } else {
+// builder.append("Y");
+// }
+// builder.toString();
+//
+// Its corresponding IR looks like:
+// block0:
+// b <- new-instance StringBuilder
+// if ... block2 // Otherwise, fallthrough
+// block1:
+// c1 <- "X"
+// b1 <- invoke-virtual b, c1, ...append
+// goto block3
+// block2:
+// c2 <- "Y"
+// b2 <- invoke-virtual b, c2, ...append
+// goto block3
+// block3:
+// invoke-virtual b, ...toString
+//
+// After rewriteMoveResult, aliased out values, b1 and b2, are gone. So the analysis can focus on
+// single SSA values, assuming it's flow-sensitive (which is not true in general).
+public class StringBuilderOptimizer {
+
+ private final AppView<?> appView;
+ private final DexItemFactory factory;
+ private final StringBuilderOptimizationConfiguration optimizationConfiguration;
+
+ private int numberOfBuildersWithMultipleToString = 0;
+ private int numberOfBuildersWithoutToString = 0;
+ private int numberOfBuildersThatEscape = 0;
+ private int numberOfBuildersWhoseResultIsInterned = 0;
+ private int numberOfBuildersWithNonTrivialStateChange = 0;
+ private int numberOfBuildersWithNonStringArg = 0;
+
+ @VisibleForTesting
+ StringBuilderOptimizer(
+ AppView<? extends AppInfo> appView, StringBuilderOptimizationConfiguration configuration) {
+ this.appView = appView;
+ this.factory = appView.dexItemFactory();
+ this.optimizationConfiguration = configuration;
+ }
+
+ public StringBuilderOptimizer(AppView<? extends AppInfo> appView) {
+ this.appView = appView;
+ this.factory = appView.dexItemFactory();
+ this.optimizationConfiguration = new DefaultStringBuilderOptimizationConfiguration();
+ }
+
+ public void logResults() {
+ assert Log.ENABLED;
+ Log.info(getClass(),
+ "# builders w/ multiple toString(): %s", numberOfBuildersWithMultipleToString);
+ Log.info(getClass(),
+ "# builders w/o toString(): %s", numberOfBuildersWithoutToString);
+ Log.info(getClass(),
+ "# builders that escape: %s", numberOfBuildersThatEscape);
+ Log.info(getClass(),
+ "# builders whose result is interned: %s", numberOfBuildersWhoseResultIsInterned);
+ Log.info(getClass(),
+ "# builders w/ non-trivial state change: %s", numberOfBuildersWithNonTrivialStateChange);
+ Log.info(getClass(),
+ "# builders w/ non-string arg: %s", numberOfBuildersWithNonStringArg);
+ }
+
+ public Set<Value> computeTrivialStringConcatenation(IRCode code) {
+ StringConcatenationAnalysis analysis = new StringConcatenationAnalysis(code);
+ Set<Value> builders =
+ analysis.findAllLocalBuilders()
+ .stream()
+ .filter(analysis::canBeOptimized)
+ .collect(Collectors.toSet());
+ // TODO(b/114002137): compute const-string for candidate builders.
+ return builders;
+ }
+
+ class StringConcatenationAnalysis {
+
+ // Inspired by {@link JumboStringTest}. Some code intentionally may have too many append(...).
+ private static final int CONCATENATION_THRESHOLD = 200;
+
+ private final IRCode code;
+
+ // A map from SSA Value of StringBuilder type to its toString() counts.
+ // Reused (e.g., concatenated, toString, concatenated more, toString) builders are out of scope.
+ // TODO(b/114002137): some of those toString could have constant string states.
+ final Object2IntMap<Value> builderToStringCounts = new Object2IntArrayMap<>();
+
+ StringConcatenationAnalysis(IRCode code) {
+ this.code = code;
+ }
+
+ // This optimization focuses on builders that are created and used locally.
+ // In the first step, we collect builders that are created in the current method.
+ // In the next step, we will filter out builders that cannot be optimized. To avoid multiple
+ // iterations per builder, we're collecting # of uses of those builders by iterating the code
+ // twice in this step.
+ private Set<Value> findAllLocalBuilders() {
+ // During the first iteration, collect builders that are locally created.
+ // TODO(b/114002137): Make sure new-instance is followed by <init> before any other calls.
+ for (Instruction instr : code.instructions()) {
+ if (instr.isNewInstance()
+ && optimizationConfiguration.isBuilderType(instr.asNewInstance().clazz)) {
+ Value builder = instr.asNewInstance().dest();
+ assert !builderToStringCounts.containsKey(builder);
+ builderToStringCounts.put(builder, 0);
+ }
+ }
+ if (builderToStringCounts.isEmpty()) {
+ return ImmutableSet.of();
+ }
+ int concatenationCount = 0;
+ // During the second iteration, count builders' usage.
+ for (Instruction instr : code.instructions()) {
+ if (!instr.isInvokeVirtual()) {
+ continue;
+ }
+ InvokeVirtual invoke = instr.asInvokeVirtual();
+ DexMethod invokedMethod = invoke.getInvokedMethod();
+ if (optimizationConfiguration.isAppendMethod(invokedMethod)) {
+ concatenationCount++;
+ // The analysis might be overwhelmed.
+ if (concatenationCount > CONCATENATION_THRESHOLD) {
+ return ImmutableSet.of();
+ }
+ } else if (optimizationConfiguration.isToStringMethod(invokedMethod)) {
+ assert invoke.inValues().size() == 1;
+ Value receiver = invoke.getReceiver().getAliasedValue();
+ for (Value builder : collectAllLinkedBuilders(receiver)) {
+ if (builderToStringCounts.containsKey(builder)) {
+ int count = builderToStringCounts.getInt(builder);
+ builderToStringCounts.put(builder, count + 1);
+ }
+ }
+ }
+ }
+ return builderToStringCounts.keySet();
+ }
+
+ private Set<Value> collectAllLinkedBuilders(Value builder) {
+ Set<Value> builders = Sets.newIdentityHashSet();
+ Set<Value> visited = Sets.newIdentityHashSet();
+ collectAllLinkedBuilders(builder, builders, visited);
+ return builders;
+ }
+
+ private void collectAllLinkedBuilders(Value builder, Set<Value> builders, Set<Value> visited) {
+ if (!visited.add(builder)) {
+ return;
+ }
+ if (builder.isPhi()) {
+ for (Value operand : builder.asPhi().getOperands()) {
+ collectAllLinkedBuilders(operand, builders, visited);
+ }
+ } else {
+ builders.add(builder);
+ }
+ }
+
+ private boolean canBeOptimized(Value builder) {
+ // If the builder is definitely null, it may be handled by other optimizations.
+ // E.g., any further operations, such as append, will raise NPE.
+ // But, as we collect local builders, it should never be null.
+ assert !builder.isAlwaysNull(appView);
+ // Before checking the builder usage, make sure we have its usage count.
+ assert builderToStringCounts.containsKey(builder);
+ // If a builder is reused, chances are the code is not trivial, e.g., building a prefix
+ // at some point; appending different suffices in different conditions; and building again.
+ if (builderToStringCounts.getInt(builder) > 1) {
+ numberOfBuildersWithMultipleToString++;
+ return false;
+ }
+ // If a builder is not used, i.e., never converted to string, it doesn't make sense to
+ // attempt to compute its compile-time constant string.
+ if (builderToStringCounts.getInt(builder) < 1) {
+ numberOfBuildersWithoutToString++;
+ return false;
+ }
+ // Make sure builder is neither phi nor coming from outside of the method.
+ assert !builder.isPhi() && builder.definition.isNewInstance();
+ assert builder.getTypeLattice().isClassType();
+ DexType builderType = builder.getTypeLattice().asClassTypeLatticeElement().getClassType();
+ assert optimizationConfiguration.isBuilderType(builderType);
+ EscapeAnalysis escapeAnalysis =
+ new EscapeAnalysis(
+ appView, new StringBuilderOptimizerEscapeAnalysisConfiguration(builder));
+ return !escapeAnalysis.isEscaping(code, builder);
+ }
+ }
+
+ class DefaultStringBuilderOptimizationConfiguration
+ implements StringBuilderOptimizationConfiguration {
+ @Override
+ public boolean isBuilderType(DexType type) {
+ return type == factory.stringBuilderType
+ || type == factory.stringBufferType;
+ }
+
+ @Override
+ public boolean isBuilderInit(DexType builderType, DexMethod method) {
+ return builderType == method.holder
+ && factory.isConstructor(method);
+ }
+
+ @Override
+ public boolean isAppendMethod(DexMethod method) {
+ return factory.stringBuilderMethods.isAppendMethod(method)
+ || factory.stringBufferMethods.isAppendMethod(method);
+ }
+
+ @Override
+ public boolean isSupportedAppendMethod(InvokeMethod invoke) {
+ DexMethod invokedMethod = invoke.getInvokedMethod();
+ assert isAppendMethod(invokedMethod);
+ // Any methods other than append(arg) are not trivial since they may change the builder
+ // state not monotonically.
+ if (invoke.inValues().size() > 2) {
+ numberOfBuildersWithNonTrivialStateChange++;
+ return false;
+ }
+ for (DexType argType : invokedMethod.proto.parameters.values) {
+ if (!canHandleArgumentType(argType)) {
+ numberOfBuildersWithNonStringArg++;
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean isToStringMethod(DexMethod method) {
+ return method == factory.stringBuilderMethods.toString
+ || method == factory.stringBufferMethods.toString;
+ }
+
+ private boolean canHandleArgumentType(DexType argType) {
+ // TODO(b/113859361): passed to another builder should be an eligible case.
+ // TODO(b/114002137): Improve arg extraction and type conversion.
+ // For now, skip any append(arg) that receives non-string types.
+ return argType != factory.stringType && argType != factory.charSequenceType;
+ }
+ }
+
+ class StringBuilderOptimizerEscapeAnalysisConfiguration implements EscapeAnalysisConfiguration {
+ final Value builder;
+ final DexType builderType;
+
+ private StringBuilderOptimizerEscapeAnalysisConfiguration(Value builder) {
+ this.builder = builder;
+ assert builder.getTypeLattice().isClassType();
+ builderType = builder.getTypeLattice().asClassTypeLatticeElement().getClassType();
+ }
+
+ // Use of Builder#toString is legitimate.
+ // TODO(b/134745277): but, the escape analysis can be filtered this out by types.
+ private boolean isUsingToStringAlias(EscapeAnalysis analysis, Value alias) {
+ if (!alias.isPhi() && alias.definition.isInvokeMethod()) {
+ DexMethod invokedMethod = alias.definition.asInvokeMethod().getInvokedMethod();
+ return optimizationConfiguration.isToStringMethod(invokedMethod)
+ && alias.definition.inValues().stream().anyMatch(analysis::isValueOfInterestOrAlias);
+ }
+ return false;
+ }
+
+ private void logEscapingRoute(boolean legitimate) {
+ if (!legitimate) {
+ numberOfBuildersThatEscape++;
+ }
+ }
+
+ @Override
+ public boolean isLegitimateEscapeRoute(
+ AppView<?> appView,
+ EscapeAnalysis escapeAnalysis,
+ Instruction escapeRoute,
+ DexMethod context) {
+ boolean legitimate;
+ if (escapeRoute.isReturn()) {
+ legitimate = isUsingToStringAlias(escapeAnalysis, escapeRoute.asReturn().returnValue());
+ logEscapingRoute(legitimate);
+ return legitimate;
+ }
+ if (escapeRoute.isThrow()) {
+ legitimate = isUsingToStringAlias(escapeAnalysis, escapeRoute.asThrow().exception());
+ logEscapingRoute(legitimate);
+ return legitimate;
+ }
+ if (escapeRoute.isStaticPut()) {
+ legitimate = isUsingToStringAlias(escapeAnalysis, escapeRoute.asStaticPut().inValue());
+ logEscapingRoute(legitimate);
+ return legitimate;
+ }
+ if (escapeRoute.isArrayPut()) {
+ if (escapeRoute.asArrayPut().array().isArgument()) {
+ legitimate = isUsingToStringAlias(escapeAnalysis, escapeRoute.asArrayPut().value());
+ logEscapingRoute(legitimate);
+ return legitimate;
+ }
+ // Putting the builder (or aliases) into a local array is legitimate.
+ // If that local array is used again with array-get, the escape analysis will pick up the
+ // out-value of that instruction and keep tracing the value uses anyway.
+ return true;
+ }
+
+ if (escapeRoute.isInvokeMethod()) {
+ boolean useBuilder = false;
+ boolean useToStringAlias = false;
+ for (Value arg : escapeRoute.asInvokeMethod().inValues()) {
+ // Direct use of the builder should be caught and examined later.
+ if (arg == builder) {
+ useBuilder = true;
+ break;
+ }
+ useToStringAlias |= isUsingToStringAlias(escapeAnalysis, arg);
+ }
+ // It's legitimate if a call doesn't use the builder directly, but use aliased values of
+ // Builder#toString.
+ if (!useBuilder && useToStringAlias) {
+ return true;
+ }
+
+ // Program class may call String#intern(). Only allow library calls.
+ // TODO(b/114002137): For now, we allow only library calls to avoid a case like
+ // identity(Builder.toString()).intern(); but it's too restrictive.
+ DexClass holderClass =
+ appView.definitionFor(escapeRoute.asInvokeMethod().getInvokedMethod().holder);
+ if (holderClass != null && !holderClass.isLibraryClass()) {
+ logEscapingRoute(false);
+ return false;
+ }
+
+ InvokeMethod invoke = escapeRoute.asInvokeMethod();
+ DexMethod invokedMethod = invoke.getInvokedMethod();
+ // Make sure builder's uses are local, i.e., not escaping from the current method.
+ if (invokedMethod.holder != builderType) {
+ numberOfBuildersThatEscape++;
+ logEscapingRoute(false);
+ return false;
+ }
+ // <init> is legitimate.
+ if (optimizationConfiguration.isBuilderInit(builderType, invokedMethod)) {
+ return true;
+ }
+ if (optimizationConfiguration.isToStringMethod(invokedMethod)) {
+ Value out = escapeRoute.outValue();
+ if (out != null) {
+ // If Builder#toString is interned, it could be used for equality check.
+ // Replacing builder-based runtime result with a compile time constant may change
+ // the program's runtime behavior.
+ for (Instruction outUser : out.uniqueUsers()) {
+ if (outUser.isInvokeMethodWithReceiver()
+ && outUser.asInvokeMethodWithReceiver().getInvokedMethod()
+ == factory.stringMethods.intern) {
+ numberOfBuildersWhoseResultIsInterned++;
+ logEscapingRoute(false);
+ return false;
+ }
+ }
+ }
+ // Otherwise, use of Builder#toString is legitimate.
+ return true;
+ }
+ // Even though all invocations belong to the builder type, there are some methods other
+ // than append/toString, e.g., setCharAt, setLength, subSequence, etc.
+ // Seeing any of them indicates that this code is not trivial.
+ if (!optimizationConfiguration.isAppendMethod(invokedMethod)) {
+ numberOfBuildersWithNonTrivialStateChange++;
+ logEscapingRoute(false);
+ return false;
+ }
+ if (!optimizationConfiguration.isSupportedAppendMethod(invoke)) {
+ return false;
+ }
+
+ // Reaching here means that this invocation is part of trivial patterns we're looking for.
+ return true;
+ }
+
+ // All other cases are not legitimate.
+ logEscapingRoute(false);
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index adf737c..78cf79e 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -167,6 +167,8 @@
public boolean enableServiceLoaderRewriting = true;
// TODO(b/120138731): Enable this when it is worthwhile, e.g., combined with Class#forName.
public boolean enableNameReflectionOptimization = false;
+ // TODO(b/114002137): Enable this when it is worthwhile, e.g., support non-String args.
+ public boolean enableStringConcatenationOptimization = false;
public boolean enableTreeShakingOfLibraryMethodOverrides = false;
// This defines the max depth threshold for the cycle eliminator. If the length of a call chain
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizerAnalysisTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizerAnalysisTest.java
new file mode 100644
index 0000000..6995a33
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizerAnalysisTest.java
@@ -0,0 +1,154 @@
+// Copyright (c) 2019, 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 static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.AnalysisTestBase;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
+import java.util.Set;
+import java.util.function.Consumer;
+import org.junit.Test;
+
+public class StringBuilderOptimizerAnalysisTest extends AnalysisTestBase {
+
+ public StringBuilderOptimizerAnalysisTest() throws Exception {
+ super(StringConcatenationTestClass.class.getTypeName(), StringConcatenationTestClass.class);
+ }
+
+ @Test
+ public void testTrivialSequence() throws Exception {
+ buildAndCheckIR("trivialSequence", checkBuilderDetection(builders -> {
+ assertEquals(1, builders.size());
+ }));
+ }
+
+ @Test
+ public void testNonStringArgs() throws Exception {
+ buildAndCheckIR("nonStringArgs", checkBuilderDetection(builders -> {
+ // TODO(b/114002137): Improve arg extraction and type conversion.
+ assertEquals(0, builders.size());
+ }));
+ }
+
+ @Test
+ public void testTypeConversion() throws Exception {
+ buildAndCheckIR("typeConversion", checkBuilderDetection(builders -> {
+ // TODO(b/114002137): Improve arg extraction and type conversion.
+ assertEquals(0, builders.size());
+ }));
+ }
+
+ @Test
+ public void testNestedBuilders_appendBuilderItself() throws Exception {
+ buildAndCheckIR("nestedBuilders_appendBuilderItself", checkBuilderDetection(builders -> {
+ assertEquals(1, builders.size());
+ }));
+ }
+
+ @Test
+ public void testNestedBuilders_appendBuilderResult() throws Exception {
+ buildAndCheckIR("nestedBuilders_appendBuilderResult", checkBuilderDetection(builders -> {
+ assertEquals(2, builders.size());
+ }));
+ }
+
+ @Test
+ public void testSimplePhi() throws Exception {
+ buildAndCheckIR("simplePhi", checkBuilderDetection(builders -> {
+ // TODO(b/114002137): Improve arg extraction and type conversion.
+ assertEquals(0, builders.size());
+ }));
+ }
+
+ @Test
+ public void testPhiAtInit() throws Exception {
+ buildAndCheckIR("phiAtInit", checkBuilderDetection(builders -> {
+ assertEquals(2, builders.size());
+ }));
+ }
+
+ @Test
+ public void testPhiWithDifferentInits() throws Exception {
+ buildAndCheckIR("phiWithDifferentInits", checkBuilderDetection(builders -> {
+ assertEquals(2, builders.size());
+ }));
+ }
+
+ @Test
+ public void testLoop() throws Exception {
+ buildAndCheckIR("loop", checkBuilderDetection(builders -> {
+ assertEquals(2, builders.size());
+ }));
+ }
+
+ @Test
+ public void testLoopWithBuilder() throws Exception {
+ buildAndCheckIR("loopWithBuilder", checkBuilderDetection(builders -> {
+ assertEquals(1, builders.size());
+ }));
+ }
+
+ // TODO(b/114002137): later, test what const-string is computed for builders.
+ private Consumer<IRCode> checkBuilderDetection(Consumer<Set<Value>> builderConsumer) {
+ return code -> {
+ StringBuilderOptimizer optimizer =
+ new StringBuilderOptimizer(
+ appView, new StringBuilderOptimizationConfigurationForTesting());
+ Set<Value> builders = optimizer.computeTrivialStringConcatenation(code);
+ builderConsumer.accept(builders);
+ };
+ }
+
+ class StringBuilderOptimizationConfigurationForTesting
+ implements StringBuilderOptimizationConfiguration {
+ @Override
+ public boolean isBuilderType(DexType type) {
+ String descriptor = type.toDescriptorString();
+ return descriptor.equals(appView.dexItemFactory().stringBuilderType.toDescriptorString())
+ || descriptor.equals(appView.dexItemFactory().stringBufferType.toDescriptorString());
+ }
+
+ @Override
+ public boolean isBuilderInit(DexType builderType, DexMethod method) {
+ return builderType == method.holder
+ && method.name.toString().equals("<init>");
+ }
+
+ @Override
+ public boolean isAppendMethod(DexMethod method) {
+ return isBuilderType(method.holder) && method.name.toString().equals("append");
+ }
+
+ @Override
+ public boolean isSupportedAppendMethod(InvokeMethod invoke) {
+ DexMethod invokedMethod = invoke.getInvokedMethod();
+ assert isAppendMethod(invokedMethod);
+ if (invoke.inValues().size() > 2) {
+ return false;
+ }
+ for (DexType argType : invokedMethod.proto.parameters.values) {
+ if (!canHandleArgumentType(argType)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean isToStringMethod(DexMethod method) {
+ return isBuilderType(method.holder) && method.name.toString().equals("toString");
+ }
+
+ private boolean canHandleArgumentType(DexType argType) {
+ String descriptor = argType.toDescriptorString();
+ return descriptor.equals(appView.dexItemFactory().stringType.toDescriptorString())
+ || descriptor.equals(appView.dexItemFactory().charSequenceType.toDescriptorString());
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatenationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatenationTest.java
index b0c07a7..3d3c010 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatenationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatenationTest.java
@@ -14,6 +14,7 @@
import com.android.tools.r8.TestParameters;
import com.android.tools.r8.TestParametersCollection;
import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.StringUtils;
import com.android.tools.r8.utils.codeinspector.ClassSubject;
import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -35,6 +36,8 @@
"Hello,R8",
"Hello,",
"Hello,D8",
+ "Hello,R8",
+ "Hello,R8",
"na;na;na;na;na;na;na;na;Batman!",
"na;na;na;na;na;na;na;na;Batman!"
);
@@ -60,7 +63,10 @@
}
private void test(
- TestRunResult result, int expectedStringCount1, int expectedStringCount2)
+ TestRunResult result,
+ int expectedStringCount1,
+ int expectedStringCount2,
+ int expectedStringCount3)
throws Exception {
CodeInspector codeInspector = result.inspector();
ClassSubject mainClass = codeInspector.clazz(MAIN);
@@ -93,7 +99,7 @@
assertThat(method, isPresent());
count = Streams.stream(method.iterateInstructions(
i -> i.isConstString(JumboStringMode.ALLOW))).count();
- assertEquals(expectedStringCount2, count);
+ assertEquals(expectedStringCount3, count);
method = mainClass.uniqueMethodWithName("simplePhi");
assertThat(method, isPresent());
@@ -101,6 +107,18 @@
i -> i.isConstString(JumboStringMode.ALLOW))).count();
assertEquals(5, count);
+ method = mainClass.uniqueMethodWithName("phiAtInit");
+ assertThat(method, isPresent());
+ count = Streams.stream(method.iterateInstructions(
+ i -> i.isConstString(JumboStringMode.ALLOW))).count();
+ assertEquals(3, count);
+
+ method = mainClass.uniqueMethodWithName("phiWithDifferentInits");
+ assertThat(method, isPresent());
+ count = Streams.stream(method.iterateInstructions(
+ i -> i.isConstString(JumboStringMode.ALLOW))).count();
+ assertEquals(3, count);
+
method = mainClass.uniqueMethodWithName("loop");
assertThat(method, isPresent());
count = Streams.stream(method.iterateInstructions(
@@ -123,34 +141,44 @@
.debug()
.addProgramClasses(MAIN)
.setMinApi(parameters.getRuntime())
+ .addOptionsModification(this::configure)
.run(parameters.getRuntime(), MAIN)
.assertSuccessWithOutput(JAVA_OUTPUT);
- test(result, 3, 4);
+ test(result, 3, 4, 4);
result =
testForD8()
.release()
.addProgramClasses(MAIN)
.setMinApi(parameters.getRuntime())
+ .addOptionsModification(this::configure)
.run(parameters.getRuntime(), MAIN)
.assertSuccessWithOutput(JAVA_OUTPUT);
- // TODO(b/114002137): could be 1 and 3.
- test(result, 3, 4);
+ // TODO(b/114002137): could be 1, 4, and 3.
+ test(result, 3, 4, 4);
}
@Test
public void testR8() throws Exception {
+ assumeTrue("CF does not canonicalize constants.", parameters.isDexRuntime());
+
R8TestRunResult result =
testForR8(parameters.getBackend())
.addProgramClasses(MAIN)
.enableInliningAnnotations()
.addKeepMainRule(MAIN)
- .setMinApi(parameters.getRuntime())
.noMinification()
+ .setMinApi(parameters.getRuntime())
+ .addOptionsModification(this::configure)
.run(parameters.getRuntime(), MAIN)
.assertSuccessWithOutput(JAVA_OUTPUT);
- // TODO(b/114002137): could be 1 and 3.
- test(result, 3, 4);
+ // TODO(b/114002137): could be 1, 4, and 3.
+ test(result, 3, 4, 4);
+ }
+
+ // TODO(b/114002137): Once enabled, remove this test-specific setting.
+ private void configure(InternalOptions options) {
+ options.enableStringConcatenationOptimization = true;
}
}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatenationTestClass.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatenationTestClass.java
index 3cfb5de..9a5856d 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatenationTestClass.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatenationTestClass.java
@@ -84,6 +84,24 @@
}
@NeverInline
+ public static void phiAtInit() {
+ // TODO(b/114002137): Use ASM to test two new-instance calls flow into the same <init>
+ StringBuilder builder =
+ System.currentTimeMillis() > 0 ? new StringBuilder("Hello") : new StringBuilder("Hi");
+ builder.append(",R8");
+ System.out.println(builder.toString());
+ }
+
+ @NeverInline
+ public static void phiWithDifferentInits() {
+ StringBuilder b1 = new StringBuilder("Hello");
+ StringBuilder b2 = new StringBuilder("Hi");
+ StringBuilder builder = System.currentTimeMillis() > 0 ? b1 : b2;
+ builder.append(",R8");
+ System.out.println(builder.toString());
+ }
+
+ @NeverInline
public static void loop() {
String r = "";
for (int i = 0; i < 8; i++) {
@@ -109,6 +127,8 @@
nestedBuilders_appendBuilderItself();
nestedBuilders_appendBuilderResult();
simplePhi();
+ phiAtInit();
+ phiWithDifferentInits();
loop();
loopWithBuilder();
}