blob: 814812c57b5326ba5e4535e5ab38b512c6fb46cc [file] [log] [blame]
// Copyright (c) 2017, 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.AppInfoWithSubtyping;
import com.android.tools.r8.graph.DexAccessFlags;
import com.android.tools.r8.graph.DexClass;
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.DexMethod;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.GraphLense;
import com.android.tools.r8.ir.code.BasicBlock;
import com.android.tools.r8.ir.code.DominatorTree;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InstructionIterator;
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.Value;
import com.android.tools.r8.ir.code.ValueNumberGenerator;
import com.android.tools.r8.ir.conversion.CallGraph;
import com.android.tools.r8.ir.conversion.IRConverter;
import com.android.tools.r8.ir.conversion.LensCodeRewriter;
import com.android.tools.r8.ir.conversion.OptimizationFeedback;
import com.android.tools.r8.logging.Log;
import com.android.tools.r8.utils.InternalOptions;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
public class Inliner {
protected final AppInfoWithSubtyping appInfo;
private final GraphLense graphLense;
private final InternalOptions options;
// State for inlining methods which are known to be called twice.
private boolean applyDoubleInlining = false;
private final Set<DexEncodedMethod> doubleInlineCallers = Sets.newIdentityHashSet();
private final Set<DexEncodedMethod> doubleInlineSelectedTargets = Sets.newIdentityHashSet();
private final Map<DexEncodedMethod, DexEncodedMethod> doubleInlineeCandidates = new HashMap<>();
public Inliner(AppInfoWithSubtyping appInfo, GraphLense graphLense, InternalOptions options) {
this.appInfo = appInfo;
this.graphLense = graphLense;
this.options = options;
}
private Constraint instructionAllowedForInlining(
DexEncodedMethod method, Instruction instruction) {
Constraint result = instruction.inliningConstraint(appInfo, method.method.holder);
if ((result == Constraint.NEVER) && instruction.isDebugInstruction()) {
return Constraint.ALWAYS;
}
return result;
}
public Constraint computeInliningConstraint(IRCode code, DexEncodedMethod method) {
Constraint result = Constraint.ALWAYS;
InstructionIterator it = code.instructionIterator();
while (it.hasNext()) {
Instruction instruction = it.next();
Constraint state = instructionAllowedForInlining(method, instruction);
if (state == Constraint.NEVER) {
return Constraint.NEVER;
}
if (state.ordinal() < result.ordinal()) {
result = state;
}
}
return result;
}
boolean hasInliningAccess(DexEncodedMethod method, DexEncodedMethod target) {
if (!isVisibleWithFlags(target.method.holder, method.method.holder, target.accessFlags)) {
return false;
}
// The class needs also to be visible for us to have access.
DexClass targetClass = appInfo.definitionFor(target.method.holder);
return isVisibleWithFlags(target.method.holder, method.method.holder, targetClass.accessFlags);
}
private boolean isVisibleWithFlags(DexType target, DexType context, DexAccessFlags flags) {
if (flags.isPublic()) {
return true;
}
if (flags.isPrivate()) {
return target == context;
}
if (flags.isProtected()) {
return context.isSubtypeOf(target, appInfo) || target.isSamePackage(context);
}
// package-private
return target.isSamePackage(context);
}
synchronized DexEncodedMethod doubleInlining(DexEncodedMethod method,
DexEncodedMethod target) {
if (!applyDoubleInlining) {
if (doubleInlineeCandidates.containsKey(target)) {
// Both calls can be inlined.
doubleInlineCallers.add(doubleInlineeCandidates.get(target));
doubleInlineCallers.add(method);
doubleInlineSelectedTargets.add(target);
} else {
// First call can be inlined.
doubleInlineeCandidates.put(target, method);
}
// Just preparing for double inlining.
return null;
} else {
// Don't perform the actual inlining if this was not selected.
if (!doubleInlineSelectedTargets.contains(target)) {
return null;
}
}
return target;
}
public synchronized void processDoubleInlineCallers(IRConverter converter,
OptimizationFeedback feedback) {
if (doubleInlineCallers.size() > 0) {
applyDoubleInlining = true;
List<DexEncodedMethod> methods = doubleInlineCallers
.stream()
.sorted(DexEncodedMethod::slowCompare)
.collect(Collectors.toList());
for (DexEncodedMethod method : methods) {
converter.processMethod(method, feedback, Outliner::noProcessing);
assert method.isProcessed();
}
}
}
/**
* Encodes the constraints for inlining a method's instructions into a different context.
* <p>
* This only takes the instructions into account and not whether a method should be inlined
* or what reason for inlining it might have. Also, it does not take the visibility of the
* method itself into account.
*/
public enum Constraint {
// The ordinal values are important so please do not reorder.
NEVER, // Never inline this.
SAMECLASS, // Only inline this into methods with same holder.
PACKAGE, // Only inline this into methods with holders from same package.
SUBCLASS, // Only inline this into methods with holders from a subclass.
ALWAYS; // No restrictions for inlining this.
static {
assert NEVER.ordinal() < SAMECLASS.ordinal();
assert SAMECLASS.ordinal() < PACKAGE.ordinal();
assert PACKAGE.ordinal() < SUBCLASS.ordinal();
assert SUBCLASS.ordinal() < ALWAYS.ordinal();
}
public static Constraint deriveConstraint(DexType contextHolder, DexType targetHolder,
DexAccessFlags flags, AppInfoWithSubtyping appInfo) {
if (flags.isPublic()) {
return ALWAYS;
} else if (flags.isPrivate()) {
return targetHolder == contextHolder ? SAMECLASS : NEVER;
} else if (flags.isProtected()) {
if (targetHolder.isSamePackage(contextHolder)) {
// Even though protected, this is visible via the same package from the context.
return PACKAGE;
} else if (contextHolder.isSubtypeOf(targetHolder, appInfo)) {
return SUBCLASS;
}
return NEVER;
} else {
/* package-private */
return targetHolder.isSamePackage(contextHolder) ? PACKAGE : NEVER;
}
}
public static Constraint classIsVisible(DexType context, DexType clazz,
AppInfoWithSubtyping appInfo) {
DexClass definition = appInfo.definitionFor(clazz);
return definition == null ? NEVER
: deriveConstraint(context, clazz, definition.accessFlags, appInfo);
}
public static Constraint min(Constraint one, Constraint other) {
return one.ordinal() < other.ordinal() ? one : other;
}
}
/**
* Encodes the reason why a method should be inlined.
* <p>
* This is independent of determining whether a method can be inlined, except for the FORCE
* state, that will inline a method irrespective of visibility and instruction checks.
*/
public enum Reason {
FORCE, // Inlinee is marked for forced inlining (bridge method or renamed constructor).
ALWAYS, // Inlinee is marked for inlining due to alwaysinline directive.
SINGLE_CALLER, // Inlinee has precisely one caller.
DUAL_CALLER, // Inlinee has precisely two callers.
SIMPLE, // Inlinee has simple code suitable for inlining.
}
static public class InlineAction {
public final DexEncodedMethod target;
public final Invoke invoke;
final Reason reason;
InlineAction(DexEncodedMethod target, Invoke invoke, Reason reason) {
this.target = target;
this.invoke = invoke;
this.reason = reason;
}
boolean forceInline() {
return reason != Reason.SIMPLE;
}
public IRCode buildIR(ValueNumberGenerator generator, AppInfoWithSubtyping appInfo,
GraphLense graphLense, InternalOptions options) {
if (target.isProcessed()) {
assert target.getCode().isDexCode();
return target.buildIR(generator, options);
} else {
// Build the IR for a yet not processed method, and perform minimal IR processing.
IRCode code;
if (target.getCode().isJarCode()) {
code = target.getCode().asJarCode().buildIR(target, generator, options);
} else {
code = target.getCode().asDexCode().buildIR(target, generator, options);
}
new LensCodeRewriter(graphLense, appInfo).rewrite(code, target);
return code;
}
}
}
private int numberOfInstructions(IRCode code) {
int numOfInstructions = 0;
for (BasicBlock block : code.blocks) {
numOfInstructions += block.getInstructions().size();
}
return numOfInstructions;
}
private boolean legalConstructorInline(DexEncodedMethod method,
InvokeMethod invoke, IRCode code) {
// In the Java VM Specification section "4.10.2.4. Instance Initialization Methods and
// Newly Created Objects" it says:
//
// Before that method invokes another instance initialization method of myClass or its direct
// superclass on this, the only operation the method can perform on this is assigning fields
// declared within myClass.
// Allow inlining a constructor into a constructor of the same class, as the constructor code
// is expected to adhere to the VM specification.
DexType methodHolder = method.method.holder;
boolean methodIsConstructor = method.isInstanceInitializer();
if (methodIsConstructor && methodHolder == invoke.asInvokeMethod().getInvokedMethod().holder) {
return true;
}
// Don't allow inlining a constructor into a non-constructor if the first use of the
// un-initialized object is not an argument of an invoke of <init>.
// Also, we cannot inline a constructor if it initializes final fields, as such is only allowed
// from within a constructor of the corresponding class.
// Lastly, we can only inline a constructor, if its own <init> call is on the method's class. If
// we inline into a constructor, calls to super.<init> are also OK.
InstructionIterator iterator = code.instructionIterator();
Instruction instruction = iterator.next();
// A constructor always has the un-initialized object as the first argument.
assert instruction.isArgument();
Value unInitializedObject = instruction.outValue();
boolean seenSuperInvoke = false;
while (iterator.hasNext()) {
instruction = iterator.next();
if (instruction.inValues().contains(unInitializedObject)) {
if (instruction.isInvokeDirect() && !seenSuperInvoke) {
DexMethod target = instruction.asInvokeDirect().getInvokedMethod();
seenSuperInvoke = appInfo.dexItemFactory.isConstructor(target);
if (seenSuperInvoke
// Calls to init on same class are always OK.
&& target.holder != methodHolder
// If we are inlining into a constructor, calls to superclass init are OK.
&& (!methodHolder.isImmediateSubtypeOf(target.holder) || !methodIsConstructor)) {
return false;
}
}
if (!seenSuperInvoke) {
return false;
}
}
if (instruction.isInstancePut()) {
// Fields may not be initialized outside of a constructor.
if (!methodIsConstructor) {
return false;
}
DexField field = instruction.asInstancePut().getField();
DexEncodedField target = appInfo.lookupInstanceTarget(field.getHolder(), field);
if (target != null && target.accessFlags.isFinal()) {
return false;
}
}
}
return true;
}
public void performInlining(DexEncodedMethod method, IRCode code, CallGraph callGraph) {
int instruction_allowance = 1500;
instruction_allowance -= numberOfInstructions(code);
if (instruction_allowance < 0) {
return;
}
computeReceiverMustBeNonNull(code);
InliningOracle oracle = new InliningOracle(this, method, callGraph);
List<BasicBlock> blocksToRemove = new ArrayList<>();
ListIterator<BasicBlock> blockIterator = code.listIterator();
while (blockIterator.hasNext() && (instruction_allowance >= 0)) {
BasicBlock block = blockIterator.next();
if (blocksToRemove.contains(block)) {
continue;
}
InstructionListIterator iterator = block.listIterator();
while (iterator.hasNext() && (instruction_allowance >= 0)) {
Instruction current = iterator.next();
if (current.isInvokeMethod()) {
InvokeMethod invoke = current.asInvokeMethod();
InlineAction result = invoke.computeInlining(oracle);
if (result != null) {
DexEncodedMethod target = appInfo.lookup(invoke.getType(), invoke.getInvokedMethod());
if (target == null) {
// The declared target cannot be found so skip inlining.
continue;
}
if (!(target.isProcessed() || result.reason == Reason.FORCE)) {
// Do not inline code that was not processed unless we have to force inline.
continue;
}
IRCode inlinee = result
.buildIR(code.valueNumberGenerator, appInfo, graphLense, options);
if (inlinee != null) {
// TODO(64432527): Get rid of this additional check by improved inlining.
if (block.hasCatchHandlers() && inlinee.getNormalExitBlock() == null) {
continue;
}
if (callGraph.isBreaker(method, target)) {
// Make sure we don't inline a call graph breaker.
continue;
}
// If this code did not go through the full pipeline, apply inlining to make sure
// that force inline targets get processed.
if (!target.isProcessed()) {
assert result.reason == Reason.FORCE;
if (Log.ENABLED) {
Log.verbose(getClass(), "Forcing extra inline on " + target.toSourceString());
}
performInlining(target, inlinee, callGraph);
}
// Make sure constructor inlining is legal.
assert !target.isClassInitializer();
if (target.isInstanceInitializer()
&& !legalConstructorInline(method, invoke, inlinee)) {
continue;
}
DexType downcast = null;
if (invoke.isInvokeMethodWithReceiver()) {
// If the invoke has a receiver but the declared method holder is different
// from the computed target holder, inlining requires a downcast of the receiver.
if (result.target.method.getHolder() != target.method.getHolder()) {
downcast = result.target.method.getHolder();
}
}
// Inline the inlinee code in place of the invoke instruction
// Back up before the invoke instruction.
iterator.previous();
instruction_allowance -= numberOfInstructions(inlinee);
if (instruction_allowance >= 0 || result.forceInline()) {
iterator.inlineInvoke(code, inlinee, blockIterator, blocksToRemove, downcast);
}
// If we inlined the invoke from a bridge method, it is no longer a bridge method.
if (method.accessFlags.isBridge()) {
method.accessFlags.unsetSynthetic();
method.accessFlags.unsetBridge();
}
}
}
}
}
}
oracle.finish();
code.removeBlocks(blocksToRemove);
assert code.isConsistentSSA();
}
// Determine whether the receiver of an invocation is guaranteed to be non-null based on
// the dominator tree. If a method call is dominated by another method call with the same
// receiver, the receiver most be non-null when we reach the dominated call.
//
// We bail out for exception handling. If an invoke is covered by a try block we cannot use
// dominance to determine that the receiver is non-null at a dominated call:
//
// Object o;
// try {
// o.m();
// } catch (NullPointerException e) {
// o.f(); // Dominated by other call with receiver o, but o is null.
// }
//
private void computeReceiverMustBeNonNull(IRCode code) {
DominatorTree dominatorTree = new DominatorTree(code);
InstructionIterator it = code.instructionIterator();
while (it.hasNext()) {
Instruction instruction = it.next();
if (instruction.isInvokeMethodWithReceiver()) {
Value receiverValue = instruction.inValues().get(0);
for (Instruction user : receiverValue.uniqueUsers()) {
if (user.isInvokeMethodWithReceiver() &&
user.inValues().get(0) == receiverValue &&
!user.getBlock().hasCatchHandlers() &&
dominatorTree.strictlyDominatedBy(instruction.getBlock(), user.getBlock())) {
instruction.asInvokeMethodWithReceiver().setIsDominatedByCallWithSameReceiver();
break;
}
}
}
}
}
}