| // 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); |
| } |
| } |