blob: 9ff44ebce3b08fb4c463661ec395f0f7266157b1 [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 com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.AppInfo;
import com.android.tools.r8.graph.AppInfoWithSubtyping;
import com.android.tools.r8.graph.DexClass;
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.DexType;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.InstanceGet;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InvokeDirect;
import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
import com.android.tools.r8.ir.code.NewInstance;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.optimize.Inliner.InliningInfo;
import com.android.tools.r8.ir.optimize.Inliner.Reason;
import com.google.common.collect.Streams;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public final class ClassInliner {
private final DexItemFactory factory;
private final ConcurrentHashMap<DexType, Boolean> knownClasses = new ConcurrentHashMap<>();
private static final Map<DexField, Integer> NO_MAPPING = new IdentityHashMap<>();
public interface InlinerAction {
void inline(Map<InvokeMethodWithReceiver, InliningInfo> methods);
}
public ClassInliner(DexItemFactory factory) {
this.factory = factory;
}
// Process method code and inline eligible class instantiations, in short:
//
// - collect all 'new-instance' instructions in the original code. Note that class
// inlining, if happens, mutates code and can add 'new-instance' instructions.
// Processing them as well is possible, but does not seem to have much value.
//
// - for each 'new-instance' 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
// onlyInitializesFieldsWithNoOtherSideEffects flag in method's optimization
// info); eligible constructor also defines a set of instance fields directly
// initialized with parameter values, called field initialization mapping below
// -> has only 'eligible' uses, i.e:
// * it is a receiver of a field read if the field is present in the
// field initialization mapping
// * it is a receiver of virtual or interface call with single target being
// a method only reading fields in the current field initialization mapping
//
// - inline eligible 'new-instance' instructions, i.e:
// -> force inline methods called on the instance (which may introduce additional
// instance field reads, but only for fields present in the current field
// initialization mapping)
// -> replace instance field reads with appropriate values passed to the constructor
// according to field initialization mapping
// -> remove constructor call
// -> remove 'new-instance' instructions
//
// For example:
//
// Original code:
// class C {
// static class L {
// final int x;
// L(int x) {
// this.x = x;
// }
// int getX() {
// return x;
// }
// }
// static int method1() {
// return new L(1).x;
// }
// static int method2() {
// return new L(1).getX();
// }
// }
//
// Code after class C is 'inlined':
// class C {
// static int method1() {
// return 1;
// }
// static int method2() {
// return 1;
// }
// }
//
public final void processMethodCode(
AppInfoWithSubtyping appInfo,
DexEncodedMethod method,
IRCode code,
Predicate<DexEncodedMethod> isProcessedConcurrently,
InlinerAction inliner) {
// Collect all the new-instance instructions in the code before inlining.
List<NewInstance> newInstances = Streams.stream(code.instructionIterator())
.filter(Instruction::isNewInstance)
.map(Instruction::asNewInstance)
.collect(Collectors.toList());
nextNewInstance:
for (NewInstance newInstance : newInstances) {
Value eligibleInstance = newInstance.outValue();
if (eligibleInstance == null) {
continue;
}
DexType eligibleClass = newInstance.clazz;
if (!isClassEligible(appInfo, eligibleClass)) {
continue;
}
// No Phi users.
if (eligibleInstance.numberOfPhiUsers() > 0) {
continue;
}
Set<Instruction> uniqueUsers = eligibleInstance.uniqueUsers();
// Find an initializer invocation.
InvokeDirect eligibleInitCall = null;
Map<DexField, Integer> mappings = null;
for (Instruction user : uniqueUsers) {
if (!user.isInvokeDirect()) {
continue;
}
InvokeDirect candidate = user.asInvokeDirect();
DexMethod candidateInit = candidate.getInvokedMethod();
if (factory.isConstructor(candidateInit) &&
candidate.inValues().lastIndexOf(eligibleInstance) == 0) {
if (candidateInit.holder != eligibleClass) {
// Inlined constructor call? We won't get field initialization mapping in this
// case, but since we only support eligible classes extending java.lang.Object,
// it's safe to assume an empty mapping.
if (candidateInit.holder == factory.objectType) {
mappings = Collections.emptyMap();
}
} else {
// Is it a call to an *eligible* constructor?
mappings = getConstructorFieldMappings(appInfo, candidateInit, isProcessedConcurrently);
}
eligibleInitCall = candidate;
break;
}
}
if (mappings == null) {
continue;
}
// Check all regular users.
Map<InvokeMethodWithReceiver, InliningInfo> methodCalls = new IdentityHashMap<>();
for (Instruction user : uniqueUsers) {
if (user == eligibleInitCall) {
continue /* next user */;
}
if (user.isInstanceGet()) {
InstanceGet instanceGet = user.asInstanceGet();
if (mappings.containsKey(instanceGet.getField())) {
continue /* next user */;
}
// Not replaceable field read.
continue nextNewInstance;
}
if (user.isInvokeVirtual() || user.isInvokeInterface()) {
InvokeMethodWithReceiver invoke = user.asInvokeMethodWithReceiver();
if (invoke.inValues().lastIndexOf(eligibleInstance) > 0) {
continue nextNewInstance; // Instance must only be passes as a receiver.
}
DexEncodedMethod singleTarget =
findSingleTarget(appInfo, invoke, eligibleClass);
if (singleTarget == null) {
continue nextNewInstance;
}
if (isProcessedConcurrently.test(singleTarget)) {
continue nextNewInstance;
}
if (method == singleTarget) {
continue nextNewInstance; // Don't inline itself.
}
if (!singleTarget.getOptimizationInfo()
.isReceiverOnlyUsedForReadingFields(mappings.keySet())) {
continue nextNewInstance; // Target must be trivial.
}
if (!singleTarget.isInliningCandidate(method, Reason.SIMPLE, appInfo)) {
// 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.
continue nextNewInstance;
}
methodCalls.put(invoke, new InliningInfo(singleTarget, eligibleClass));
continue /* next user */;
}
continue nextNewInstance; // Unsupported user.
}
// Force-inline of method invocation if any.
inlineAllCalls(inliner, methodCalls);
assert assertOnlyConstructorAndFieldReads(eligibleInstance, eligibleInitCall, mappings);
// Replace all field reads with arguments passed to the constructor.
patchFieldReads(eligibleInstance, eligibleInitCall, mappings);
assert assertOnlyConstructor(eligibleInstance, eligibleInitCall);
// Remove constructor call and new-instance instructions.
removeInstruction(eligibleInitCall);
removeInstruction(newInstance);
code.removeAllTrivialPhis();
}
}
private void inlineAllCalls(
InlinerAction inliner, Map<InvokeMethodWithReceiver, InliningInfo> methodCalls) {
if (!methodCalls.isEmpty()) {
inliner.inline(methodCalls);
}
}
private void patchFieldReads(
Value instance, InvokeDirect invokeMethod, Map<DexField, Integer> mappings) {
for (Instruction user : instance.uniqueUsers()) {
if (!user.isInstanceGet()) {
continue;
}
InstanceGet fieldRead = user.asInstanceGet();
// Replace the field read with
assert mappings.containsKey(fieldRead.getField());
Value arg = invokeMethod.inValues().get(1 + mappings.get(fieldRead.getField()));
assert arg != null;
Value value = fieldRead.outValue();
if (value != null) {
value.replaceUsers(arg);
assert value.numberOfAllUsers() == 0;
}
// Remove instruction.
removeInstruction(fieldRead);
}
}
private void removeInstruction(Instruction instruction) {
instruction.inValues().forEach(v -> v.removeUser(instruction));
instruction.getBlock().removeInstruction(instruction);
}
private boolean assertOnlyConstructorAndFieldReads(
Value instance, InvokeDirect invokeMethod, Map<DexField, Integer> mappings) {
for (Instruction user : instance.uniqueUsers()) {
if (user != invokeMethod &&
!(user.isInstanceGet() && mappings.containsKey(user.asFieldInstruction().getField()))) {
throw new Unreachable("Not all calls are inlined!");
}
}
return true;
}
private boolean assertOnlyConstructor(Value instance, InvokeDirect invokeMethod) {
for (Instruction user : instance.uniqueUsers()) {
if (user != invokeMethod) {
throw new Unreachable("Not all field reads are substituted!");
}
}
return true;
}
private DexEncodedMethod findSingleTarget(
AppInfo appInfo, InvokeMethodWithReceiver invoke, DexType instanceType) {
// We don't use computeSingleTarget(...) on invoke since it sometimes fails to
// find the single target, while this code may be more successful since we exactly
// know what is the actual type of the receiver.
// Note that we also intentionally limit ourselves to methods directly defined in
// the instance's class. This may be improved later.
DexClass clazz = appInfo.definitionFor(instanceType);
if (clazz != null) {
DexMethod callee = invoke.getInvokedMethod();
for (DexEncodedMethod candidate : clazz.virtualMethods()) {
if (candidate.method.name == callee.name && candidate.method.proto == callee.proto) {
return candidate;
}
}
}
return null;
}
private Map<DexField, Integer> getConstructorFieldMappings(
AppInfo appInfo, DexMethod init, Predicate<DexEncodedMethod> isProcessedConcurrently) {
assert isClassEligible(appInfo, init.holder);
DexEncodedMethod definition = appInfo.definitionFor(init);
if (definition == null) {
return NO_MAPPING;
}
if (isProcessedConcurrently.test(definition)) {
return NO_MAPPING;
}
if (definition.accessFlags.isAbstract() || definition.accessFlags.isNative()) {
return NO_MAPPING;
}
return definition.getOptimizationInfo().onlyInitializesFieldsWithNoOtherSideEffects();
}
private boolean isClassEligible(AppInfo appInfo, DexType clazz) {
Boolean eligible = knownClasses.get(clazz);
if (eligible == null) {
Boolean computed = computeClassEligible(appInfo, clazz);
Boolean 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:
// - not an abstract or interface
// - directly extends java.lang.Object
// - does not declare finalizer
// - does not trigger any static initializers
private boolean computeClassEligible(AppInfo appInfo, DexType clazz) {
DexClass definition = appInfo.definitionFor(clazz);
if (definition == null || definition.isLibraryClass() ||
definition.accessFlags.isAbstract() || definition.accessFlags.isInterface()) {
return false;
}
// Must directly extend Object.
if (definition.superType != factory.objectType) {
return false;
}
// Class must not define finalizer.
for (DexEncodedMethod method : definition.virtualMethods()) {
if (method.method.name == factory.finalizeMethodName &&
method.method.proto == factory.objectMethods.finalize.proto) {
return false;
}
}
// Check for static initializers in this class or any of interfaces it implements.
return !appInfo.canTriggerStaticInitializer(clazz);
}
}