blob: 48e501acc2601b1f3ccb6fa66b39721e142763c9 [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.contexts.CompilationContext.MethodProcessingContext;
import com.android.tools.r8.graph.AppView;
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.ProgramMethod;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InstructionOrPhi;
import com.android.tools.r8.ir.conversion.MethodProcessor;
import com.android.tools.r8.ir.conversion.passes.BranchSimplifier;
import com.android.tools.r8.ir.conversion.passes.TrivialCheckCastAndInstanceOfRemover;
import com.android.tools.r8.ir.optimize.AffectedValues;
import com.android.tools.r8.ir.optimize.Inliner;
import com.android.tools.r8.ir.optimize.InliningOracle;
import com.android.tools.r8.ir.optimize.classinliner.InlineCandidateProcessor.IllegalClassInlinerStateException;
import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
import com.android.tools.r8.ir.optimize.inliner.InliningIRProvider;
import com.android.tools.r8.ir.optimize.string.StringOptimizer;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.utils.LazyBox;
import com.android.tools.r8.utils.Timing;
import com.google.common.collect.Lists;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
public final class ClassInliner {
enum EligibilityStatus {
ELIGIBLE,
NOT_ELIGIBLE
}
private final AppView<AppInfoWithLiveness> appView;
private final Inliner inliner;
private final ConcurrentHashMap<DexClass, EligibilityStatus> knownClasses =
new ConcurrentHashMap<>();
public ClassInliner(AppView<AppInfoWithLiveness> appView, Inliner inliner) {
this.appView = appView;
this.inliner = inliner;
}
// Process method code and inline eligible class instantiations, in short:
//
// - collect all 'new-instance' and 'static-get' instructions (called roots below) in
// the original code. Note that class inlining, if happens, mutates code and may add
// new root instructions. Processing them as well is possible, but does not seem to
// bring much value.
//
// - for each 'new-instance' root we check if it is eligible for inlining, i.e:
// -> the class of the new instance is 'eligible' (see computeClassEligible(...))
// -> the instance is initialized with 'eligible' constructor (see comments in
// CodeRewriter::identifyClassInlinerEligibility(...))
// -> has only 'eligible' uses, i.e:
// * as a receiver of a field read/write for a field defined in same class
// as method.
// * as a receiver of virtual or interface call with single target being
// an eligible method according to identifyClassInlinerEligibility(...);
// NOTE: if method receiver is used as a return value, the method call
// should ignore return value
//
// - for each 'static-get' root we check if it is eligible for inlining, i.e:
// -> the class of the new instance is 'eligible' (see computeClassEligible(...))
// *and* has a trivial class constructor (see CoreRewriter::computeClassInitializerInfo
// and description in isClassAndUsageEligible(...)) initializing the field root reads
// -> has only 'eligible' uses, (see above)
//
// - inline eligible root instructions, i.e:
// -> force inline methods called on the instance (including the initializer);
// (this may introduce additional instance field reads/writes on the receiver)
// -> replace instance field reads with appropriate values calculated based on
// fields writes
// -> remove the call to superclass initializer (if root is 'new-instance')
// -> remove all field writes
// -> remove root instructions
//
// For example:
//
// Original code:
// class C {
// static class L {
// final int x;
// L(int x) {
// this.x = x;
// }
// int getX() {
// return x;
// }
// }
// static class F {
// final static F I = new F();
// int getX() {
// return 123;
// }
// }
// static int method1() {
// return new L(1).x;
// }
// static int method2() {
// return new L(1).getX();
// }
// static int method3() {
// return F.I.getX();
// }
// }
//
// Code after class C is 'inlined':
// class C {
// static int method1() {
// return 1;
// }
// static int method2() {
// return 1;
// }
// static int method3() {
// return 123;
// }
// }
//
public void processMethodCode(
ProgramMethod method,
IRCode code,
OptimizationFeedback feedback,
MethodProcessor methodProcessor,
MethodProcessingContext methodProcessingContext,
LazyBox<InliningOracle> defaultOracle) {
// Collect all the new-instance and static-get instructions in the code before inlining.
List<Instruction> roots =
Lists.newArrayList(code.instructions(insn -> insn.isNewInstance() || insn.isStaticGet()));
// We loop inlining iterations until there was no inlining, but still use same set
// of roots to avoid infinite inlining. Looping makes possible for some roots to
// become eligible after other roots are inlined.
boolean anyInlinedGeneratedMessageLiteBuilders = false;
boolean anyInlinedMethods = false;
boolean repeat;
do {
repeat = false;
Iterator<Instruction> rootsIterator = roots.iterator();
while (rootsIterator.hasNext()) {
Instruction root = rootsIterator.next();
InlineCandidateProcessor processor =
new InlineCandidateProcessor(appView, code, inliner, methodProcessor, method, root);
// Assess eligibility of instance and class.
EligibilityStatus status = processor.isInstanceEligible();
if (status != EligibilityStatus.ELIGIBLE) {
// This root will never be inlined.
rootsIterator.remove();
continue;
}
status = isClassEligible(processor.getEligibleClass());
if (status != EligibilityStatus.ELIGIBLE) {
// This root will never be inlined.
rootsIterator.remove();
continue;
}
// Assess users eligibility and compute inlining of direct calls and extra methods needed.
InstructionOrPhi ineligibleUser = processor.areInstanceUsersEligible(defaultOracle);
if (ineligibleUser != null) {
// This root may succeed if users change in future.
continue;
}
// Is inlining allowed.
InliningIRProvider inliningIRProvider =
new InliningIRProvider(
appView, method, code, inliner.getLensCodeRewriter(), methodProcessor);
ClassInlinerCostAnalysis costAnalysis =
new ClassInlinerCostAnalysis(appView, inliningIRProvider, processor.getReceivers());
if (costAnalysis.willExceedInstructionBudget(
code,
processor.getEligibleClass(),
processor.getDirectInlinees(),
processor.getIndirectInlinees())) {
// This root is unlikely to be inlined in the future.
rootsIterator.remove();
continue;
}
if (appView.protoShrinker() != null && root.isNewInstance()) {
DexType instantiatedType = root.asNewInstance().clazz;
DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(instantiatedType));
if (clazz != null
&& appView.protoShrinker().references.isGeneratedMessageLiteBuilder(clazz)) {
anyInlinedGeneratedMessageLiteBuilders = true;
}
}
// Inline the class instance.
AffectedValues affectedValues = new AffectedValues();
try {
anyInlinedMethods |=
processor.processInlining(code, affectedValues, feedback, inliningIRProvider);
} catch (IllegalClassInlinerStateException e) {
// We introduced a user that we cannot handle in the class inliner as a result of force
// inlining. Abort gracefully from class inlining without removing the instance.
//
// Alternatively we would need to collect additional information about the behavior of
// methods (which is bad for memory), or we would need to analyze the called methods
// before inlining them. The latter could be good solution, since we are going to build IR
// for the methods that need to be inlined anyway.
assert false;
anyInlinedMethods = true;
}
// Restore normality.
code.removeAllDeadAndTrivialPhis(affectedValues);
affectedValues.narrowingWithAssumeRemoval(appView, code);
code.removeRedundantBlocks();
assert code.isConsistentSSA(appView);
rootsIterator.remove();
repeat = true;
}
} while (repeat);
if (anyInlinedGeneratedMessageLiteBuilders) {
// Inline all calls to dynamicMethod() where the given MethodToInvoke is considered simple.
appView.withGeneratedMessageLiteBuilderShrinker(
shrinker ->
shrinker.inlineCallsToDynamicMethod(
method, code, feedback, methodProcessor, methodProcessingContext, inliner));
}
if (anyInlinedMethods) {
// If a method was inlined we may be able to remove check-cast instructions because we may
// have more information about the types of the arguments at the call site. This is
// particularly important for bridge methods.
new TrivialCheckCastAndInstanceOfRemover(appView)
.run(code, methodProcessor, methodProcessingContext, Timing.empty());
// If a method was inlined we may be able to prune additional branches.
new BranchSimplifier(appView)
.run(code, methodProcessor, methodProcessingContext, Timing.empty());
// If a method was inlined we may see more trivial computation/conversion of String.
new StringOptimizer(appView)
.run(code, methodProcessor, methodProcessingContext, Timing.empty());
}
}
private EligibilityStatus isClassEligible(DexProgramClass clazz) {
EligibilityStatus eligible = knownClasses.get(clazz);
if (eligible == null) {
EligibilityStatus computed = computeClassEligible(clazz);
EligibilityStatus existing = knownClasses.putIfAbsent(clazz, computed);
assert existing == null || existing == computed;
eligible = existing == null ? computed : existing;
}
return eligible;
}
// Class is eligible for this optimization. Eligibility implementation:
// - is not an abstract class or interface
// - does not declare finalizer
// - does not trigger any static initializers except for its own
private EligibilityStatus computeClassEligible(DexProgramClass clazz) {
if (clazz == null
|| clazz.isAbstract()
|| clazz.isInterface()
|| !appView.getKeepInfo(clazz).isClassInliningAllowed(appView.options())) {
return EligibilityStatus.NOT_ELIGIBLE;
}
// Class must not define finalizer.
DexItemFactory dexItemFactory = appView.dexItemFactory();
if (clazz.lookupMethod(dexItemFactory.objectMembers.finalize) != null) {
return EligibilityStatus.NOT_ELIGIBLE;
}
// Check for static initializers in this class or any of interfaces it implements.
if (clazz.classInitializationMayHaveSideEffects(appView)) {
return EligibilityStatus.NOT_ELIGIBLE;
}
if (!appView.testing().allowClassInliningOfSynthetics
&& appView.getSyntheticItems().isSyntheticClass(clazz)) {
return EligibilityStatus.NOT_ELIGIBLE;
}
return EligibilityStatus.ELIGIBLE;
}
}