blob: 5f6ad1a021ffe219e823fea288624574fc7020dd [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.classinliner;
import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
import com.android.tools.r8.errors.InternalCompilerError;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.AccessControl;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexClassAndMethod;
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.FieldResolutionResult.SuccessfulFieldResolutionResult;
import com.android.tools.r8.graph.LibraryMethod;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.graph.ResolutionResult;
import com.android.tools.r8.graph.ResolutionResult.SingleResolutionResult;
import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
import com.android.tools.r8.ir.analysis.type.Nullability;
import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
import com.android.tools.r8.ir.analysis.value.AbstractValue;
import com.android.tools.r8.ir.analysis.value.ObjectState;
import com.android.tools.r8.ir.analysis.value.SingleConstValue;
import com.android.tools.r8.ir.code.AliasedValueConfiguration;
import com.android.tools.r8.ir.code.AssumeAndCheckCastAliasedValueConfiguration;
import com.android.tools.r8.ir.code.BasicBlock;
import com.android.tools.r8.ir.code.CheckCast;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.If;
import com.android.tools.r8.ir.code.InstanceGet;
import com.android.tools.r8.ir.code.InstancePut;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InstructionListIterator;
import com.android.tools.r8.ir.code.InstructionOrPhi;
import com.android.tools.r8.ir.code.InvokeDirect;
import com.android.tools.r8.ir.code.InvokeMethod;
import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
import com.android.tools.r8.ir.code.Phi;
import com.android.tools.r8.ir.code.StaticGet;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.conversion.MethodProcessor;
import com.android.tools.r8.ir.optimize.Inliner;
import com.android.tools.r8.ir.optimize.Inliner.InliningInfo;
import com.android.tools.r8.ir.optimize.Inliner.Reason;
import com.android.tools.r8.ir.optimize.InliningOracle;
import com.android.tools.r8.ir.optimize.classinliner.ClassInliner.EligibilityStatus;
import com.android.tools.r8.ir.optimize.classinliner.analysis.NonEmptyParameterUsage;
import com.android.tools.r8.ir.optimize.classinliner.analysis.ParameterUsage;
import com.android.tools.r8.ir.optimize.classinliner.constraint.ClassInlinerMethodConstraint;
import com.android.tools.r8.ir.optimize.info.FieldOptimizationInfo;
import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
import com.android.tools.r8.ir.optimize.inliner.InliningIRProvider;
import com.android.tools.r8.ir.optimize.inliner.NopWhyAreYouNotInliningReporter;
import com.android.tools.r8.kotlin.KotlinClassLevelInfo;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.utils.OptionalBool;
import com.android.tools.r8.utils.SetUtils;
import com.android.tools.r8.utils.Timing;
import com.android.tools.r8.utils.WorkList;
import com.android.tools.r8.utils.collections.ProgramMethodSet;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.function.Supplier;
final class InlineCandidateProcessor {
private static final AliasedValueConfiguration aliasesThroughAssumeAndCheckCasts =
AssumeAndCheckCastAliasedValueConfiguration.getInstance();
private final AppView<AppInfoWithLiveness> appView;
private final DexItemFactory dexItemFactory;
private final Inliner inliner;
private final Function<DexProgramClass, EligibilityStatus> isClassEligible;
private final MethodProcessor methodProcessor;
private final ProgramMethod method;
private final Instruction root;
private Value eligibleInstance;
private DexProgramClass eligibleClass;
private ObjectState objectState;
private final Map<InvokeMethod, InliningInfo> directMethodCalls = new IdentityHashMap<>();
private final ProgramMethodSet indirectMethodCallsOnInstance = ProgramMethodSet.create();
private final Map<InvokeMethod, ProgramMethod> directInlinees = new IdentityHashMap<>();
private final List<ProgramMethod> indirectInlinees = new ArrayList<>();
// Sets of values that must/may be an alias of the "root" instance (including the root instance
// itself).
private final ClassInlinerReceiverSet receivers;
InlineCandidateProcessor(
AppView<AppInfoWithLiveness> appView,
Inliner inliner,
Function<DexProgramClass, EligibilityStatus> isClassEligible,
MethodProcessor methodProcessor,
ProgramMethod method,
Instruction root) {
this.appView = appView;
this.dexItemFactory = appView.dexItemFactory();
this.inliner = inliner;
this.isClassEligible = isClassEligible;
this.method = method;
this.root = root;
this.methodProcessor = methodProcessor;
this.receivers = new ClassInlinerReceiverSet(root.outValue());
}
DexProgramClass getEligibleClass() {
return eligibleClass;
}
Map<InvokeMethod, ProgramMethod> getDirectInlinees() {
return directInlinees;
}
List<ProgramMethod> getIndirectInlinees() {
return indirectInlinees;
}
ClassInlinerReceiverSet getReceivers() {
return receivers;
}
// Checks if the root instruction defines eligible value, i.e. the value
// exists and we have a definition of its class.
EligibilityStatus isInstanceEligible() {
eligibleInstance = root.outValue();
if (eligibleInstance == null) {
return EligibilityStatus.NOT_ELIGIBLE;
}
if (root.isNewInstance()) {
eligibleClass = asProgramClassOrNull(appView.definitionFor(root.asNewInstance().clazz));
if (eligibleClass == null) {
return EligibilityStatus.NOT_ELIGIBLE;
}
if (method.getHolder() == eligibleClass) {
return EligibilityStatus.NOT_ELIGIBLE;
}
if (eligibleClass.classInitializationMayHaveSideEffectsInContext(appView, method)) {
return EligibilityStatus.NOT_ELIGIBLE;
}
return EligibilityStatus.ELIGIBLE;
}
assert root.isStaticGet();
StaticGet staticGet = root.asStaticGet();
SuccessfulFieldResolutionResult fieldResolutionResult =
appView.appInfo().resolveField(staticGet.getField()).asSuccessfulResolution();
if (fieldResolutionResult == null) {
return EligibilityStatus.NOT_ELIGIBLE;
}
if (method.getHolder() == fieldResolutionResult.getResolvedHolder()) {
return EligibilityStatus.NOT_ELIGIBLE;
}
if (staticGet.instructionMayHaveSideEffects(appView, method)) {
return EligibilityStatus.NOT_ELIGIBLE;
}
DexEncodedField field = fieldResolutionResult.getResolvedField();
FieldOptimizationInfo optimizationInfo = field.getOptimizationInfo();
ClassTypeElement dynamicLowerBoundType = optimizationInfo.getDynamicLowerBoundType();
if (dynamicLowerBoundType == null
|| !dynamicLowerBoundType.equals(optimizationInfo.getDynamicUpperBoundType())) {
return EligibilityStatus.NOT_ELIGIBLE;
}
eligibleClass =
asProgramClassOrNull(appView.definitionFor(dynamicLowerBoundType.getClassType()));
if (eligibleClass == null) {
return EligibilityStatus.NOT_ELIGIBLE;
}
AbstractValue abstractValue = optimizationInfo.getAbstractValue();
objectState =
abstractValue.isSingleFieldValue()
? abstractValue.asSingleFieldValue().getState()
: ObjectState.empty();
return EligibilityStatus.ELIGIBLE;
}
// Checks if the class is eligible and is properly used. Regarding general class
// eligibility rules see comment on computeClassEligible(...).
//
// In addition to class being eligible this method also checks:
// -- for 'new-instance' root:
// * class itself does not have static initializer
// -- for 'static-get' root:
// * class does not have instance fields
// * class is final
// * class has class initializer marked as TrivialClassInitializer, and
// class initializer initializes the field we are reading here.
EligibilityStatus isClassAndUsageEligible() {
return isClassEligible.apply(eligibleClass);
}
/**
* Checks if the inlining candidate instance users are eligible, see comment on {@link
* ClassInliner#processMethodCode}.
*
* @return null if all users are eligible, or the first ineligible user.
*/
InstructionOrPhi areInstanceUsersEligible(Supplier<InliningOracle> defaultOracle) {
// No Phi users.
if (eligibleInstance.hasPhiUsers()) {
return eligibleInstance.firstPhiUser(); // Not eligible.
}
Set<Instruction> currentUsers = eligibleInstance.uniqueUsers();
while (!currentUsers.isEmpty()) {
Set<Instruction> indirectUsers = Sets.newIdentityHashSet();
for (Instruction user : currentUsers) {
if (user.isAssume() || user.isCheckCast()) {
if (user.isCheckCast()) {
CheckCast checkCast = user.asCheckCast();
// TODO(b/175863158): Allow unsafe casts by rewriting into throw new ClassCastException.
boolean isCheckCastUnsafe =
!checkCast.getType().isClassType()
|| !appView.appInfo().isSubtype(eligibleClass.type, checkCast.getType());
if (isCheckCastUnsafe) {
return user; // Not eligible.
}
}
Value alias = user.outValue();
if (receivers.isReceiverAlias(alias)) {
continue; // Already processed.
}
if (alias.hasPhiUsers()) {
return alias.firstPhiUser(); // Not eligible.
}
if (!receivers.addReceiverAlias(alias)) {
return user; // Not eligible.
}
indirectUsers.addAll(alias.uniqueUsers());
continue;
}
if (user.isInstanceGet()) {
DexEncodedField field =
appView
.appInfo()
.resolveField(user.asFieldInstruction().getField())
.getResolvedField();
if (field == null || field.isStatic()) {
return user; // Not eligible.
}
if (root.isStaticGet()
&& !objectState.hasMaterializableFieldValueThatMatches(
appView, field, method, AbstractValue::isSingleConstValue)) {
return user; // Not eligible.
}
continue;
}
if (user.isInstancePut()) {
if (root.isStaticGet()) {
// We can't remove instructions that mutate the singleton instance.
return user; // Not eligible.
}
if (!receivers.addIllegalReceiverAlias(user.asInstancePut().value())) {
return user; // Not eligible.
}
DexEncodedField field =
appView
.appInfo()
.resolveField(user.asFieldInstruction().getField())
.getResolvedField();
if (field == null || field.isStatic()) {
return user; // Not eligible.
}
continue;
}
if (user.isInvokeMethod()) {
InvokeMethod invoke = user.asInvokeMethod();
SingleResolutionResult resolutionResult =
appView
.appInfo()
.resolveMethod(invoke.getInvokedMethod(), invoke.getInterfaceBit())
.asSingleResolution();
if (resolutionResult == null
|| resolutionResult.isAccessibleFrom(method, appView).isPossiblyFalse()) {
return user; // Not eligible.
}
// TODO(b/156853206): Avoid duplicating resolution.
DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, method);
if (singleTarget == null) {
return user; // Not eligible.
}
if (singleTarget.isLibraryMethod()
&& isEligibleLibraryMethodCall(invoke, singleTarget.asLibraryMethod())) {
continue;
}
ProgramMethod singleProgramTarget = singleTarget.asProgramMethod();
if (!isEligibleSingleTarget(singleProgramTarget)) {
return user; // Not eligible.
}
if (AccessControl.isClassAccessible(singleProgramTarget.getHolder(), method, appView)
.isPossiblyFalse()) {
return user; // Not eligible.
}
// Eligible constructor call (for new instance roots only).
if (user.isInvokeConstructor(dexItemFactory)) {
InvokeDirect invokeDirect = user.asInvokeDirect();
boolean isCorrespondingConstructorCall =
root.isNewInstance() && root.outValue() == invokeDirect.getReceiver();
if (isCorrespondingConstructorCall) {
InliningInfo inliningInfo =
isEligibleConstructorCall(invokeDirect, singleProgramTarget);
if (inliningInfo != null) {
directMethodCalls.put(invoke, inliningInfo);
continue;
}
}
return user; // Not eligible.
}
// Eligible non-constructor method call.
if (isEligibleDirectMethodCall(
invoke, resolutionResult, singleProgramTarget, defaultOracle, indirectUsers)) {
continue;
}
return user; // Not eligible.
}
// Allow some IF instructions.
if (user.isIf()) {
If ifInsn = user.asIf();
If.Type type = ifInsn.getType();
if (ifInsn.isZeroTest() && (type == If.Type.EQ || type == If.Type.NE)) {
// Allow ==/!= null tests, we know that the instance is a non-null value.
continue;
}
}
return user; // Not eligible.
}
currentUsers = indirectUsers;
}
return null; // Eligible.
}
// Process inlining, includes the following steps:
//
// * remove linked assume instructions if any so that users of the eligible field are up-to-date.
// * inline extra methods if any, collect new direct method calls
// * inline direct methods if any
// * remove superclass initializer call and field reads
// * remove field writes
// * remove root instruction
//
// Returns `true` if at least one method was inlined.
boolean processInlining(
IRCode code,
Set<Value> affectedValues,
InliningIRProvider inliningIRProvider)
throws IllegalClassInlinerStateException {
// Verify that `eligibleInstance` is not aliased.
assert eligibleInstance == eligibleInstance.getAliasedValue();
boolean anyInlinedMethods = forceInlineDirectMethodInvocations(code, inliningIRProvider);
anyInlinedMethods |= forceInlineIndirectMethodInvocations(code, inliningIRProvider);
rebindIndirectEligibleInstanceUsersFromPhis();
removeMiscUsages(code, affectedValues);
removeFieldReads(code);
removeFieldWrites();
removeInstruction(root);
return anyInlinedMethods;
}
private boolean forceInlineDirectMethodInvocations(
IRCode code, InliningIRProvider inliningIRProvider) throws IllegalClassInlinerStateException {
if (directMethodCalls.isEmpty()) {
return false;
}
inliner.performForcedInlining(
method, code, directMethodCalls, inliningIRProvider, Timing.empty());
// In case we are class inlining an object allocation that does not inherit directly from
// java.lang.Object, we need keep force inlining the constructor until we reach
// java.lang.Object.<init>().
if (root.isNewInstance()) {
do {
directMethodCalls.clear();
for (Instruction instruction : eligibleInstance.uniqueUsers()) {
if (instruction.isInvokeDirect()) {
InvokeDirect invoke = instruction.asInvokeDirect();
Value receiver = invoke.getReceiver().getAliasedValue();
if (receiver != eligibleInstance) {
continue;
}
DexMethod invokedMethod = invoke.getInvokedMethod();
if (invokedMethod == dexItemFactory.objectMembers.constructor) {
continue;
}
if (!dexItemFactory.isConstructor(invokedMethod)) {
throw new IllegalClassInlinerStateException();
}
DexProgramClass holder =
asProgramClassOrNull(appView.definitionForHolder(invokedMethod, method));
if (holder == null) {
throw new IllegalClassInlinerStateException();
}
ProgramMethod singleTarget = holder.lookupProgramMethod(invokedMethod);
if (singleTarget == null
|| !singleTarget
.getDefinition()
.isInliningCandidate(
method,
Reason.ALWAYS,
appView.appInfo(),
NopWhyAreYouNotInliningReporter.getInstance())) {
throw new IllegalClassInlinerStateException();
}
directMethodCalls.put(invoke, new InliningInfo(singleTarget, eligibleClass.type));
break;
}
}
if (!directMethodCalls.isEmpty()) {
inliner.performForcedInlining(
method, code, directMethodCalls, inliningIRProvider, Timing.empty());
}
} while (!directMethodCalls.isEmpty());
}
return true;
}
private boolean forceInlineIndirectMethodInvocations(
IRCode code, InliningIRProvider inliningIRProvider) throws IllegalClassInlinerStateException {
if (indirectMethodCallsOnInstance.isEmpty()) {
return false;
}
Map<InvokeMethodWithReceiver, InliningInfo> methodCallsOnInstance = new IdentityHashMap<>();
Set<Instruction> currentUsers = eligibleInstance.uniqueUsers();
while (!currentUsers.isEmpty()) {
Set<Instruction> indirectOutValueUsers = Sets.newIdentityHashSet();
for (Instruction instruction : currentUsers) {
if (instruction.isAssume() || instruction.isCheckCast()) {
indirectOutValueUsers.addAll(instruction.outValue().uniqueUsers());
continue;
}
if (instruction.isInvokeMethodWithReceiver()) {
InvokeMethodWithReceiver invoke = instruction.asInvokeMethodWithReceiver();
DexMethod invokedMethod = invoke.getInvokedMethod();
if (invokedMethod == dexItemFactory.objectMembers.constructor) {
continue;
}
Value receiver = invoke.getReceiver().getAliasedValue(aliasesThroughAssumeAndCheckCasts);
if (receiver != eligibleInstance) {
continue;
}
ClassTypeElement exactReceiverType =
ClassTypeElement.create(eligibleClass.type, Nullability.definitelyNotNull(), appView);
ProgramMethod singleTarget =
invoke.lookupSingleProgramTarget(
appView, method, exactReceiverType, exactReceiverType);
if (singleTarget == null || !indirectMethodCallsOnInstance.contains(singleTarget)) {
throw new IllegalClassInlinerStateException();
}
methodCallsOnInstance.put(invoke, new InliningInfo(singleTarget, null));
}
}
currentUsers = indirectOutValueUsers;
}
if (!methodCallsOnInstance.isEmpty()) {
inliner.performForcedInlining(
method, code, methodCallsOnInstance, inliningIRProvider, Timing.empty());
} else {
assert indirectMethodCallsOnInstance.stream()
.filter(method -> method.getDefinition().getOptimizationInfo().mayHaveSideEffects())
.allMatch(
method ->
method.getDefinition().isInstanceInitializer()
&& !method
.getDefinition()
.getOptimizationInfo()
.getContextInsensitiveInstanceInitializerInfo()
.mayHaveOtherSideEffectsThanInstanceFieldAssignments());
}
return true;
}
private void rebindIndirectEligibleInstanceUsersFromPhis() {
// Building the inlinee can cause some of the eligibleInstance users to be phi's. These phi's
// should be trivial.
// block X:
// vX <- NewInstance ...
// block Y:
// vZ : phi(vX, vY)
// block Z
// vY : phi(vX, vZ)
// These are not pruned by the trivial phi removal. We have to ensure that we rewrite all users
// also the indirect users directly using phi's, potentially through assumes and checkcast.
Set<Value> aliases = SetUtils.newIdentityHashSet(eligibleInstance);
Set<Phi> expectedDeadOrTrivialPhis = Sets.newIdentityHashSet();
WorkList<InstructionOrPhi> worklist = WorkList.newIdentityWorkList();
eligibleInstance.uniqueUsers().forEach(worklist::addIfNotSeen);
eligibleInstance.uniquePhiUsers().forEach(worklist::addIfNotSeen);
while (worklist.hasNext()) {
InstructionOrPhi instructionOrPhi = worklist.next();
if (instructionOrPhi.isPhi()) {
Phi phi = instructionOrPhi.asPhi();
expectedDeadOrTrivialPhis.add(phi);
phi.uniqueUsers().forEach(worklist::addIfNotSeen);
phi.uniquePhiUsers().forEach(worklist::addIfNotSeen);
} else {
Instruction instruction = instructionOrPhi.asInstruction();
if (aliasesThroughAssumeAndCheckCasts.isIntroducingAnAlias(instruction)) {
aliases.add(instruction.outValue());
instruction.outValue().uniqueUsers().forEach(worklist::addIfNotSeen);
instruction.outValue().uniquePhiUsers().forEach(worklist::addIfNotSeen);
}
}
}
// Check that all phis are dead or trivial.
for (Phi deadTrivialPhi : expectedDeadOrTrivialPhis) {
for (Value operand : deadTrivialPhi.getOperands()) {
operand = operand.getAliasedValue(aliasesThroughAssumeAndCheckCasts);
// If the operand is a phi we should have found it in the search above.
if (operand.isPhi() && !expectedDeadOrTrivialPhis.contains(operand.asPhi())) {
throw new InternalCompilerError(
"Unexpected non-trivial phi in method eligible for class inlining");
}
// If the operand is not a phi, it should be an alias (or the eligibleInstance).
if (!operand.isPhi() && !aliases.contains(operand)) {
throw new InternalCompilerError(
"Unexpected non-trivial phi in method eligible for class inlining");
}
}
deadTrivialPhi.replaceUsers(eligibleInstance);
deadTrivialPhi.removeDeadPhi();
}
// We can now prune the aliases
for (Value alias : aliases) {
if (alias == eligibleInstance) {
continue;
}
assert alias.definition.isAssume() || alias.definition.isCheckCast();
alias.replaceUsers(eligibleInstance);
removeInstruction(alias.definition);
}
// Verify that no more assume or check-cast instructions are left as users.
assert eligibleInstance.aliasedUsers().stream().noneMatch(Instruction::isAssume);
assert eligibleInstance.aliasedUsers().stream().noneMatch(Instruction::isCheckCast);
}
// Remove miscellaneous users before handling field reads.
private void removeMiscUsages(IRCode code, Set<Value> affectedValues) {
boolean needToRemoveUnreachableBlocks = false;
for (Instruction user : eligibleInstance.uniqueUsers()) {
if (user.isInvokeMethod()) {
InvokeMethod invoke = user.asInvokeMethod();
// Remove the call to java.lang.Object.<init>().
if (user.isInvokeDirect()) {
if (root.isNewInstance()
&& invoke.getInvokedMethod() == dexItemFactory.objectMembers.constructor) {
removeInstruction(invoke);
continue;
}
}
if (user.isInvokeStatic()) {
assert invoke.getInvokedMethod() == dexItemFactory.objectsMethods.requireNonNull;
removeInstruction(invoke);
continue;
}
DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, method);
if (singleTarget != null && singleTarget.isLibraryMethod()) {
boolean isSideEffectFree =
appView
.getLibraryMethodSideEffectModelCollection()
.isSideEffectFree(invoke, singleTarget.asLibraryMethod());
if (isSideEffectFree) {
if (!invoke.hasOutValue() || !invoke.outValue().hasAnyUsers()) {
removeInstruction(invoke);
continue;
}
}
}
}
if (user.isIf()) {
If ifInsn = user.asIf();
assert ifInsn.isZeroTest()
: "Unexpected usage in non-zero-test IF instruction: " + user;
BasicBlock block = user.getBlock();
If.Type type = ifInsn.getType();
assert type == If.Type.EQ || type == If.Type.NE
: "Unexpected type in zero-test IF instruction: " + user;
BasicBlock newBlock = type == If.Type.EQ
? ifInsn.fallthroughBlock() : ifInsn.getTrueTarget();
BasicBlock blockToRemove = type == If.Type.EQ
? ifInsn.getTrueTarget() : ifInsn.fallthroughBlock();
assert newBlock != blockToRemove;
block.replaceSuccessor(blockToRemove, newBlock);
blockToRemove.removePredecessor(block, null);
assert block.exit().isGoto();
assert block.exit().asGoto().getTarget() == newBlock;
needToRemoveUnreachableBlocks = true;
continue;
}
if (user.isInstanceGet() || user.isInstancePut()) {
// Leave field reads/writes until next steps.
continue;
}
if (user.isMonitor()) {
// Since this instance never escapes and is guaranteed to be non-null, any monitor
// instructions are no-ops.
removeInstruction(user);
continue;
}
throw new Unreachable(
"Unexpected usage left in method `"
+ method.toSourceString()
+ "` after inlining: "
+ user);
}
if (needToRemoveUnreachableBlocks) {
affectedValues.addAll(code.removeUnreachableBlocks());
}
}
// Replace field reads with appropriate values, insert phis when needed.
private void removeFieldReads(IRCode code) {
Set<Value> affectedValues = Sets.newIdentityHashSet();
if (root.isNewInstance()) {
removeFieldReadsFromNewInstance(code, affectedValues);
} else {
assert root.isStaticGet();
removeFieldReadsFromStaticGet(code, affectedValues);
}
if (!affectedValues.isEmpty()) {
new TypeAnalysis(appView).narrowing(affectedValues);
}
}
private void removeFieldReadsFromNewInstance(IRCode code, Set<Value> affectedValues) {
TreeSet<InstanceGet> uniqueInstanceGetUsersWithDeterministicOrder =
new TreeSet<>(Comparator.comparingInt(x -> x.outValue().getNumber()));
for (Instruction user : eligibleInstance.uniqueUsers()) {
if (user.isInstanceGet()) {
if (user.hasUsedOutValue()) {
uniqueInstanceGetUsersWithDeterministicOrder.add(user.asInstanceGet());
} else {
removeInstruction(user);
}
continue;
}
if (user.isInstancePut()) {
// Skip in this iteration since these instructions are needed to properly calculate what
// value should field reads be replaced with.
assert root.isNewInstance();
continue;
}
throw new Unreachable(
"Unexpected usage left in method `"
+ method.toSourceString()
+ "` after inlining: "
+ user);
}
Map<DexField, FieldValueHelper> fieldHelpers = new IdentityHashMap<>();
for (InstanceGet user : uniqueInstanceGetUsersWithDeterministicOrder) {
// Replace a field read with appropriate value.
removeFieldReadFromNewInstance(code, user, affectedValues, fieldHelpers);
}
}
private void removeFieldReadFromNewInstance(
IRCode code,
InstanceGet fieldRead,
Set<Value> affectedValues,
Map<DexField, FieldValueHelper> fieldHelpers) {
Value value = fieldRead.outValue();
if (value != null) {
FieldValueHelper helper =
fieldHelpers.computeIfAbsent(
fieldRead.getField(), field -> new FieldValueHelper(field, code, root, appView));
Value newValue = helper.getValueForFieldRead(fieldRead.getBlock(), fieldRead);
value.replaceUsers(newValue);
for (FieldValueHelper fieldValueHelper : fieldHelpers.values()) {
fieldValueHelper.replaceValue(value, newValue);
}
assert !value.hasAnyUsers();
// `newValue` could be a phi introduced by FieldValueHelper. Its initial type is set as the
// type of read field, but it could be more precise than that due to (multiple) inlining.
// In addition to values affected by `newValue`, it's necessary to revisit `newValue` itself.
affectedValues.add(newValue);
affectedValues.addAll(newValue.affectedValues());
}
removeInstruction(fieldRead);
}
private void removeFieldReadsFromStaticGet(IRCode code, Set<Value> affectedValues) {
Set<BasicBlock> seen = Sets.newIdentityHashSet();
Set<Instruction> users = eligibleInstance.uniqueUsers();
for (Instruction user : users) {
BasicBlock block = user.getBlock();
if (block == null || !seen.add(block)) {
continue;
}
InstructionListIterator instructionIterator = block.listIterator(code);
while (instructionIterator.hasNext()) {
Instruction instruction = instructionIterator.next();
if (!users.contains(instruction)) {
continue;
}
if (instruction.isInstanceGet()) {
if (instruction.hasUsedOutValue()) {
replaceFieldReadFromStaticGet(
code, instructionIterator, user.asInstanceGet(), affectedValues);
} else {
instructionIterator.removeOrReplaceByDebugLocalRead();
}
continue;
}
if (instruction.isInstancePut()) {
instructionIterator.removeOrReplaceByDebugLocalRead();
continue;
}
throw new Unreachable(
"Unexpected usage left in method `"
+ method.toSourceString()
+ "` after inlining: "
+ user);
}
}
}
private void replaceFieldReadFromStaticGet(
IRCode code,
InstructionListIterator instructionIterator,
InstanceGet fieldRead,
Set<Value> affectedValues) {
DexField fieldReference = fieldRead.getField();
DexClass holder = appView.definitionFor(fieldReference.getHolderType(), method);
DexEncodedField field = fieldReference.lookupOnClass(holder);
if (field == null) {
throw reportUnknownFieldReadFromSingleton(fieldRead);
}
AbstractValue abstractValue = objectState.getAbstractFieldValue(field);
if (!abstractValue.isSingleConstValue()) {
throw reportUnknownFieldReadFromSingleton(fieldRead);
}
SingleConstValue singleConstValue = abstractValue.asSingleConstValue();
if (!singleConstValue.isMaterializableInContext(appView, method)) {
throw reportUnknownFieldReadFromSingleton(fieldRead);
}
Instruction replacement =
singleConstValue.createMaterializingInstruction(appView, code, fieldRead);
instructionIterator.replaceCurrentInstruction(replacement, affectedValues);
}
private RuntimeException reportUnknownFieldReadFromSingleton(InstanceGet fieldRead) {
throw appView
.reporter()
.fatalError(
"Unexpected usage left in method `"
+ method.toSourceString()
+ "` after inlining: "
+ fieldRead.toString());
}
private void removeFieldWrites() {
for (Instruction user : eligibleInstance.uniqueUsers()) {
if (!user.isInstancePut()) {
throw new Unreachable(
"Unexpected usage left in method `"
+ method.toSourceString()
+ "` after field reads removed: "
+ user);
}
assert root.isNewInstance();
InstancePut instancePut = user.asInstancePut();
DexEncodedField field =
appView
.appInfo()
.resolveFieldOn(eligibleClass, instancePut.getField())
.getResolvedField();
if (field == null) {
throw new Unreachable(
"Unexpected field write left in method `"
+ method.toSourceString()
+ "` after field reads removed: "
+ user);
}
removeInstruction(user);
}
}
private InliningInfo isEligibleConstructorCall(InvokeDirect invoke, ProgramMethod singleTarget) {
assert dexItemFactory.isConstructor(invoke.getInvokedMethod());
assert isEligibleSingleTarget(singleTarget);
// Must be a constructor called on the receiver.
if (!receivers.isDefiniteReceiverAlias(invoke.getReceiver())) {
return null;
}
// None of the subsequent arguments may be an alias of the receiver.
List<Value> inValues = invoke.inValues();
for (int i = 1; i < inValues.size(); i++) {
if (!receivers.addIllegalReceiverAlias(inValues.get(i))) {
return null;
}
}
// Must be a constructor of the exact same class.
DexMethod init = invoke.getInvokedMethod();
if (init.holder != eligibleClass.type) {
// Calling a constructor on a class that is different from the type of the instance.
// Gracefully abort class inlining (see the test B116282409).
return null;
}
// Check that the `eligibleInstance` does not escape via the constructor.
InstanceInitializerInfo instanceInitializerInfo =
singleTarget.getDefinition().getOptimizationInfo().getInstanceInitializerInfo(invoke);
if (instanceInitializerInfo.receiverMayEscapeOutsideConstructorChain()) {
return null;
}
// Check that the entire constructor chain can be inlined into the current context.
DexMethod parent = instanceInitializerInfo.getParent();
while (parent != dexItemFactory.objectMembers.constructor) {
if (parent == null) {
return null;
}
DexProgramClass parentClass =
asProgramClassOrNull(appView.definitionForHolder(parent, method));
if (parentClass == null) {
return null;
}
ProgramMethod encodedParent = parentClass.lookupProgramMethod(parent);
if (encodedParent == null) {
return null;
}
if (methodProcessor.isProcessedConcurrently(encodedParent)) {
return null;
}
DexEncodedMethod encodedParentMethod = encodedParent.getDefinition();
if (!encodedParentMethod.isInliningCandidate(
method,
Reason.ALWAYS,
appView.appInfo(),
NopWhyAreYouNotInliningReporter.getInstance())) {
return null;
}
parent =
encodedParentMethod
.getOptimizationInfo()
.getContextInsensitiveInstanceInitializerInfo()
.getParent();
}
return new InliningInfo(singleTarget, eligibleClass.type);
}
// An invoke is eligible for inlining in the following cases:
//
// - if it does not return the receiver
// - if there are no uses of the out value
// - if it is a regular chaining pattern where the only users of the out value are receivers to
// other invocations. In that case, we should add all indirect users of the out value to ensure
// they can also be inlined.
private boolean scheduleNewUsersForAnalysis(
InvokeMethod invoke,
ProgramMethod singleTarget,
int parameter,
Set<Instruction> indirectUsers) {
ClassInlinerMethodConstraint classInlinerMethodConstraint =
singleTarget.getDefinition().getOptimizationInfo().getClassInlinerMethodConstraint();
ParameterUsage usage = classInlinerMethodConstraint.getParameterUsage(parameter);
OptionalBool returnsParameter;
if (usage.isParameterReturned()) {
if (singleTarget.getDefinition().getOptimizationInfo().returnsArgument()) {
assert singleTarget.getDefinition().getOptimizationInfo().getReturnedArgument()
== parameter;
returnsParameter = OptionalBool.TRUE;
} else {
returnsParameter = OptionalBool.UNKNOWN;
}
} else {
returnsParameter = OptionalBool.FALSE;
}
if (returnsParameter.isFalse()) {
return true;
}
Value outValue = invoke.outValue();
if (outValue == null || !outValue.hasAnyUsers()) {
return true;
}
// For CF we no longer perform the code-rewrite in CodeRewriter.rewriteMoveResult that removes
// out values if they alias to the receiver since that naively produces a lot of popping values
// from the stack.
if (outValue.hasPhiUsers() || outValue.hasDebugUsers()) {
return false;
}
// We cannot guarantee the invoke returns the receiver or another instance and since the
// return value is used we have to bail out.
if (returnsParameter.isUnknown()) {
return false;
}
// Add the out-value as a definite-alias if the invoke instruction is guaranteed to return the
// receiver. Otherwise, the out-value may be an alias of the receiver, and it is added to the
// may-alias set.
assert returnsParameter.isTrue();
if (!receivers.addReceiverAlias(outValue)) {
return false;
}
indirectUsers.addAll(outValue.uniqueUsers());
return true;
}
private boolean isEligibleIndirectVirtualMethodCall(
DexMethod invokedMethod, ProgramMethod singleTarget) {
if (!isEligibleSingleTarget(singleTarget)) {
return false;
}
if (singleTarget.getDefinition().isLibraryMethodOverride().isTrue()) {
return false;
}
if (!isEligibleVirtualMethodCall(invokedMethod, singleTarget)) {
return false;
}
ParameterUsage usage =
singleTarget
.getDefinition()
.getOptimizationInfo()
.getClassInlinerMethodConstraint()
.getParameterUsage(0);
assert !usage.isTop();
if (usage.isBottom()) {
return true;
}
NonEmptyParameterUsage nonEmptyUsage = usage.asNonEmpty();
return nonEmptyUsage.getMethodCallsWithParameterAsReceiver().isEmpty()
&& !nonEmptyUsage.isParameterReturned();
}
private boolean isEligibleVirtualMethodCall(DexMethod callee, ProgramMethod singleTarget) {
assert isEligibleSingleTarget(singleTarget);
// We should not inline a method if the invocation has type interface or virtual and the
// signature of the invocation resolves to a private or static method.
// TODO(b/147212189): Why not inline private methods? If access is permitted it is valid.
ResolutionResult resolutionResult =
appView.appInfo().resolveMethodOnClass(callee, eligibleClass);
if (resolutionResult.isSingleResolution()
&& !resolutionResult.getSingleTarget().isNonPrivateVirtualMethod()) {
return false;
}
if (!singleTarget.getDefinition().isNonPrivateVirtualMethod()) {
return false;
}
if (method.getDefinition() == singleTarget.getDefinition()) {
return false; // Don't inline itself.
}
MethodOptimizationInfo optimizationInfo = singleTarget.getDefinition().getOptimizationInfo();
ClassInlinerMethodConstraint classInlinerMethodConstraint =
optimizationInfo.getClassInlinerMethodConstraint();
int parameter = 0;
if (root.isNewInstance()) {
return classInlinerMethodConstraint.isEligibleForNewInstanceClassInlining(
singleTarget, parameter);
}
assert root.isStaticGet();
return classInlinerMethodConstraint.isEligibleForStaticGetClassInlining(
appView, parameter, objectState, method);
}
// Analyzes if a method invoke the eligible instance is passed to is eligible. In short,
// it can be eligible if:
//
// -- eligible instance is passed as argument #N which is only used in the method to
// call a method on this object (we call it indirect method call), and method is
// eligible according to the same rules defined for direct method call eligibility
// (except we require the method receiver to not be used in return instruction)
//
// -- eligible instance is used in zero-test 'if' instructions testing if the value
// is null/not-null (since we know the instance is not null, those checks can
// be rewritten)
//
// -- method itself can be inlined
//
private boolean isEligibleDirectMethodCall(
InvokeMethod invoke,
SingleResolutionResult resolutionResult,
ProgramMethod singleTarget,
Supplier<InliningOracle> defaultOracle,
Set<Instruction> indirectUsers) {
if (!((invoke.isInvokeDirect() && !invoke.isInvokeConstructor(dexItemFactory))
|| invoke.isInvokeInterface()
|| invoke.isInvokeStatic()
|| invoke.isInvokeVirtual())) {
return false;
}
// If we got here with invocation on receiver the user is ineligible.
if (invoke.isInvokeMethodWithReceiver()) {
// A definitely null receiver will throw an error on call site.
Value receiver = invoke.asInvokeMethodWithReceiver().getReceiver();
if (receiver.getType().isDefinitelyNull()) {
return false;
}
}
// Check if the method is inline-able by standard inliner.
InliningOracle oracle = defaultOracle.get();
if (!oracle.passesInliningConstraints(
invoke,
resolutionResult,
singleTarget,
Reason.ALWAYS,
NopWhyAreYouNotInliningReporter.getInstance())) {
return false;
}
// Go through all arguments, see if all usages of eligibleInstance are good.
if (!isEligibleParameterUsages(invoke, singleTarget, indirectUsers)) {
return false;
}
directMethodCalls.put(invoke, new InliningInfo(singleTarget, null));
// Looks good.
markSizeOfDirectTargetForInlining(invoke, singleTarget);
return true;
}
private boolean isEligibleLibraryMethodCall(InvokeMethod invoke, LibraryMethod singleTarget) {
boolean isSideEffectFree =
appView.getLibraryMethodSideEffectModelCollection().isSideEffectFree(invoke, singleTarget);
if (isSideEffectFree) {
return !invoke.hasOutValue() || !invoke.outValue().hasAnyUsers();
}
if (singleTarget.getReference() == dexItemFactory.objectsMethods.requireNonNull) {
return !invoke.hasOutValue() || !invoke.outValue().hasAnyUsers();
}
return false;
}
private boolean isEligibleParameterUsages(
InvokeMethod invoke, ProgramMethod singleTarget, Set<Instruction> indirectUsers) {
// Go through all arguments, see if all usages of eligibleInstance are good.
for (int parameter = 0; parameter < invoke.arguments().size(); parameter++) {
Value argument = invoke.getArgument(parameter);
if (receivers.isReceiverAlias(argument)) {
// Have parameter usage info?
if (!isEligibleParameterUsage(invoke, singleTarget, parameter, indirectUsers)) {
return false;
}
} else {
// Nothing to worry about, unless `argument` becomes an alias of the receiver later.
int finalParameter = parameter;
receivers.addDeferredAliasValidityCheck(
argument,
() -> isEligibleParameterUsage(invoke, singleTarget, finalParameter, indirectUsers));
}
}
return true;
}
private boolean isEligibleParameterUsage(
InvokeMethod invoke,
ProgramMethod singleTarget,
int parameter,
Set<Instruction> indirectUsers) {
ClassInlinerMethodConstraint classInlinerMethodConstraint =
singleTarget.getDefinition().getOptimizationInfo().getClassInlinerMethodConstraint();
if (root.isNewInstance()) {
if (!classInlinerMethodConstraint.isEligibleForNewInstanceClassInlining(
singleTarget, parameter)) {
return false;
}
} else {
assert root.isStaticGet();
if (!classInlinerMethodConstraint.isEligibleForStaticGetClassInlining(
appView, parameter, objectState, method)) {
return false;
}
}
ParameterUsage usage = classInlinerMethodConstraint.getParameterUsage(parameter);
if (!scheduleNewUsersForAnalysis(invoke, singleTarget, parameter, indirectUsers)) {
return false;
}
if (!usage.isBottom()) {
NonEmptyParameterUsage nonEmptyUsage = usage.asNonEmpty();
for (DexMethod invokedMethod : nonEmptyUsage.getMethodCallsWithParameterAsReceiver()) {
SingleResolutionResult resolutionResult =
appView.appInfo().resolveMethodOn(eligibleClass, invokedMethod).asSingleResolution();
if (resolutionResult == null || !resolutionResult.getResolvedHolder().isProgramClass()) {
return false;
}
// Is the method called indirectly still eligible?
ProgramMethod indirectSingleTarget = resolutionResult.getResolutionPair().asProgramMethod();
if (!isEligibleIndirectVirtualMethodCall(invokedMethod, indirectSingleTarget)) {
return false;
}
markSizeOfIndirectTargetForInlining(indirectSingleTarget);
}
}
return true;
}
private boolean exemptFromInstructionLimit(ProgramMethod inlinee) {
KotlinClassLevelInfo kotlinInfo = inlinee.getHolder().getKotlinInfo();
return kotlinInfo.isSyntheticClass() && kotlinInfo.asSyntheticClass().isLambda();
}
private void markSizeOfIndirectTargetForInlining(ProgramMethod inlinee) {
assert !methodProcessor.isProcessedConcurrently(inlinee);
if (!exemptFromInstructionLimit(inlinee)) {
indirectInlinees.add(inlinee);
}
indirectMethodCallsOnInstance.add(inlinee);
}
private void markSizeOfDirectTargetForInlining(InvokeMethod invoke, ProgramMethod inlinee) {
assert invoke != null;
assert !methodProcessor.isProcessedConcurrently(inlinee);
if (!exemptFromInstructionLimit(inlinee)) {
directInlinees.put(invoke, inlinee);
}
}
private boolean isEligibleSingleTarget(ProgramMethod singleTarget) {
if (singleTarget == null) {
return false;
}
if (methodProcessor.isProcessedConcurrently(singleTarget)) {
return false;
}
if (!singleTarget
.getDefinition()
.isInliningCandidate(
method,
Reason.ALWAYS,
appView.appInfo(),
NopWhyAreYouNotInliningReporter.getInstance())) {
// If `singleTarget` is not an inlining candidate, we won't be able to inline it here.
//
// Note that there may be some false negatives here since the method may
// reference private fields of its class which are supposed to be replaced
// with arguments after inlining. We should try and improve it later.
//
// Using -allowaccessmodification mitigates this.
return false;
}
return true;
}
private void removeInstruction(Instruction instruction) {
instruction.inValues().forEach(v -> v.removeUser(instruction));
instruction.getBlock().removeInstruction(instruction);
}
static class IllegalClassInlinerStateException extends Exception {
IllegalClassInlinerStateException() {
assert false;
}
}
}