blob: 65c05e651b98a47ffb2525a3cd6f97d0c2bc0622 [file] [log] [blame]
// Copyright (c) 2021, 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.analysis;
import static com.android.tools.r8.ir.code.Opcodes.ASSUME;
import static com.android.tools.r8.ir.code.Opcodes.CHECK_CAST;
import static com.android.tools.r8.ir.code.Opcodes.IF;
import static com.android.tools.r8.ir.code.Opcodes.INSTANCE_GET;
import static com.android.tools.r8.ir.code.Opcodes.INSTANCE_PUT;
import static com.android.tools.r8.ir.code.Opcodes.INVOKE_DIRECT;
import static com.android.tools.r8.ir.code.Opcodes.INVOKE_INTERFACE;
import static com.android.tools.r8.ir.code.Opcodes.INVOKE_STATIC;
import static com.android.tools.r8.ir.code.Opcodes.INVOKE_VIRTUAL;
import static com.android.tools.r8.ir.code.Opcodes.MONITOR;
import static com.android.tools.r8.ir.code.Opcodes.RETURN;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.ClasspathOrLibraryClass;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.FieldResolutionResult;
import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.ir.analysis.framework.intraprocedural.AbstractTransferFunction;
import com.android.tools.r8.ir.analysis.framework.intraprocedural.FailedTransferFunctionResult;
import com.android.tools.r8.ir.analysis.framework.intraprocedural.TransferFunctionResult;
import com.android.tools.r8.ir.analysis.type.TypeElement;
import com.android.tools.r8.ir.code.AliasedValueConfiguration;
import com.android.tools.r8.ir.code.Argument;
import com.android.tools.r8.ir.code.Assume;
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.InvokeDirect;
import com.android.tools.r8.ir.code.InvokeInterface;
import com.android.tools.r8.ir.code.InvokeStatic;
import com.android.tools.r8.ir.code.InvokeVirtual;
import com.android.tools.r8.ir.code.Monitor;
import com.android.tools.r8.ir.code.Return;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.google.common.collect.Sets;
import java.util.Set;
class TransferFunction implements AbstractTransferFunction<ParameterUsages> {
private static final AliasedValueConfiguration aliasedValueConfiguration =
AssumeAndCheckCastAliasedValueConfiguration.getInstance();
private final AppView<AppInfoWithLiveness> appView;
private final DexItemFactory dexItemFactory;
private final ProgramMethod method;
// The last argument instruction.
private final Argument lastArgument;
// Caches the parent or forwarding constructor call (only used in constructors).
private InvokeDirect constructorInvoke;
// The arguments that are considered by the analysis. We don't consider primitive arguments since
// they cannot be class inlined.
private Set<Value> argumentsOfInterest = Sets.newIdentityHashSet();
// Instructions that use one of the arguments. Instructions that don't use any of the arguments
// do not have any impact on the ability to class inline the arguments, therefore they are
// skipped.
private Set<Instruction> instructionsOfInterest = Sets.newIdentityHashSet();
TransferFunction(AppView<AppInfoWithLiveness> appView, ProgramMethod method, IRCode code) {
this.appView = appView;
this.dexItemFactory = appView.dexItemFactory();
this.method = method;
this.lastArgument = code.getLastArgument();
}
@Override
public TransferFunctionResult<ParameterUsages> apply(
Instruction instruction, ParameterUsages state) {
if (instruction.isArgument()) {
Argument argument = instruction.asArgument();
ParameterUsages result = analyzeArgument(argument, state);
// After analyzing the last argument instruction, only proceed if there is at least one
// argument that may be eligible for class inlining.
if (argument == lastArgument
&& result.asNonEmpty().allMatch((context, usagePerContext) -> usagePerContext.isTop())) {
return fail();
}
return result;
}
if (!instructionsOfInterest.contains(instruction)) {
// The instruction does not use any of the argument values that we are analyzing, so for the
// purpose of class inlining we can ignore this instruction.
return state;
}
assert !state.isBottom();
assert !state.isTop();
ParameterUsages outState = apply(instruction, state.asNonEmpty());
return outState != state ? widen(outState) : outState;
}
private ParameterUsages apply(Instruction instruction, NonEmptyParameterUsages state) {
switch (instruction.opcode()) {
case ASSUME:
return analyzeAssume(instruction.asAssume(), state);
case CHECK_CAST:
return analyzeCheckCast(instruction.asCheckCast(), state);
case IF:
return analyzeIf(instruction.asIf(), state);
case INSTANCE_GET:
return analyzeInstanceGet(instruction.asInstanceGet(), state);
case INSTANCE_PUT:
return analyzeInstancePut(instruction.asInstancePut(), state);
case INVOKE_DIRECT:
return analyzeInvokeDirect(instruction.asInvokeDirect(), state);
case INVOKE_INTERFACE:
return analyzeInvokeInterface(instruction.asInvokeInterface(), state);
case INVOKE_STATIC:
return analyzeInvokeStatic(instruction.asInvokeStatic(), state);
case INVOKE_VIRTUAL:
return analyzeInvokeVirtual(instruction.asInvokeVirtual(), state);
case MONITOR:
return analyzeMonitor(instruction.asMonitor(), state);
case RETURN:
return analyzeReturn(instruction.asReturn(), state);
default:
return fail(instruction, state);
}
}
@Override
public ParameterUsages computeBlockEntryState(
BasicBlock block, BasicBlock predecessor, ParameterUsages predecessorExitState) {
// TODO(b/173337498): Fork a new `FIELD=x` analysis context for the successor block if the
// predecessor ends with an if or switch instruction, and the successor block is the
// `FIELD=x` target of the predecessor. To avoid an excessive number of contexts being
// created, only allow forking new contexts for $r8$classId fields synthesized by the
// horizontal class merger.
return predecessorExitState;
}
private ParameterUsages analyzeArgument(Argument argument, ParameterUsages state) {
// Only consider arguments that could store an instance eligible for class inlining. Note that
// we can't ignore parameters with a library type, since instances of program classes could
// still flow into such parameters.
Value value = argument.outValue();
if (!isMaybeEligibleForClassInlining(value.getType()) || value.hasPhiUsers()) {
return state.put(argument.getIndex(), ParameterUsagePerContext.top());
}
// Mark the users of this argument for analysis, and fork the analysis of this argument in the
// default analysis context.
argumentsOfInterest.add(value);
instructionsOfInterest.addAll(value.aliasedUsers(aliasedValueConfiguration));
return state.put(argument.getIndex(), NonEmptyParameterUsagePerContext.createInitial());
}
private ParameterUsages analyzeAssume(Assume assume, NonEmptyParameterUsages state) {
// Mark the value as ineligible for class inlining if it has phi users.
return assume.outValue().hasPhiUsers() ? fail(assume, state) : state;
}
private ParameterUsages analyzeCheckCast(CheckCast checkCast, NonEmptyParameterUsages state) {
// Mark the value as ineligible for class inlining if it has phi users.
if (checkCast.outValue().hasPhiUsers()) {
return fail(checkCast, state);
}
return state.rebuildParameter(
checkCast.object(), (context, usage) -> usage.addCastWithParameter(checkCast.getType()));
}
private ParameterUsages analyzeIf(If theIf, NonEmptyParameterUsages state) {
// Null/not-null tests are ok.
if (theIf.isZeroTest()) {
assert argumentsOfInterest.contains(theIf.lhs().getAliasedValue(aliasedValueConfiguration));
return state;
}
// For non-null tests, mark the inputs as ineligible for class inlining.
return fail(theIf, state);
}
private ParameterUsages analyzeInstanceGet(
InstanceGet instanceGet, NonEmptyParameterUsages state) {
// Instance field reads are OK, as long as the field resolves, since the class inliner will
// just replace the field read by the value of the field.
FieldResolutionResult resolutionResult = appView.appInfo().resolveField(instanceGet.getField());
if (resolutionResult.isSingleFieldResolutionResult()) {
// Record that the field is read from the parameter. For class inlining of singletons, this
// parameter is only eligible for class inlining if the singleton's field value is known.
return state.rebuildParameter(
instanceGet.object(),
(context, usage) -> usage.addFieldReadFromParameter(instanceGet.getField()));
}
return fail(instanceGet, state);
}
private ParameterUsages analyzeInstancePut(
InstancePut instancePut, NonEmptyParameterUsages state) {
// Instance field writes are OK, as long as the field resolves and the receiver is not being
// assigned (in that case the receiver escapes, and thus it is not eligible for class
// inlining).
Value valueRoot = instancePut.value().getAliasedValue(aliasedValueConfiguration);
if (isArgumentOfInterest(valueRoot)) {
state = state.abandonClassInliningInCurrentContexts(valueRoot);
}
Value objectRoot = instancePut.object().getAliasedValue(aliasedValueConfiguration);
if (!isArgumentOfInterest(objectRoot)) {
return state;
}
FieldResolutionResult resolutionResult = appView.appInfo().resolveField(instancePut.getField());
if (resolutionResult.isSingleFieldResolutionResult()) {
return state.rebuildParameter(objectRoot, (context, usage) -> usage.setParameterMutated());
} else {
return state.abandonClassInliningInCurrentContexts(objectRoot);
}
}
private ParameterUsages analyzeInvokeDirect(InvokeDirect invoke, NonEmptyParameterUsages state) {
// We generally don't class inline instances that escape through invoke-direct calls, but we
// make an exception for forwarding/parent constructor calls that does not leak the receiver.
state =
state.abandonClassInliningInCurrentContexts(
invoke.getNonReceiverArguments(), this::isArgumentOfInterest);
Value receiverRoot = invoke.getReceiver().getAliasedValue(aliasedValueConfiguration);
if (!isArgumentOfInterest(receiverRoot)) {
return state;
}
if (!receiverRoot.isThis()
|| !method.getDefinition().isInstanceInitializer()
|| !invoke.isInvokeConstructor(dexItemFactory)) {
return state.abandonClassInliningInCurrentContexts(receiverRoot);
}
SingleResolutionResult resolutionResult =
appView
.appInfo()
.resolveMethodOnClassHolder(invoke.getInvokedMethod())
.asSingleResolution();
if (resolutionResult == null) {
return state.abandonClassInliningInCurrentContexts(receiverRoot);
}
InstanceInitializerInfo instanceInitializerInfo =
resolutionResult
.getResolvedMethod()
.getOptimizationInfo()
.getInstanceInitializerInfo(invoke);
if (instanceInitializerInfo.receiverMayEscapeOutsideConstructorChain()) {
return state.abandonClassInliningInCurrentContexts(receiverRoot);
}
// We require that there is exactly one forwarding/parent constructor call.
if (constructorInvoke != null && constructorInvoke != invoke) {
return state.abandonClassInliningInCurrentContexts(receiverRoot);
}
constructorInvoke = invoke;
return state;
}
private ParameterUsages analyzeInvokeInterface(
InvokeInterface invoke, NonEmptyParameterUsages state) {
// We only allow invoke-interface instructions where the parameter is in the receiver position.
state =
state.abandonClassInliningInCurrentContexts(
invoke.getNonReceiverArguments(), this::isArgumentOfInterest);
Value receiverRoot = invoke.getReceiver().getAliasedValue(aliasedValueConfiguration);
if (!isArgumentOfInterest(receiverRoot)) {
return state;
}
SingleResolutionResult resolutionResult =
appView
.appInfo()
.resolveMethodOnInterfaceHolder(invoke.getInvokedMethod())
.asSingleResolution();
if (resolutionResult == null) {
return state.abandonClassInliningInCurrentContexts(receiverRoot);
}
return state.rebuildParameter(
receiverRoot, (context, usage) -> usage.addMethodCallWithParameterAsReceiver(invoke));
}
private ParameterUsages analyzeInvokeStatic(InvokeStatic invoke, NonEmptyParameterUsages state) {
// We generally don't class inline instances that escape through invoke-static calls, but we
// make an exception for calls to Objects.requireNonNull().
SingleResolutionResult resolutionResult =
appView
.appInfo()
.unsafeResolveMethodDueToDexFormat(invoke.getInvokedMethod())
.asSingleResolution();
if (resolutionResult != null
&& resolutionResult.getResolvedMethod().getReference()
== dexItemFactory.objectsMethods.requireNonNull) {
return state;
}
return fail(invoke, state);
}
private ParameterUsages analyzeInvokeVirtual(
InvokeVirtual invoke, NonEmptyParameterUsages state) {
// We only allow invoke-virtual instructions where the parameter is in the receiver position.
state =
state.abandonClassInliningInCurrentContexts(
invoke.getNonReceiverArguments(), this::isArgumentOfInterest);
Value receiverRoot = invoke.getReceiver().getAliasedValue(aliasedValueConfiguration);
if (!isArgumentOfInterest(receiverRoot)) {
return state;
}
SingleResolutionResult resolutionResult =
appView
.appInfo()
.resolveMethodOnClassHolder(invoke.getInvokedMethod())
.asSingleResolution();
if (resolutionResult == null) {
return state.abandonClassInliningInCurrentContexts(receiverRoot);
}
return state.rebuildParameter(
receiverRoot, (context, usage) -> usage.addMethodCallWithParameterAsReceiver(invoke));
}
private ParameterUsages analyzeMonitor(Monitor monitor, NonEmptyParameterUsages state) {
// Record that the receiver is used as a lock in each context that may reach this monitor
// instruction.
return state.rebuildParameter(
monitor.object(), (context, usage) -> usage.setParameterUsedAsLock());
}
private ParameterUsages analyzeReturn(Return theReturn, NonEmptyParameterUsages state) {
return state.rebuildParameter(
theReturn.returnValue(), (context, usage) -> usage.setParameterReturned());
}
private FailedTransferFunctionResult<ParameterUsages> fail() {
return new FailedTransferFunctionResult<>();
}
private ParameterUsages fail(Instruction instruction, NonEmptyParameterUsages state) {
return state.abandonClassInliningInCurrentContexts(
instruction.inValues(), this::isArgumentOfInterest);
}
private boolean isArgumentOfInterest(Value value) {
assert value.getAliasedValue(aliasedValueConfiguration) == value;
return value.isArgument() && argumentsOfInterest.contains(value);
}
private boolean isMaybeEligibleForClassInlining(TypeElement type) {
if (!type.isClassType()) {
// Primitives and arrays will never be class inlined.
return false;
}
DexClass clazz = appView.definitionFor(type.asClassType().getClassType());
if (clazz == null) {
// We cannot class inline in presence of missing classes.
return false;
}
return clazz.isProgramClass()
? isMaybeEligibleForClassInlining(clazz.asProgramClass())
: isMaybeEligibleForClassInlining(clazz.asClasspathOrLibraryClass());
}
private boolean isMaybeEligibleForClassInlining(DexProgramClass clazz) {
// We can only class inline parameters that does not inherit from other classpath or library
// classes than java.lang.Object.
DexType superType = clazz.getSuperType();
do {
DexClass superClass = appView.definitionFor(superType);
if (superClass == null) {
return false;
}
if (!superClass.isProgramClass()) {
return superClass.getType() == dexItemFactory.objectType;
}
superType = superClass.getSuperType();
} while (true);
}
private boolean isMaybeEligibleForClassInlining(ClasspathOrLibraryClass clazz) {
// We can only class inline a parameter that is either java.lang.Object or an interface type.
return clazz.getType() == dexItemFactory.objectType || clazz.isInterface();
}
private TransferFunctionResult<ParameterUsages> widen(ParameterUsages state) {
// Currently we only fork one context.
int maxNumberOfContexts = 1;
ParameterUsages widened =
state.rebuildParameters(
(parameter, usagePerContext) -> {
if (usagePerContext.isBottom() || usagePerContext.isTop()) {
return usagePerContext;
}
NonEmptyParameterUsagePerContext nonEmptyUsagePerContext = usagePerContext.asKnown();
if (nonEmptyUsagePerContext.getNumberOfContexts() == maxNumberOfContexts
&& nonEmptyUsagePerContext.allMatch(
(context, usageInContext) -> usageInContext.isTop())) {
return ParameterUsagePerContext.top();
}
return usagePerContext;
});
if (!widened.isBottom()
&& !widened.isTop()
&& widened.asNonEmpty().allMatch((parameter, usagePerContext) -> usagePerContext.isTop())) {
return fail();
}
return widened;
}
}