Canonicalize calls with no side effects and deterministic outputs.
Bug: 132123953
Change-Id: Iaaba650149eb95864c69d59342015aad729ce02d
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index add6578..a3a329e 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -1008,6 +1008,7 @@
public static boolean UNKNOWN_INITIALIZER_ENABLING_JAVA_ASSERTIONS = false;
public static ParameterUsagesInfo UNKNOWN_PARAMETER_USAGE_INFO = null;
public static boolean UNKNOWN_MAY_HAVE_SIDE_EFFECTS = true;
+ public static boolean UNKNOWN_RETURN_VALUE_ONLY_DEPENDS_ON_ARGUMENTS = false;
public static BitSet NO_NULL_PARAMETER_OR_THROW_FACTS = null;
public static BitSet NO_NULL_PARAMETER_ON_NORMAL_EXITS_FACTS = null;
@@ -1143,6 +1144,11 @@
}
@Override
+ public boolean returnValueOnlyDependsOnArguments() {
+ return UNKNOWN_RETURN_VALUE_ONLY_DEPENDS_ON_ARGUMENTS;
+ }
+
+ @Override
public boolean returnValueHasBeenPropagated() {
return false;
}
@@ -1161,6 +1167,8 @@
private int returnedArgument = DefaultMethodOptimizationInfoImpl.UNKNOWN_RETURNED_ARGUMENT;
private boolean mayHaveSideEffects =
DefaultMethodOptimizationInfoImpl.UNKNOWN_MAY_HAVE_SIDE_EFFECTS;
+ private boolean returnValueOnlyDependsOnArguments =
+ DefaultMethodOptimizationInfoImpl.UNKNOWN_RETURN_VALUE_ONLY_DEPENDS_ON_ARGUMENTS;
private boolean neverReturnsNull = DefaultMethodOptimizationInfoImpl.UNKNOWN_NEVER_RETURNS_NULL;
private boolean neverReturnsNormally =
DefaultMethodOptimizationInfoImpl.UNKNOWN_NEVER_RETURNS_NORMALLY;
@@ -1369,6 +1377,11 @@
}
@Override
+ public boolean returnValueOnlyDependsOnArguments() {
+ return returnValueOnlyDependsOnArguments;
+ }
+
+ @Override
public void setParameterUsages(ParameterUsagesInfo parametersUsages) {
this.parametersUsages = parametersUsages;
}
@@ -1421,6 +1434,11 @@
}
@Override
+ public void markReturnValueOnlyDependsOnArguments() {
+ returnValueOnlyDependsOnArguments = true;
+ }
+
+ @Override
public void markNeverReturnsNull() {
neverReturnsNull = true;
}
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 cb9d725..d5ebf00 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -47,6 +47,7 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Function;
+import java.util.stream.Collectors;
public class DexItemFactory {
@@ -94,6 +95,8 @@
public DexItemFactory() {
this.kotlin = new Kotlin(this);
+ assert libraryMethodsWithoutSideEffects
+ .containsAll(libraryMethodsWithReturnValueDependingOnlyOnArguments);
}
public static boolean isInternalSentinel(DexItem item) {
@@ -349,6 +352,43 @@
public final ServiceLoaderMethods serviceLoaderMethods = new ServiceLoaderMethods();
+ public final BiMap<DexType, DexType> primitiveToBoxed = HashBiMap.create(
+ ImmutableMap.<DexType, DexType>builder()
+ .put(booleanType, boxedBooleanType)
+ .put(byteType, boxedByteType)
+ .put(charType, boxedCharType)
+ .put(shortType, boxedShortType)
+ .put(intType, boxedIntType)
+ .put(longType, boxedLongType)
+ .put(floatType, boxedFloatType)
+ .put(doubleType, boxedDoubleType)
+ .build());
+
+ public DexType getBoxedForPrimitiveType(DexType primitive) {
+ assert primitive.isPrimitiveType();
+ return primitiveToBoxed.get(primitive);
+ }
+
+ public DexType getPrimitiveFromBoxed(DexType boxedPrimitive) {
+ return primitiveToBoxed.inverse().get(boxedPrimitive);
+ }
+
+ // Boxed Boxed#valueOf(Primitive), e.g., Boolean Boolean#valueOf(B)
+ private Set<DexMethod> boxedValueOfMethods() {
+ return primitiveToBoxed.entrySet().stream()
+ .map(
+ entry -> {
+ DexType primitive = entry.getKey();
+ DexType boxed = entry.getValue();
+ return createMethod(
+ boxed.descriptor,
+ valueOfMethodName,
+ boxed.descriptor,
+ new DexString[] {primitive.descriptor});
+ })
+ .collect(Collectors.toSet());
+ }
+
public final DexMethod metafactoryMethod =
createMethod(
metafactoryType,
@@ -414,6 +454,18 @@
.addAll(classMethods.getNames)
.addAll(stringBufferMethods.constructorMethods)
.addAll(stringBuilderMethods.constructorMethods)
+ .addAll(boxedValueOfMethods())
+ .build();
+
+ // TODO(b/119596718): More idempotent methods? Any singleton accessors? E.g.,
+ // java.util.Calendar#getInstance(...) // 4 variants
+ // java.util.Locale#getDefault() // returns JVM default locale.
+ // android.os.Looper#myLooper() // returns the associated Looper instance.
+ // Note that this set is used for canonicalization of method invocations, together with a set of
+ // library methods that do not have side effects.
+ public Set<DexMethod> libraryMethodsWithReturnValueDependingOnlyOnArguments =
+ ImmutableSet.<DexMethod>builder()
+ .addAll(boxedValueOfMethods())
.build();
public Set<DexType> libraryTypesAssumedToBePresent =
@@ -436,27 +488,6 @@
return dexMethod == metafactoryMethod || dexMethod == metafactoryAltMethod;
}
- public final BiMap<DexType, DexType> primitiveToBoxed = HashBiMap.create(
- ImmutableMap.<DexType, DexType>builder()
- .put(booleanType, boxedBooleanType)
- .put(byteType, boxedByteType)
- .put(charType, boxedCharType)
- .put(shortType, boxedShortType)
- .put(intType, boxedIntType)
- .put(longType, boxedLongType)
- .put(floatType, boxedFloatType)
- .put(doubleType, boxedDoubleType)
- .build());
-
- public DexType getBoxedForPrimitiveType(DexType primitive) {
- assert primitive.isPrimitiveType();
- return primitiveToBoxed.get(primitive);
- }
-
- public DexType getPrimitiveFromBoxed(DexType boxedPrimitive) {
- return primitiveToBoxed.inverse().get(boxedPrimitive);
- }
-
public class LongMethods {
public final DexMethod compare;
diff --git a/src/main/java/com/android/tools/r8/graph/MethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/graph/MethodOptimizationInfo.java
index 213bb11..108779c 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodOptimizationInfo.java
@@ -69,6 +69,8 @@
boolean mayHaveSideEffects();
+ boolean returnValueOnlyDependsOnArguments();
+
boolean returnValueHasBeenPropagated();
UpdatableMethodOptimizationInfo mutableCopy();
diff --git a/src/main/java/com/android/tools/r8/graph/UpdatableMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/graph/UpdatableMethodOptimizationInfo.java
index 5791733..da39812 100644
--- a/src/main/java/com/android/tools/r8/graph/UpdatableMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/UpdatableMethodOptimizationInfo.java
@@ -28,6 +28,8 @@
void markMayNotHaveSideEffects();
+ void markReturnValueOnlyDependsOnArguments();
+
void markNeverReturnsNull();
void markNeverReturnsNormally();
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/DeterminismAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/DeterminismAnalysis.java
new file mode 100644
index 0000000..b8e6950
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/DeterminismAnalysis.java
@@ -0,0 +1,73 @@
+// 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.analysis;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+// Determine whether the given method generates the deterministic output for the given same input.
+// For now, this analysis is very conservative in the sense that it only allows methods that depend
+// on arguments. To that end, the followings are not allowed.
+// 1) any non-argument access, e.g., static-get, may result in different outputs.
+// 2) even for argument access, states of instance or array can be different in any way.
+// 3) if the current method invokes another method that is not deterministic, neither is it.
+public class DeterminismAnalysis {
+
+ public static boolean returnValueOnlyDependsOnArguments(
+ AppView<AppInfoWithLiveness> appView, IRCode code) {
+ for (Instruction instr : code.instructions()) {
+ if (instr.outValue() == null || !instr.outValue().isUsed()) {
+ continue;
+ }
+ if (instr.isStaticGet()) {
+ // Unless we model the heap and take into account the snapshot per call site.
+ return false;
+ }
+ if (instr.isInstanceGet()) {
+ // Unless we trace the state of the instance.
+ return false;
+ }
+ if (instr.isArrayGet() || instr.isArrayLength()) {
+ // Unless we trace the state of the array.
+ return false;
+ }
+ if (instr.isInvokeMethod()) {
+ DexEncodedMethod target =
+ instr.asInvokeMethod().lookupSingleTarget(appView, code.method.method.holder);
+ if (target != null && target.getOptimizationInfo().returnValueOnlyDependsOnArguments()) {
+ continue;
+ }
+ return false;
+ }
+ if (instr.isInvokeCustom() || instr.isInvokePolymorphic()) {
+ // Unless we can nail down the single target for these invocations.
+ return false;
+ }
+ if (instr.isCreatingInstanceOrArray()) {
+ // Unless we determine the new instance/array is local.
+ return false;
+ }
+ if (instr.isMoveException()) {
+ return false;
+ }
+ // isArrayPut and isFieldPut are missed as they don't have out value.
+ assert instr.isArgument()
+ || instr.isAssume()
+ || instr.isBinop()
+ || instr.isUnop()
+ || instr.isMonitor()
+ || instr.isMove()
+ || instr.isCheckCast()
+ || instr.isInstanceOf()
+ || instr.isConstInstruction()
+ || instr.isJumpInstruction()
+ || instr.isDebugInstruction()
+ : "Instruction that impacts determinism: " + instr;
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index cc54a60..e6249ff 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -1014,4 +1014,27 @@
}
}
}
+
+ public Position findFirstNonNonePosition() {
+ Instruction rightAfterArguments =
+ entryBlock().listIterator().nextUntil(instr -> !instr.isArgument());
+ Position firstNonArgumentPosition = rightAfterArguments.getPosition();
+ Set<BasicBlock> visitedBlocks = new HashSet<>();
+ while (rightAfterArguments != null) {
+ // Make sure we are not looping.
+ if (visitedBlocks.contains(rightAfterArguments.getBlock())) {
+ break;
+ }
+ visitedBlocks.add(rightAfterArguments.getBlock());
+ // The very first non-argument instruction can be chained via goto.
+ if (rightAfterArguments.isGoto()) {
+ rightAfterArguments = rightAfterArguments.asGoto().getTarget().getInstructions().getFirst();
+ } else if (rightAfterArguments.getPosition().isSome()) {
+ return rightAfterArguments.getPosition();
+ } else {
+ break;
+ }
+ }
+ return firstNonArgumentPosition;
+ }
}
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 e1863b2..dbdc2dc 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
@@ -26,6 +26,7 @@
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.DexTypeList;
import com.android.tools.r8.graph.GraphLense;
+import com.android.tools.r8.ir.analysis.DeterminismAnalysis;
import com.android.tools.r8.ir.analysis.InitializedClassesOnNormalExitAnalysis;
import com.android.tools.r8.ir.analysis.TypeChecker;
import com.android.tools.r8.ir.analysis.constant.SparseConditionalConstantPropagation;
@@ -245,8 +246,7 @@
options.enableNestBasedAccessDesugaring ? new D8NestBasedAccessDesugaring(appView) : null;
}
this.deadCodeRemover = new DeadCodeRemover(appView, codeRewriter);
- this.idempotentFunctionCallCanonicalizer =
- new IdempotentFunctionCallCanonicalizer(appView.dexItemFactory());
+ this.idempotentFunctionCallCanonicalizer = new IdempotentFunctionCallCanonicalizer(appView);
}
public Set<DexCallSite> getDesugaredCallSites() {
@@ -612,6 +612,9 @@
}
if (Log.ENABLED) {
+ if (idempotentFunctionCallCanonicalizer != null) {
+ idempotentFunctionCallCanonicalizer.logResults();
+ }
if (libraryMethodOverrideAnalysis != null) {
libraryMethodOverrideAnalysis.logResults();
}
@@ -1157,6 +1160,7 @@
computeDynamicReturnType(feedback, method, code);
computeInitializedClassesOnNormalExit(feedback, method, code);
computeMayHaveSideEffects(feedback, method, code);
+ computeReturnValueOnlyDependsOnArguments(feedback, method, code);
computeNonNullParamOrThrow(feedback, method, code);
}
}
@@ -1295,11 +1299,25 @@
instruction ->
instruction.instructionMayHaveSideEffects(appView, method.method.holder));
if (!mayHaveSideEffects) {
+ // If the method is native, we don't know what could happen.
+ assert !method.accessFlags.isNative();
feedback.methodMayNotHaveSideEffects(method);
}
}
}
+ private void computeReturnValueOnlyDependsOnArguments(
+ OptimizationFeedback feedback, DexEncodedMethod method, IRCode code) {
+ if (!options.enableDeterminismAnalysis) {
+ return;
+ }
+ boolean returnValueOnlyDependsOnArguments =
+ DeterminismAnalysis.returnValueOnlyDependsOnArguments(appView.withLiveness(), code);
+ if (returnValueOnlyDependsOnArguments) {
+ feedback.methodReturnValueOnlyDependsOnArguments(method);
+ }
+ }
+
// Returns true if `method` is an initializer and the enclosing class overrides the method
// `void java.lang.Object.finalize()`.
private boolean hasNonTrivialFinalizeMethod(DexType type) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
index bf7e94f..752f35e 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
@@ -32,6 +32,8 @@
void methodMayNotHaveSideEffects(DexEncodedMethod method);
+ void methodReturnValueOnlyDependsOnArguments(DexEncodedMethod method);
+
void methodNeverReturnsNull(DexEncodedMethod method);
void methodNeverReturnsNormally(DexEncodedMethod method);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDelayed.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDelayed.java
index 4ca780d..82f6391 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDelayed.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDelayed.java
@@ -85,6 +85,11 @@
}
@Override
+ public synchronized void methodReturnValueOnlyDependsOnArguments(DexEncodedMethod method) {
+ getOptimizationInfoForUpdating(method).markReturnValueOnlyDependsOnArguments();
+ }
+
+ @Override
public synchronized void markAsPropagated(DexEncodedMethod method) {
getOptimizationInfoForUpdating(method).markAsPropagated();
}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
index 7eb3549..c0655df 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
@@ -48,6 +48,9 @@
public void methodMayNotHaveSideEffects(DexEncodedMethod method) {}
@Override
+ public void methodReturnValueOnlyDependsOnArguments(DexEncodedMethod method) {}
+
+ @Override
public void methodNeverReturnsNull(DexEncodedMethod method) {}
@Override
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackSimple.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackSimple.java
index 44639e3..1fc7a91 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackSimple.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackSimple.java
@@ -54,6 +54,11 @@
}
@Override
+ public void methodReturnValueOnlyDependsOnArguments(DexEncodedMethod method) {
+ // Ignored.
+ }
+
+ @Override
public void methodNeverReturnsNull(DexEncodedMethod method) {
// Ignored.
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
index 085137f..3ff6074 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
@@ -18,15 +18,13 @@
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenCustomHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectSortedMap.FastSortedEntrySet;
import java.util.ArrayList;
-import java.util.HashSet;
import java.util.List;
-import java.util.Set;
/**
* Canonicalize constants.
*/
public class ConstantCanonicalizer {
-
+ // Threshold to limit the number of constant canonicalization.
private static final int MAX_CANONICALIZED_CONSTANT = 15;
public static void canonicalize(AppView<?> appView, IRCode code) {
@@ -104,7 +102,7 @@
// Double-check the entry block does not have catch handlers.
// Otherwise, we need to split it before moving canonicalized const-string, which may throw.
assert !code.entryBlock().hasCatchHandlers();
- Position firstNonNonePosition = findFirstNonNonePosition(code);
+ final Position firstNonNonePosition = code.findFirstNonNonePosition();
FastSortedEntrySet<ConstInstruction, List<Value>> entries =
valuesDefinedByConstant.object2ObjectEntrySet();
// Sort the most frequently used constant first and exclude constant use only one time, such
@@ -156,30 +154,6 @@
it.add(canonicalizedConstant);
}
- private static Position findFirstNonNonePosition(IRCode code) {
- BasicBlock entryBlock = code.entryBlock();
- Instruction rightAfterArguments =
- entryBlock.listIterator().nextUntil(instr -> !instr.isArgument());
- Position firstNonArgumentPosition = rightAfterArguments.getPosition();
- Set<BasicBlock> visitedBlocks = new HashSet<>();
- while (rightAfterArguments != null) {
- // Make sure we are not looping.
- if (visitedBlocks.contains(rightAfterArguments.getBlock())) {
- break;
- }
- visitedBlocks.add(rightAfterArguments.getBlock());
- // The very first non-argument instruction can be chained via goto.
- if (rightAfterArguments.isGoto()) {
- rightAfterArguments = rightAfterArguments.asGoto().getTarget().getInstructions().getFirst();
- } else if (rightAfterArguments.getPosition().isSome()) {
- return rightAfterArguments.getPosition();
- } else {
- break;
- }
- }
- return firstNonArgumentPosition;
- }
-
private static boolean constantUsedByInvokeRange(ConstInstruction constant) {
for (Instruction user : constant.outValue().uniqueUsers()) {
if (user.isInvoke() && user.asInvoke().requiredArgumentRegisters() > 5) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizer.java b/src/main/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizer.java
index 2e31219..9b04a16 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizer.java
@@ -3,9 +3,12 @@
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.ir.optimize;
+import static com.android.tools.r8.optimize.MemberRebindingAnalysis.isMemberVisibleFromOriginalContext;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.ir.code.BasicBlock;
import com.android.tools.r8.ir.code.IRCode;
@@ -13,17 +16,19 @@
import com.android.tools.r8.ir.code.InstructionListIterator;
import com.android.tools.r8.ir.code.Invoke;
import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Position;
import com.android.tools.r8.ir.code.Value;
-import com.google.common.collect.ImmutableSet;
+import com.android.tools.r8.logging.Log;
+import com.android.tools.r8.utils.StringUtils;
import com.google.common.collect.Maps;
import it.unimi.dsi.fastutil.Hash.Strategy;
+import it.unimi.dsi.fastutil.objects.Object2IntArrayMap;
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenCustomHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectSortedMap.FastSortedEntrySet;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
/**
* Canonicalize idempotent function calls.
@@ -46,28 +51,36 @@
* // Update users of vx, vy, and vz.
*/
public class IdempotentFunctionCallCanonicalizer {
- private static final int MAX_CANONICALIZED_CALL = 7;
+ // Threshold to limit the number of invocation canonicalization.
+ private static final int MAX_CANONICALIZED_CALL = 15;
- private final Set<DexMethod> idempotentMethods;
+ private final AppView<?> appView;
+ private final DexItemFactory factory;
- public IdempotentFunctionCallCanonicalizer(DexItemFactory factory) {
- ImmutableSet.Builder<DexMethod> idempotentMethodsBuilder = ImmutableSet.builder();
- // Boxed Boxed#valueOf(Primitive), e.g., Boolean Boolean#valueOf(B)
- for (Entry<DexType, DexType> entry : factory.primitiveToBoxed.entrySet()) {
- DexType primitive = entry.getKey();
- DexType boxed = entry.getValue();
- idempotentMethodsBuilder.add(
- factory.createMethod(
- boxed.descriptor,
- factory.valueOfMethodName,
- boxed.descriptor,
- new DexString[] {primitive.descriptor}));
+ private int numberOfLibraryCallCanonicalization = 0;
+ private int numberOfProgramCallCanonicalization = 0;
+ private Object2IntMap<Long> histogramOfCanonicalizationCandidatesPerMethod = null;
+
+ public IdempotentFunctionCallCanonicalizer(AppView<?> appView) {
+ this.appView = appView;
+ this.factory = appView.dexItemFactory();
+ if (Log.ENABLED) {
+ histogramOfCanonicalizationCandidatesPerMethod = new Object2IntArrayMap<>();
}
- // TODO(b/119596718): More idempotent methods? Any singleton accessors? E.g.,
- // java.util.Calendar#getInstance(...) // 4 variants
- // java.util.Locale#getDefault() // returns JVM default locale.
- // android.os.Looper#myLooper() // returns the associated Looper instance.
- idempotentMethods = idempotentMethodsBuilder.build();
+ }
+
+ public void logResults() {
+ assert Log.ENABLED;
+ Log.info(getClass(),
+ "# invoke canonicalization (library): %s", numberOfLibraryCallCanonicalization);
+ Log.info(getClass(),
+ "# invoke canonicalization (program): %s", numberOfProgramCallCanonicalization);
+ assert histogramOfCanonicalizationCandidatesPerMethod != null;
+ Log.info(getClass(), "------ histogram of invoke canonicalization candidates ------");
+ histogramOfCanonicalizationCandidatesPerMethod.forEach((length, count) -> {
+ Log.info(getClass(),
+ "%s: %s (%s)", length, StringUtils.times("*", Math.min(count, 53)), count);
+ });
}
public void canonicalize(IRCode code) {
@@ -89,6 +102,7 @@
}
});
+ DexType context = code.method.method.holder;
// Collect invocations along with arguments.
for (BasicBlock block : code.blocks) {
InstructionListIterator it = block.listIterator();
@@ -98,10 +112,6 @@
continue;
}
InvokeMethod invoke = current.asInvokeMethod();
- // Interested in known-to-be idempotent method call.
- if (!idempotentMethods.contains(invoke.getInvokedMethod())) {
- continue;
- }
// If the out value of the current invocation is not used and removed, we don't care either.
if (invoke.outValue() == null) {
continue;
@@ -110,7 +120,36 @@
if (current.outValue().hasLocalInfo()) {
continue;
}
- assert !current.inValues().isEmpty();
+ DexMethod invokedMethod = invoke.getInvokedMethod();
+ // Interested in known-to-be idempotent methods.
+ if (!factory.libraryMethodsWithReturnValueDependingOnlyOnArguments.contains(invokedMethod)
+ || !factory.libraryMethodsWithoutSideEffects.contains(invokedMethod)) {
+ if (!appView.enableWholeProgramOptimizations()) {
+ // Give up in D8
+ continue;
+ }
+ assert appView.appInfo().hasLiveness();
+ // Check if the call has a single target; that target is side effect free; and
+ // that target's output depends only on arguments.
+ DexEncodedMethod target = invoke.lookupSingleTarget(appView.withLiveness(), context);
+ if (target == null
+ || target.getOptimizationInfo().mayHaveSideEffects()
+ || !target.getOptimizationInfo().returnValueOnlyDependsOnArguments()) {
+ continue;
+ }
+ // Verify that the target method is accessible in the current context.
+ if (!isMemberVisibleFromOriginalContext(
+ appView, context, target.method.holder, target.accessFlags)) {
+ continue;
+ }
+ // Check if the call could throw a NPE as a result of the receiver being null.
+ if (current.isInvokeMethodWithReceiver()) {
+ Value receiver = current.asInvokeMethodWithReceiver().getReceiver().getAliasedValue();
+ if (receiver.getTypeLattice().isNullable()) {
+ continue;
+ }
+ }
+ }
// TODO(b/119596718): Use dominant tree to extend it to non-canonicalized in values?
// For now, interested in inputs that are also canonicalized constants.
boolean invocationCanBeMovedToEntryBlock = true;
@@ -134,16 +173,32 @@
return;
}
+ // Double-check the entry block does not have catch handlers.
+ assert !code.entryBlock().hasCatchHandlers();
+
// InvokeMethod is not treated as dead code explicitly, i.e., cannot rely on dead code remover.
Map<InvokeMethod, Value> deadInvocations = Maps.newHashMap();
FastSortedEntrySet<InvokeMethod, List<Value>> entries = returnValues.object2ObjectEntrySet();
+ if (Log.ENABLED) {
+ Long numOfCandidates = entries.stream().filter(a -> a.getValue().size() > 1).count();
+ int count = histogramOfCanonicalizationCandidatesPerMethod.getOrDefault(numOfCandidates, 0);
+ histogramOfCanonicalizationCandidatesPerMethod.put(numOfCandidates, count + 1);
+ }
entries.stream()
.filter(a -> a.getValue().size() > 1)
.sorted((a, b) -> Integer.compare(b.getValue().size(), a.getValue().size()))
.limit(MAX_CANONICALIZED_CALL)
.forEach((entry) -> {
InvokeMethod invoke = entry.getKey();
+ if (Log.ENABLED) {
+ if (factory.libraryMethodsWithReturnValueDependingOnlyOnArguments
+ .contains(invoke.getInvokedMethod())) {
+ numberOfLibraryCallCanonicalization += entry.getValue().size() - 1;
+ } else {
+ numberOfProgramCallCanonicalization += entry.getValue().size() - 1;
+ }
+ }
Value canonicalizedValue = code.createValue(
invoke.outValue().getTypeLattice(), invoke.outValue().getLocalInfo());
Invoke canonicalizedInvoke =
@@ -153,7 +208,16 @@
null,
canonicalizedValue,
invoke.inValues());
- insertCanonicalizedInvoke(code, canonicalizedInvoke);
+ // Note that it is fine to use any position, since the invoke has no side effects, which
+ // is guaranteed not to throw. That is, we will never have a stack trace with this call.
+ // Nonetheless, here we pick the position of the very first invocation.
+ Position firstInvocationPosition = entry.getValue().get(0).definition.getPosition();
+ canonicalizedInvoke.setPosition(firstInvocationPosition);
+ if (invoke.inValues().size() > 0) {
+ insertCanonicalizedInvokeWithInValues(code, canonicalizedInvoke);
+ } else {
+ insertCanonicalizedInvokeWithoutInValues(code, canonicalizedInvoke);
+ }
for (Value oldOutValue : entry.getValue()) {
deadInvocations.put(oldOutValue.definition.asInvokeMethod(), canonicalizedValue);
}
@@ -182,7 +246,8 @@
assert code.isConsistentSSA();
}
- private static void insertCanonicalizedInvoke(IRCode code, Invoke canonicalizedInvoke) {
+ private static void insertCanonicalizedInvokeWithInValues(
+ IRCode code, Invoke canonicalizedInvoke) {
BasicBlock entryBlock = code.entryBlock();
// Insert the canonicalized invoke after in values.
int numberOfInValuePassed = 0;
@@ -193,7 +258,26 @@
numberOfInValuePassed++;
}
if (numberOfInValuePassed == canonicalizedInvoke.inValues().size()) {
- canonicalizedInvoke.setPosition(current.getPosition());
+ // If this invocation uses arguments and this iteration ends in the middle of Arguments,
+ // proceed further so that Arguments can be packed first (as per entry block's properties).
+ if (it.hasNext() && it.peekNext().isArgument()) {
+ it.nextUntil(instr -> !instr.isArgument());
+ }
+ break;
+ }
+ }
+ it.add(canonicalizedInvoke);
+ }
+
+ private static void insertCanonicalizedInvokeWithoutInValues(
+ IRCode code, Invoke canonicalizedInvoke) {
+ BasicBlock entryBlock = code.entryBlock();
+ // Insert the canonicalized invocation at the start of the block right after the argument
+ // instructions.
+ InstructionListIterator it = entryBlock.listIterator();
+ while (it.hasNext()) {
+ if (!it.next().isArgument()) {
+ it.previous();
break;
}
}
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 0964666..b456b95 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -163,6 +163,7 @@
public boolean enableClassStaticizer = true;
public boolean enableInitializedClassesAnalysis = true;
public boolean enableSideEffectAnalysis = true;
+ public boolean enableDeterminismAnalysis = true;
public boolean enableServiceLoaderRewriting = true;
// TODO(b/120138731): Enable this when it is worthwhile, e.g., combined with Class#forName.
public boolean enableNameReflectionOptimization = false;
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/DeterminismAnalysisTest.java b/src/test/java/com/android/tools/r8/ir/analysis/DeterminismAnalysisTest.java
new file mode 100644
index 0000000..1ea789d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/analysis/DeterminismAnalysisTest.java
@@ -0,0 +1,99 @@
+// 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.analysis;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.ir.code.IRCode;
+import java.util.function.Consumer;
+import org.junit.Test;
+
+public class DeterminismAnalysisTest extends AnalysisTestBase {
+
+ public DeterminismAnalysisTest() throws Exception {
+ super(TestClass.class.getTypeName(), TestClass.class);
+ }
+
+ @Test
+ public void testMax() throws Exception {
+ buildAndCheckIR("max", checkAnalysisResult(true));
+ }
+
+ @Test
+ public void testSget() throws Exception {
+ buildAndCheckIR("sget", checkAnalysisResult(false));
+ }
+
+ @Test
+ public void testIget() throws Exception {
+ buildAndCheckIR("iget", checkAnalysisResult(false));
+ }
+
+ @Test
+ public void testAget() throws Exception {
+ buildAndCheckIR("aget", checkAnalysisResult(false));
+ }
+
+ @Test
+ public void testAlength() throws Exception {
+ buildAndCheckIR("alength", checkAnalysisResult(false));
+ }
+
+ @Test
+ public void testCreateInstance() throws Exception {
+ buildAndCheckIR("createInstance", checkAnalysisResult(false));
+ }
+
+ @Test
+ public void testCreateArray() throws Exception {
+ buildAndCheckIR("createArray", checkAnalysisResult(false));
+ }
+
+ private Consumer<IRCode> checkAnalysisResult(boolean expectedResult) {
+ return code -> {
+ assertEquals(expectedResult,
+ DeterminismAnalysis.returnValueOnlyDependsOnArguments(appView.withLiveness(), code));
+ };
+ }
+
+ static class TestClass {
+
+ static int ID;
+ Object field;
+
+ static int max(int x, int y) {
+ return x > y ? x : y;
+ }
+
+ static int sget() {
+ return ID;
+ }
+
+ static Object iget(TestClass instance) {
+ return instance != null ? instance.field : null;
+ }
+
+ static TestClass aget(TestClass[] args, int i) {
+ return args != null ? args[i] : null;
+ }
+
+ static int alength(TestClass[] args) {
+ return args.length;
+ }
+
+ static TestClass createInstance(Object field) {
+ TestClass instance = new TestClass();
+ instance.field = field;
+ return instance;
+ }
+
+ static TestClass[] createArray(int size, Object[] fields) {
+ TestClass[] array = new TestClass[size];
+ for (int i = 0; i < size; i++) {
+ array[i].field = fields[i];
+ }
+ return array;
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizationTest.java
index 1cc0288..52c0a70 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/IdempotentFunctionCallCanonicalizationTest.java
@@ -9,6 +9,7 @@
import static org.junit.Assume.assumeTrue;
import com.android.tools.r8.D8TestRunResult;
+import com.android.tools.r8.NeverInline;
import com.android.tools.r8.R8TestRunResult;
import com.android.tools.r8.TestBase;
import com.android.tools.r8.TestParameters;
@@ -29,7 +30,40 @@
import org.junit.runners.Parameterized;
class IdempotentFunctionMain {
+ static int SEED;
+
+ @NeverInline
+ static int random(int arg) {
+ return SEED * arg;
+ }
+
+ @NeverInline
+ static int max(int x, int y) {
+ return x > y ? x : y;
+ }
+
public static void main(String[] args) {
+ {
+ SEED = 0;
+ System.out.print(random(2));
+ SEED = 1;
+ // Should not be canonicalized.
+ System.out.print(random(2));
+
+ System.out.print(max(1, 2));
+
+ System.out.print(max(3, 2));
+ // Different order of arguments. Not canonicalized.
+ System.out.print(max(2, 3));
+ // Canonicalized.
+ System.out.print(max(3, 2));
+
+ // Canonicalized.
+ System.out.print(max(1, 2));
+
+ System.out.println();
+ }
+
// Primitive will be boxed automatically.
{
Map<String, Boolean> map = new HashMap<>();
@@ -82,6 +116,7 @@
public class IdempotentFunctionCallCanonicalizationTest extends TestBase {
private static final Class<?> MAIN = IdempotentFunctionMain.class;
private static final String JAVA_OUTPUT = StringUtils.lines(
+ "0223332",
"true",
"false",
"8",
@@ -94,6 +129,8 @@
private static final int EXPECTED_BOOLEAN_VALUE_OF = 2;
private static final int EXPECTED_INTEGER_VALUE_OF = 2;
private static final int EXPECTED_LONG_VALUE_OF = 7;
+ private static final int EXPECTED_MAX_CALLS = 3;
+ private static final int TOTAL_MAX_CALLS = 5;
@Parameterized.Parameters(name = "{0}")
public static TestParametersCollection data() {
@@ -122,7 +159,7 @@
&& method.name.toString().equals("valueOf");
}
- private long countValueOf(MethodSubject method, String descriptor) {
+ private static long countValueOf(MethodSubject method, String descriptor) {
return Streams.stream(method.iterateInstructions(instructionSubject -> {
if (instructionSubject.isInvoke()) {
return isValueOf(instructionSubject.getMethod(), descriptor);
@@ -131,8 +168,18 @@
})).count();
}
+ private static long countMaxCall(MethodSubject method) {
+ return Streams.stream(method.iterateInstructions(instructionSubject -> {
+ if (instructionSubject.isInvoke()) {
+ return instructionSubject.getMethod().name.toString().equals("max");
+ }
+ return false;
+ })).count();
+ }
+
private void test(
TestRunResult result,
+ int expectedMaxCount,
int expectedBooleanValueOfCount,
int expectedIntValueOfCount,
int expectedLongValueOfCount) throws Exception {
@@ -140,6 +187,7 @@
ClassSubject mainClass = codeInspector.clazz(MAIN);
MethodSubject mainMethod = mainClass.mainMethod();
assertThat(mainMethod, isPresent());
+ assertEquals(expectedMaxCount, countMaxCall(mainMethod));
assertEquals(expectedBooleanValueOfCount, countValueOf(mainMethod, BOOLEAN_DESCRIPTOR));
assertEquals(expectedIntValueOfCount, countValueOf(mainMethod, INTEGER_DESCRIPTOR));
assertEquals(expectedLongValueOfCount, countValueOf(mainMethod, LONG_DESCRIPTOR));
@@ -156,7 +204,12 @@
.setMinApi(parameters.getRuntime())
.run(parameters.getRuntime(), MAIN)
.assertSuccessWithOutput(JAVA_OUTPUT);
- test(result, EXPECTED_BOOLEAN_VALUE_OF, EXPECTED_INTEGER_VALUE_OF, EXPECTED_LONG_VALUE_OF);
+ test(
+ result,
+ TOTAL_MAX_CALLS,
+ EXPECTED_BOOLEAN_VALUE_OF,
+ EXPECTED_INTEGER_VALUE_OF,
+ EXPECTED_LONG_VALUE_OF);
result =
testForD8()
@@ -165,7 +218,12 @@
.setMinApi(parameters.getRuntime())
.run(parameters.getRuntime(), MAIN)
.assertSuccessWithOutput(JAVA_OUTPUT);
- test(result, EXPECTED_BOOLEAN_VALUE_OF, EXPECTED_INTEGER_VALUE_OF, EXPECTED_LONG_VALUE_OF);
+ test(
+ result,
+ TOTAL_MAX_CALLS,
+ EXPECTED_BOOLEAN_VALUE_OF,
+ EXPECTED_INTEGER_VALUE_OF,
+ EXPECTED_LONG_VALUE_OF);
}
@Test
@@ -175,12 +233,19 @@
.addProgramClasses(MAIN)
.enableInliningAnnotations()
.addKeepMainRule(MAIN)
+ .noMinification()
.setMinApi(parameters.getRuntime())
.run(parameters.getRuntime(), MAIN)
.assertSuccessWithOutput(JAVA_OUTPUT);
+ int expectedMaxCount = parameters.isCfRuntime() ? TOTAL_MAX_CALLS : EXPECTED_MAX_CALLS;
int expectedBooleanValueOfCount = parameters.isCfRuntime() ? 6 : EXPECTED_BOOLEAN_VALUE_OF;
int expectedIntValueOfCount = parameters.isCfRuntime() ? 5 : EXPECTED_INTEGER_VALUE_OF;
int expectedLongValueOfCount = parameters.isCfRuntime() ? 10 : EXPECTED_LONG_VALUE_OF;
- test(result, expectedBooleanValueOfCount, expectedIntValueOfCount, expectedLongValueOfCount);
+ test(
+ result,
+ expectedMaxCount,
+ expectedBooleanValueOfCount,
+ expectedIntValueOfCount,
+ expectedLongValueOfCount);
}
}