blob: 6b854e6f7860877c631851fca16da8e5e607c984 [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 static com.android.tools.r8.ir.optimize.UninstantiatedTypeOptimization.Strategy.ALLOW_ARGUMENT_REMOVAL;
import static com.android.tools.r8.ir.optimize.UninstantiatedTypeOptimization.Strategy.DISALLOW_ARGUMENT_REMOVAL;
import com.android.tools.r8.graph.AppInfoWithSubtyping;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexProto;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.GraphLense.NestedGraphLense;
import com.android.tools.r8.graph.RewrittenPrototypeDescription;
import com.android.tools.r8.graph.RewrittenPrototypeDescription.ArgumentInfoCollection;
import com.android.tools.r8.graph.RewrittenPrototypeDescription.RemovedArgumentInfo;
import com.android.tools.r8.graph.TopDownClassHierarchyTraversal;
import com.android.tools.r8.ir.analysis.AbstractError;
import com.android.tools.r8.ir.analysis.TypeChecker;
import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
import com.android.tools.r8.ir.code.BasicBlock;
import com.android.tools.r8.ir.code.FieldInstruction;
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.InvokeMethod;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.optimize.MemberPoolCollection.MemberPool;
import com.android.tools.r8.logging.Log;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.utils.MethodSignatureEquivalence;
import com.android.tools.r8.utils.Timing;
import com.google.common.base.Equivalence.Wrapper;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import java.util.BitSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
public class UninstantiatedTypeOptimization {
enum Strategy {
ALLOW_ARGUMENT_REMOVAL,
DISALLOW_ARGUMENT_REMOVAL
}
public static class UninstantiatedTypeOptimizationGraphLense extends NestedGraphLense {
private final AppView<?> appView;
private final Map<DexMethod, ArgumentInfoCollection> removedArgumentsInfoPerMethod;
UninstantiatedTypeOptimizationGraphLense(
BiMap<DexMethod, DexMethod> methodMap,
Map<DexMethod, ArgumentInfoCollection> removedArgumentsInfoPerMethod,
AppView<?> appView) {
super(
ImmutableMap.of(),
methodMap,
ImmutableMap.of(),
null,
methodMap.inverse(),
appView.graphLense(),
appView.dexItemFactory());
this.appView = appView;
this.removedArgumentsInfoPerMethod = removedArgumentsInfoPerMethod;
}
@Override
public RewrittenPrototypeDescription lookupPrototypeChanges(DexMethod method) {
DexMethod originalMethod = originalMethodSignatures.getOrDefault(method, method);
RewrittenPrototypeDescription result = previousLense.lookupPrototypeChanges(originalMethod);
if (originalMethod != method) {
if (method.proto.returnType.isVoidType() && !originalMethod.proto.returnType.isVoidType()) {
result = result.withConstantReturn(originalMethod.proto.returnType, appView);
}
ArgumentInfoCollection removedArgumentsInfo = removedArgumentsInfoPerMethod.get(method);
if (removedArgumentsInfo != null) {
result = result.withRemovedArguments(removedArgumentsInfo);
}
} else {
assert !removedArgumentsInfoPerMethod.containsKey(method);
}
return result;
}
}
private static final MethodSignatureEquivalence equivalence = MethodSignatureEquivalence.get();
private final AppView<AppInfoWithLiveness> appView;
private final TypeChecker typeChecker;
private int numberOfInstanceGetOrInstancePutWithNullReceiver = 0;
private int numberOfArrayInstructionsWithNullArray = 0;
private int numberOfInvokesWithNullArgument = 0;
private int numberOfInvokesWithNullReceiver = 0;
private int numberOfMonitorWithNullReceiver = 0;
public UninstantiatedTypeOptimization(AppView<AppInfoWithLiveness> appView) {
this.appView = appView;
this.typeChecker = new TypeChecker(appView);
}
public UninstantiatedTypeOptimizationGraphLense run(
MethodPoolCollection methodPoolCollection, ExecutorService executorService, Timing timing) {
try {
methodPoolCollection.buildAll(executorService, timing);
} catch (Exception e) {
throw new RuntimeException(e);
}
Map<Wrapper<DexMethod>, Set<DexType>> changedVirtualMethods = new HashMap<>();
BiMap<DexMethod, DexMethod> methodMapping = HashBiMap.create();
Map<DexMethod, ArgumentInfoCollection> removedArgumentsInfoPerMethod = new IdentityHashMap<>();
TopDownClassHierarchyTraversal.forProgramClasses(appView)
.visit(
appView.appInfo().classes(),
clazz ->
processClass(
clazz,
changedVirtualMethods,
methodMapping,
methodPoolCollection,
removedArgumentsInfoPerMethod));
if (!methodMapping.isEmpty()) {
return new UninstantiatedTypeOptimizationGraphLense(
methodMapping, removedArgumentsInfoPerMethod, appView);
}
return null;
}
private void processClass(
DexProgramClass clazz,
Map<Wrapper<DexMethod>, Set<DexType>> changedVirtualMethods,
BiMap<DexMethod, DexMethod> methodMapping,
MethodPoolCollection methodPoolCollection,
Map<DexMethod, ArgumentInfoCollection> removedArgumentsInfoPerMethod) {
MemberPool<DexMethod> methodPool = methodPoolCollection.get(clazz);
if (clazz.isInterface()) {
// Do not allow changing the prototype of methods that override an interface method.
// This achieved by faking that there is already a method with the given signature.
for (DexEncodedMethod virtualMethod : clazz.virtualMethods()) {
RewrittenPrototypeDescription prototypeChanges =
RewrittenPrototypeDescription.createForUninstantiatedTypes(
virtualMethod.method,
appView,
getRemovedArgumentsInfo(virtualMethod, ALLOW_ARGUMENT_REMOVAL));
if (!prototypeChanges.isEmpty()) {
DexMethod newMethod = getNewMethodSignature(virtualMethod, prototypeChanges);
Wrapper<DexMethod> wrapper = equivalence.wrap(newMethod);
if (!methodPool.hasSeenDirectly(wrapper)) {
methodPool.seen(wrapper);
}
}
}
return;
}
Map<DexEncodedMethod, RewrittenPrototypeDescription> prototypeChangesPerMethod =
new IdentityHashMap<>();
for (DexEncodedMethod directMethod : clazz.directMethods()) {
RewrittenPrototypeDescription prototypeChanges =
getPrototypeChanges(directMethod, ALLOW_ARGUMENT_REMOVAL);
if (!prototypeChanges.isEmpty()) {
prototypeChangesPerMethod.put(directMethod, prototypeChanges);
}
}
// Reserve all signatures which are known to not be touched below.
Set<Wrapper<DexMethod>> usedSignatures = new HashSet<>();
for (DexEncodedMethod method : clazz.methods()) {
if (!prototypeChangesPerMethod.containsKey(method)) {
usedSignatures.add(equivalence.wrap(method.method));
}
}
// Change the return type of direct methods that return an uninstantiated type to void.
clazz
.getMethodCollection()
.replaceDirectMethods(
encodedMethod -> {
DexMethod method = encodedMethod.method;
RewrittenPrototypeDescription prototypeChanges =
prototypeChangesPerMethod.getOrDefault(
encodedMethod, RewrittenPrototypeDescription.none());
ArgumentInfoCollection removedArgumentsInfo =
prototypeChanges.getArgumentInfoCollection();
DexMethod newMethod = getNewMethodSignature(encodedMethod, prototypeChanges);
if (newMethod != method) {
Wrapper<DexMethod> wrapper = equivalence.wrap(newMethod);
// TODO(b/110806787): Can be extended to handle collisions by renaming the given
// method.
if (usedSignatures.add(wrapper)) {
methodMapping.put(method, newMethod);
if (removedArgumentsInfo.hasRemovedArguments()) {
removedArgumentsInfoPerMethod.put(newMethod, removedArgumentsInfo);
}
return encodedMethod.toTypeSubstitutedMethod(
newMethod,
removedArgumentsInfo.createParameterAnnotationsRemover(encodedMethod));
}
}
return encodedMethod;
});
// Change the return type of virtual methods that return an uninstantiated type to void.
// This is done in two steps. First we change the return type of all methods that override
// a method whose return type has already been changed to void previously. Note that
// all supertypes of the current class are always visited prior to the current class.
// This is important to ensure that a method that used to override a method in its super
// class will continue to do so after this optimization.
clazz
.getMethodCollection()
.replaceVirtualMethods(
encodedMethod -> {
DexMethod method = encodedMethod.method;
RewrittenPrototypeDescription prototypeChanges =
getPrototypeChanges(encodedMethod, DISALLOW_ARGUMENT_REMOVAL);
ArgumentInfoCollection removedArgumentsInfo =
prototypeChanges.getArgumentInfoCollection();
DexMethod newMethod = getNewMethodSignature(encodedMethod, prototypeChanges);
if (newMethod != method) {
Wrapper<DexMethod> wrapper = equivalence.wrap(newMethod);
boolean isOverrideOfPreviouslyChangedMethodInSuperClass =
changedVirtualMethods
.getOrDefault(equivalence.wrap(method), ImmutableSet.of())
.stream()
.anyMatch(other -> appView.appInfo().isSubtype(clazz.type, other));
if (isOverrideOfPreviouslyChangedMethodInSuperClass) {
assert methodPool.hasSeen(wrapper);
boolean signatureIsAvailable = usedSignatures.add(wrapper);
assert signatureIsAvailable;
methodMapping.put(method, newMethod);
return encodedMethod.toTypeSubstitutedMethod(
newMethod,
removedArgumentsInfo.createParameterAnnotationsRemover(encodedMethod));
}
}
return encodedMethod;
});
clazz
.getMethodCollection()
.replaceVirtualMethods(
encodedMethod -> {
DexMethod method = encodedMethod.method;
RewrittenPrototypeDescription prototypeChanges =
getPrototypeChanges(encodedMethod, DISALLOW_ARGUMENT_REMOVAL);
ArgumentInfoCollection removedArgumentsInfo =
prototypeChanges.getArgumentInfoCollection();
DexMethod newMethod = getNewMethodSignature(encodedMethod, prototypeChanges);
if (newMethod != method) {
Wrapper<DexMethod> wrapper = equivalence.wrap(newMethod);
// TODO(b/110806787): Can be extended to handle collisions by renaming the given
// method. Note that this also requires renaming all of the methods that override
// this
// method, though.
if (!methodPool.hasSeen(wrapper) && usedSignatures.add(wrapper)) {
methodPool.seen(wrapper);
methodMapping.put(method, newMethod);
boolean added =
changedVirtualMethods
.computeIfAbsent(
equivalence.wrap(method), key -> Sets.newIdentityHashSet())
.add(clazz.type);
assert added;
return encodedMethod.toTypeSubstitutedMethod(
newMethod,
removedArgumentsInfo.createParameterAnnotationsRemover(encodedMethod));
}
}
return encodedMethod;
});
}
private RewrittenPrototypeDescription getPrototypeChanges(
DexEncodedMethod encodedMethod, Strategy strategy) {
if (ArgumentRemovalUtils.isPinned(encodedMethod, appView)
|| appView.appInfo().keepConstantArguments.contains(encodedMethod.method)) {
return RewrittenPrototypeDescription.none();
}
return RewrittenPrototypeDescription.createForUninstantiatedTypes(
encodedMethod.method, appView, getRemovedArgumentsInfo(encodedMethod, strategy));
}
private ArgumentInfoCollection getRemovedArgumentsInfo(
DexEncodedMethod encodedMethod, Strategy strategy) {
if (strategy == DISALLOW_ARGUMENT_REMOVAL) {
return ArgumentInfoCollection.empty();
}
ArgumentInfoCollection.Builder argInfosBuilder = ArgumentInfoCollection.builder();
DexProto proto = encodedMethod.method.proto;
int offset = encodedMethod.isStatic() ? 0 : 1;
for (int i = 0; i < proto.parameters.size(); ++i) {
DexType type = proto.parameters.values[i];
if (type.isAlwaysNull(appView)) {
RemovedArgumentInfo removedArg =
RemovedArgumentInfo.builder().setIsAlwaysNull().setType(type).build();
argInfosBuilder.addArgumentInfo(i + offset, removedArg);
}
}
return argInfosBuilder.build();
}
private DexMethod getNewMethodSignature(
DexEncodedMethod encodedMethod, RewrittenPrototypeDescription prototypeChanges) {
DexItemFactory dexItemFactory = appView.dexItemFactory();
DexMethod method = encodedMethod.method;
DexProto newProto = prototypeChanges.rewriteProto(encodedMethod, dexItemFactory);
return dexItemFactory.createMethod(method.holder, newProto, method.name);
}
public void rewrite(IRCode code) {
AssumeDynamicTypeRemover assumeDynamicTypeRemover = new AssumeDynamicTypeRemover(appView, code);
Set<BasicBlock> blocksToBeRemoved = Sets.newIdentityHashSet();
ListIterator<BasicBlock> blockIterator = code.listIterator();
Set<Value> valuesToNarrow = Sets.newIdentityHashSet();
while (blockIterator.hasNext()) {
BasicBlock block = blockIterator.next();
if (blocksToBeRemoved.contains(block)) {
continue;
}
InstructionListIterator instructionIterator = block.listIterator(code);
while (instructionIterator.hasNext()) {
Instruction instruction = instructionIterator.next();
if (instruction.throwsOnNullInput()) {
Value couldBeNullValue = instruction.getNonNullInput();
if (isThrowNullCandidate(couldBeNullValue, instruction, appView, code.method.holder())) {
if (instruction.isInstanceGet() || instruction.isInstancePut()) {
++numberOfInstanceGetOrInstancePutWithNullReceiver;
} else if (instruction.isInvokeMethodWithReceiver()) {
++numberOfInvokesWithNullReceiver;
} else if (instruction.isArrayGet()
|| instruction.isArrayPut()
|| instruction.isArrayLength()) {
++numberOfArrayInstructionsWithNullArray;
} else if (instruction.isMonitor()) {
++numberOfMonitorWithNullReceiver;
} else {
assert false;
}
instructionIterator.replaceCurrentInstructionWithThrowNull(
appView, code, blockIterator, blocksToBeRemoved, valuesToNarrow);
continue;
}
}
if (instruction.isFieldInstruction()) {
rewriteFieldInstruction(
instruction.asFieldInstruction(),
blockIterator,
instructionIterator,
code,
assumeDynamicTypeRemover,
valuesToNarrow);
} else if (instruction.isInvokeMethod()) {
rewriteInvoke(
instruction.asInvokeMethod(),
blockIterator,
instructionIterator,
code,
assumeDynamicTypeRemover,
blocksToBeRemoved,
valuesToNarrow);
}
}
}
assumeDynamicTypeRemover.removeMarkedInstructions(blocksToBeRemoved).finish();
code.removeBlocks(blocksToBeRemoved);
code.removeAllDeadAndTrivialPhis(valuesToNarrow);
code.removeUnreachableBlocks();
if (!valuesToNarrow.isEmpty()) {
new TypeAnalysis(appView).narrowing(valuesToNarrow);
}
assert code.isConsistentSSA();
}
private static boolean isThrowNullCandidate(
Value couldBeNullValue,
Instruction current,
AppView<? extends AppInfoWithSubtyping> appView,
DexType context) {
if (!couldBeNullValue.isAlwaysNull(appView)) {
return false;
}
if (current.isFieldInstruction()) {
// Other resolution-related errors come first.
AbstractError abstractError =
current.asFieldInstruction().instructionInstanceCanThrow(appView, context);
if (abstractError.isThrowing()
&& abstractError.getSpecificError(appView.dexItemFactory())
!= appView.dexItemFactory().npeType) {
// We can't replace the current instruction with `throw null` if it may throw another
// Error/Exception than NullPointerException.
return false;
}
}
return true;
}
public void logResults() {
assert Log.ENABLED;
Log.info(
getClass(),
"Number of instance-get/instance-put with null receiver: %s",
numberOfInstanceGetOrInstancePutWithNullReceiver);
Log.info(
getClass(),
"Number of array instructions with null reference: %s",
numberOfArrayInstructionsWithNullArray);
Log.info(
getClass(), "Number of invokes with null argument: %s", numberOfInvokesWithNullArgument);
Log.info(
getClass(), "Number of invokes with null receiver: %s", numberOfInvokesWithNullReceiver);
Log.info(
getClass(), "Number of monitor with null receiver: %s", numberOfMonitorWithNullReceiver);
}
// instance-{get|put} with a null receiver has already been rewritten to `throw null`.
// At this point, field-instruction whose target field type is uninstantiated will be handled.
private void rewriteFieldInstruction(
FieldInstruction instruction,
ListIterator<BasicBlock> blockIterator,
InstructionListIterator instructionIterator,
IRCode code,
AssumeDynamicTypeRemover assumeDynamicTypeRemover,
Set<Value> affectedValues) {
DexType context = code.method.holder();
DexField field = instruction.getField();
DexType fieldType = field.type;
if (fieldType.isAlwaysNull(appView)) {
// TODO(b/123857022): Should be possible to use definitionFor().
DexEncodedField encodedField = appView.appInfo().resolveField(field);
if (encodedField == null) {
return;
}
boolean instructionCanBeRemoved =
instruction.instructionInstanceCanThrow(appView, context).isThrowing();
BasicBlock block = instruction.getBlock();
if (instruction.isFieldPut()) {
if (!typeChecker.checkFieldPut(instruction)) {
// Broken type hierarchy. See FieldTypeTest#test_brokenTypeHierarchy.
assert appView.options().testing.allowTypeErrors;
return;
}
// We know that the right-hand side must be null, so this is a no-op.
if (instructionCanBeRemoved) {
instructionIterator.removeOrReplaceByDebugLocalRead();
}
} else {
if (instructionCanBeRemoved) {
// Replace the field read by the constant null.
assumeDynamicTypeRemover.markUsersForRemoval(instruction.outValue());
affectedValues.addAll(instruction.outValue().affectedValues());
instructionIterator.replaceCurrentInstruction(code.createConstNull());
} else {
replaceOutValueByNull(
instruction, instructionIterator, code, assumeDynamicTypeRemover, affectedValues);
}
}
if (block.hasCatchHandlers()) {
// This block can no longer throw.
block.getCatchHandlers().getUniqueTargets().forEach(BasicBlock::unlinkCatchHandler);
}
}
}
// invoke instructions with a null receiver has already been rewritten to `throw null`.
// At this point, we attempt to explore non-null-param-or-throw optimization info and replace
// the invocation with `throw null` if an argument is known to be null and the method is going to
// throw for that null argument.
private void rewriteInvoke(
InvokeMethod invoke,
ListIterator<BasicBlock> blockIterator,
InstructionListIterator instructionIterator,
IRCode code,
AssumeDynamicTypeRemover assumeDynamicTypeRemover,
Set<BasicBlock> blocksToBeRemoved,
Set<Value> affectedValues) {
DexEncodedMethod target = invoke.lookupSingleTarget(appView, code.method.holder());
if (target == null) {
return;
}
BitSet facts = target.getOptimizationInfo().getNonNullParamOrThrow();
if (facts != null) {
for (int i = 0; i < invoke.arguments().size(); i++) {
Value argument = invoke.arguments().get(i);
if (argument.isAlwaysNull(appView) && facts.get(i)) {
instructionIterator.replaceCurrentInstructionWithThrowNull(
appView, code, blockIterator, blocksToBeRemoved, affectedValues);
++numberOfInvokesWithNullArgument;
return;
}
}
}
DexType returnType = target.method.proto.returnType;
if (returnType.isAlwaysNull(appView)) {
replaceOutValueByNull(
invoke, instructionIterator, code, assumeDynamicTypeRemover, affectedValues);
}
}
private void replaceOutValueByNull(
Instruction instruction,
InstructionListIterator instructionIterator,
IRCode code,
AssumeDynamicTypeRemover assumeDynamicTypeRemover,
Set<Value> affectedValues) {
assert instructionIterator.peekPrevious() == instruction;
if (instruction.hasOutValue()) {
Value outValue = instruction.outValue();
if (outValue.numberOfAllUsers() > 0) {
assumeDynamicTypeRemover.markUsersForRemoval(outValue);
instructionIterator.previous();
affectedValues.addAll(outValue.affectedValues());
outValue.replaceUsers(
instructionIterator.insertConstNullInstruction(code, appView.options()));
instructionIterator.next();
}
}
}
}