blob: 50443c6e770804290db8b9b59bc2428599fab9ed [file] [log] [blame]
// Copyright (c) 2018, 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;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexClassAndMethod;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.graph.ResolutionResult.SingleResolutionResult;
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.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.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
import com.android.tools.r8.logging.Log;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
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;
/**
* Canonicalize idempotent function calls.
*
* <p>For example,
*
* v1 <- const4 0x0
* ...
* vx <- invoke-static { v1 } Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean;
* ...
* vy <- invoke-static { v1 } Ljava/lang/Boolean;->valueOf(Z);Ljava/lang/Boolean;
* ...
* vz <- invoke-static { v1 } Ljava/lang/Boolean;->valueOf(Z);Ljava/lang/Boolean;
* ...
*
* ~>
*
* v1 <- const4 0x0
* v2 <- invoke-static { v1 } Ljava/lang/Boolean;->valueOf(Z);Ljava/lang/Boolean;
* // Update users of vx, vy, and vz.
*/
public class IdempotentFunctionCallCanonicalizer {
// Threshold to limit the number of invocation canonicalization.
private static final int MAX_CANONICALIZED_CALL = 15;
private final AppView<?> appView;
private final DexItemFactory factory;
private int numberOfLibraryCallCanonicalization = 0;
private int numberOfProgramCallCanonicalization = 0;
private final Object2IntMap<Long> histogramOfCanonicalizationCandidatesPerMethod;
public IdempotentFunctionCallCanonicalizer(AppView<?> appView) {
this.appView = appView;
this.factory = appView.dexItemFactory();
if (Log.ENABLED) {
histogramOfCanonicalizationCandidatesPerMethod = new Object2IntArrayMap<>();
} else {
histogramOfCanonicalizationCandidatesPerMethod = null;
}
}
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) {
Object2ObjectLinkedOpenCustomHashMap<InvokeMethod, List<Value>> returnValues =
new Object2ObjectLinkedOpenCustomHashMap<>(
new Strategy<InvokeMethod>() {
@Override
public int hashCode(InvokeMethod o) {
return o.getInvokedMethod().hashCode() * 31 + o.inValues().hashCode();
}
@Override
public boolean equals(InvokeMethod a, InvokeMethod b) {
assert a == null || !a.outValue().hasLocalInfo();
assert b == null || !b.outValue().hasLocalInfo();
return a == b
|| (a != null && b != null && a.identicalNonValueNonPositionParts(b)
&& a.inValues().equals(b.inValues()));
}
});
ProgramMethod context = code.context();
// Collect invocations along with arguments.
for (BasicBlock block : code.blocks) {
for (Instruction current : block.getInstructions()) {
if (!current.isInvokeMethod()) {
continue;
}
InvokeMethod invoke = current.asInvokeMethod();
// If the out value of the current invocation is not used and removed, we don't care either.
if (invoke.outValue() == null) {
continue;
}
// Invocations with local info cannot be canonicalized.
if (current.outValue().hasLocalInfo()) {
continue;
}
// Interested in known-to-be idempotent methods.
if (!isIdempotentLibraryMethodInvoke(invoke)) {
if (!appView.enableWholeProgramOptimizations()) {
// Give up in D8
continue;
}
assert appView.appInfo().hasLiveness();
AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
AppInfoWithLiveness appInfoWithLiveness = appViewWithLiveness.appInfo();
SingleResolutionResult resolutionResult =
appInfoWithLiveness
.resolveMethod(invoke.getInvokedMethod(), invoke.getInterfaceBit())
.asSingleResolution();
if (resolutionResult == null
|| resolutionResult
.isAccessibleFrom(context, appInfoWithLiveness)
.isPossiblyFalse()) {
continue;
}
// Check if the call has a single target; that target is side effect free; and
// that target's output depends only on arguments.
// TODO(b/156853206): This should either (i) use the resolution result from above, (ii)
// return the resolution result such that the call site can perform the accessibility
// check, or (iii) always perform the accessibility check such that it can be skipped
// at the call site.
DexClassAndMethod target = invoke.lookupSingleTarget(appViewWithLiveness, context);
if (target == null) {
continue;
}
MethodOptimizationInfo optimizationInfo = target.getDefinition().getOptimizationInfo();
if (optimizationInfo.mayHaveSideEffects()
|| !optimizationInfo.returnValueOnlyDependsOnArguments()) {
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.getType().isNullable()) {
continue;
}
}
}
// TODO(b/145259212): Use dominant tree to extend it to non-canonicalized in values?
// For now, interested in inputs that are also canonicalized constants.
boolean invocationCanBeMovedToEntryBlock = true;
for (Value in : current.inValues()) {
if (in.isPhi()
|| !in.definition.isConstInstruction()
|| in.definition.getBlock().getNumber() != 0) {
invocationCanBeMovedToEntryBlock = false;
break;
}
}
if (!invocationCanBeMovedToEntryBlock) {
continue;
}
List<Value> oldReturnValues = returnValues.computeIfAbsent(invoke, k -> new ArrayList<>());
oldReturnValues.add(current.outValue());
}
}
if (returnValues.isEmpty()) {
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 && Log.isLoggingEnabledFor(IdempotentFunctionCallCanonicalizer.class)) {
Long numOfCandidates = entries.stream().filter(a -> a.getValue().size() > 1).count();
synchronized (histogramOfCanonicalizationCandidatesPerMethod) {
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.getOutType(), invoke.outValue().getLocalInfo());
Invoke canonicalizedInvoke =
Invoke.create(
invoke.getType(),
invoke.getInvokedMethod(),
null,
canonicalizedValue,
invoke.inValues());
// 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);
}
});
if (!deadInvocations.isEmpty()) {
for (BasicBlock block : code.blocks) {
InstructionListIterator it = block.listIterator(code);
while (it.hasNext()) {
Instruction current = it.next();
if (!current.isInvokeMethod()) {
continue;
}
InvokeMethod invoke = current.asInvokeMethod();
if (deadInvocations.containsKey(invoke)) {
Value newOutValue = deadInvocations.get(invoke);
assert newOutValue != null;
invoke.outValue().replaceUsers(newOutValue);
it.removeOrReplaceByDebugLocalRead();
}
}
}
}
code.removeAllDeadAndTrivialPhis();
assert code.isConsistentSSA();
}
private boolean isIdempotentLibraryMethodInvoke(InvokeMethod invoke) {
DexMethod invokedMethod = invoke.getInvokedMethod();
return appView
.getLibraryMethodSideEffectModelCollection()
.isCallToSideEffectFreeFinalMethod(invoke)
&& factory.libraryMethodsWithReturnValueDependingOnlyOnArguments.contains(invokedMethod);
}
private static void insertCanonicalizedInvokeWithInValues(
IRCode code, Invoke canonicalizedInvoke) {
BasicBlock entryBlock = code.entryBlock();
// Insert the canonicalized invoke after in values.
int numberOfInValuePassed = 0;
InstructionListIterator it = entryBlock.listIterator(code);
while (it.hasNext()) {
Instruction current = it.next();
if (current.hasOutValue()) {
for (Value inValue : canonicalizedInvoke.inValues()) {
if (inValue == current.outValue()) {
numberOfInValuePassed++;
}
}
}
if (numberOfInValuePassed == canonicalizedInvoke.inValues().size()) {
// 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(code);
while (it.hasNext()) {
if (!it.next().isArgument()) {
it.previous();
break;
}
}
it.add(canonicalizedInvoke);
}
}