blob: 2b30686d4dde29ef052aa28343934126b8cf689d [file] [log] [blame]
// Copyright (c) 2023, 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.numberunboxer;
import static com.android.tools.r8.ir.optimize.numberunboxer.MethodBoxingStatus.UNPROCESSED_CANDIDATE;
import static com.android.tools.r8.ir.optimize.numberunboxer.NumberUnboxerBoxingStatusResolution.MethodBoxingStatusResult.BoxingStatusResult.UNBOX;
import static com.android.tools.r8.ir.optimize.numberunboxer.ValueBoxingStatus.NOT_UNBOXABLE;
import com.android.tools.r8.errors.Unimplemented;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexClassAndMember;
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.DexType;
import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
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.InvokeMethod;
import com.android.tools.r8.ir.code.Return;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.conversion.PostMethodProcessor;
import com.android.tools.r8.ir.optimize.numberunboxer.NumberUnboxerBoxingStatusResolution.MethodBoxingStatusResult;
import com.android.tools.r8.ir.optimize.numberunboxer.TransitiveDependency.MethodArg;
import com.android.tools.r8.ir.optimize.numberunboxer.TransitiveDependency.MethodRet;
import com.android.tools.r8.optimize.argumentpropagation.utils.ProgramClassesBidirectedGraph;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.utils.BooleanUtils;
import com.android.tools.r8.utils.MapUtils;
import com.android.tools.r8.utils.ThreadUtils;
import com.android.tools.r8.utils.Timing;
import com.android.tools.r8.utils.collections.DexMethodSignatureMap;
import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
public class NumberUnboxerImpl extends NumberUnboxer {
private final AppView<AppInfoWithLiveness> appView;
private final DexItemFactory factory;
private final Set<DexType> boxedTypes;
// All candidate methods are initialized to UNPROCESSED_CANDIDATE (bottom) and methods not in
// this map are not subject to unboxing.
private final Map<DexMethod, MethodBoxingStatus> candidateBoxingStatus =
new ConcurrentHashMap<>();
private Map<DexMethod, DexMethod> virtualMethodsRepresentative;
public NumberUnboxerImpl(AppView<AppInfoWithLiveness> appView) {
this.appView = appView;
this.factory = appView.dexItemFactory();
this.boxedTypes = factory.primitiveToBoxed.values();
}
/**
* The preparation agglomerate targets or virtual calls into a deterministic method amongst them.
* This allows R8 to compute the boxing status once for all targets of the same call.
*/
@Override
public void prepareForPrimaryOptimizationPass(Timing timing, ExecutorService executorService)
throws ExecutionException {
timing.begin("Prepare number unboxer tree fixer");
ImmediateProgramSubtypingInfo immediateSubtypingInfo =
ImmediateProgramSubtypingInfo.create(appView);
List<Set<DexProgramClass>> connectedComponents =
new ProgramClassesBidirectedGraph(appView, immediateSubtypingInfo)
.computeStronglyConnectedComponents();
Set<Map<DexMethod, DexMethod>> virtualMethodsRepresentativeToMerge =
ConcurrentHashMap.newKeySet();
ThreadUtils.processItems(
connectedComponents,
component ->
virtualMethodsRepresentativeToMerge.add(computeVirtualMethodRepresentative(component)),
appView.options().getThreadingModule(),
executorService);
virtualMethodsRepresentative =
MapUtils.newImmutableMap(
builder -> virtualMethodsRepresentativeToMerge.forEach(builder::putAll));
timing.end();
}
// TODO(b/307872552): Do not store irrelevant representative.
private Map<DexMethod, DexMethod> computeVirtualMethodRepresentative(
Set<DexProgramClass> component) {
DexMethodSignatureMap<List<ProgramMethod>> componentVirtualMethods =
DexMethodSignatureMap.create();
for (DexProgramClass clazz : component) {
for (ProgramMethod virtualProgramMethod : clazz.virtualProgramMethods()) {
List<ProgramMethod> set =
componentVirtualMethods.computeIfAbsent(virtualProgramMethod, k -> new ArrayList<>());
set.add(virtualProgramMethod);
}
for (ProgramMethod candidate : clazz.directProgramMethods()) {
if (shouldConsiderForUnboxing(candidate)) {
candidateBoxingStatus.put(candidate.getReference(), UNPROCESSED_CANDIDATE);
}
}
}
Map<DexMethod, DexMethod> vMethodRepresentative = new IdentityHashMap<>();
for (List<ProgramMethod> vMethods : componentVirtualMethods.values()) {
if (vMethods.size() > 1) {
if (Iterables.all(vMethods, this::shouldConsiderForUnboxing)
&& Iterables.any(vMethods, m -> !m.getDefinition().isAbstract())) {
vMethods.sort(Comparator.comparing(DexClassAndMember::getReference));
ProgramMethod representative = vMethods.get(0);
for (int i = 1; i < vMethods.size(); i++) {
vMethodRepresentative.put(
vMethods.get(i).getReference(), representative.getReference());
}
candidateBoxingStatus.put(representative.getReference(), UNPROCESSED_CANDIDATE);
}
} else {
assert vMethods.size() == 1;
ProgramMethod candidate = vMethods.get(0);
if (shouldConsiderForUnboxing(candidate) && !candidate.getDefinition().isAbstract()) {
candidateBoxingStatus.put(candidate.getReference(), UNPROCESSED_CANDIDATE);
}
}
}
return vMethodRepresentative;
}
private void registerMethodUnboxingStatusIfNeeded(
ProgramMethod method, ValueBoxingStatus returnStatus, ValueBoxingStatus[] args) {
DexMethod representative = representative(method.getReference());
if (args == null && (returnStatus == null || returnStatus.isNotUnboxable())) {
// Effectively NOT_UNBOXABLE, remove the candidate.
// TODO(b/307872552): Do we need to remove at the end of the wave for determinism?
candidateBoxingStatus.remove(representative);
return;
}
ValueBoxingStatus nonNullReturnStatus = returnStatus == null ? NOT_UNBOXABLE : returnStatus;
ValueBoxingStatus[] nonNullArgs =
args == null ? ValueBoxingStatus.notUnboxableArray(method.getReference().getArity()) : args;
MethodBoxingStatus unboxingStatus = MethodBoxingStatus.create(nonNullReturnStatus, nonNullArgs);
assert !unboxingStatus.isNoneUnboxable();
MethodBoxingStatus newStatus =
candidateBoxingStatus.computeIfPresent(
representative, (m, old) -> old.merge(unboxingStatus));
if (newStatus != null && newStatus.isNoneUnboxable()) {
// TODO(b/307872552): Do we need to remove at the end of the wave for determinism?
candidateBoxingStatus.remove(representative);
}
}
private DexMethod representative(DexMethod method) {
return virtualMethodsRepresentative.getOrDefault(method, method);
}
/**
* Analysis phase: Figures out in each method if parameters, invoke, field accesses and return
* values are used in boxing operations.
*/
@Override
public void analyze(IRCode code) {
DexMethod contextReference = code.context().getReference();
ValueBoxingStatus[] args = null;
ValueBoxingStatus returnStatus = null;
int shift = BooleanUtils.intValue(!code.context().getDefinition().isStatic());
for (Instruction next : code.instructions()) {
if (next.isArgument()) {
ValueBoxingStatus unboxingStatus = analyzeOutput(next.outValue());
if (unboxingStatus.mayBeUnboxable()) {
if (args == null) {
args = new ValueBoxingStatus[contextReference.getArity()];
Arrays.fill(args, NOT_UNBOXABLE);
}
args[next.asArgument().getIndex() - shift] = unboxingStatus;
}
} else if (next.isReturn()) {
Return ret = next.asReturn();
if (ret.hasReturnValue() && (returnStatus == null || returnStatus.mayBeUnboxable())) {
ValueBoxingStatus unboxingStatus = analyzeInput(ret.returnValue(), code.context());
if (unboxingStatus.mayBeUnboxable()) {
returnStatus =
returnStatus == null ? unboxingStatus : returnStatus.merge(unboxingStatus);
} else {
returnStatus = NOT_UNBOXABLE;
}
}
} else if (next.isInvokeMethod()) {
analyzeInvoke(next.asInvokeMethod(), code.context());
} else if (next.isInvokeCustom()) {
throw new Unimplemented();
}
}
// TODO(b/307872552): Analyse field access to unbox fields.
registerMethodUnboxingStatusIfNeeded(code.context(), returnStatus, args);
}
private void analyzeInvoke(InvokeMethod invoke, ProgramMethod context) {
ProgramMethod resolvedMethod =
appView
.appInfo()
.resolveMethodLegacy(invoke.getInvokedMethod(), invoke.getInterfaceBit())
.getResolvedProgramMethod();
if (resolvedMethod == null) {
return;
}
ValueBoxingStatus[] args = null;
int shift = invoke.getFirstNonReceiverArgumentIndex();
for (int i = shift; i < invoke.inValues().size(); i++) {
ValueBoxingStatus unboxingStatus = analyzeInput(invoke.getArgument(i), context);
if (unboxingStatus.mayBeUnboxable()) {
if (args == null) {
args = new ValueBoxingStatus[invoke.getInvokedMethod().getArity()];
Arrays.fill(args, NOT_UNBOXABLE);
}
args[i - shift] = unboxingStatus;
}
}
ValueBoxingStatus returnVal = null;
if (invoke.hasOutValue()) {
ValueBoxingStatus unboxingStatus = analyzeOutput(invoke.outValue());
if (unboxingStatus.mayBeUnboxable()) {
returnVal = unboxingStatus;
}
}
registerMethodUnboxingStatusIfNeeded(resolvedMethod, returnVal, args);
}
private boolean shouldConsiderForUnboxing(Value value) {
return value.getType().isClassType()
&& shouldConsiderForUnboxing(value.getType().asClassType().getClassType());
}
private boolean shouldConsiderForUnboxing(ProgramMethod method) {
if (appView.getKeepInfo().isPinned(method, appView.options())) {
return false;
}
return shouldConsiderForUnboxing(method.getReturnType())
|| Iterables.any(method.getParameters(), this::shouldConsiderForUnboxing);
}
private boolean shouldConsiderForUnboxing(DexType type) {
// TODO(b/307872552): So far we consider only boxed type value to unbox them into their
// corresponding primitive type, for example, Integer -> int. It would be nice to support
// the pattern checkCast(BoxType) followed by a boxing operation, so that for example when
// we have MyClass<T> and T is proven to be an Integer, we can unbox into int.
// Types to consider: Object, Serializable, Comparable, Number.
return boxedTypes.contains(type);
}
// Inputs are values flowing into a method return, an invoke argument or a field write.
private ValueBoxingStatus analyzeInput(Value inValue, ProgramMethod context) {
if (!shouldConsiderForUnboxing(inValue)) {
return NOT_UNBOXABLE;
}
DexType boxedType = inValue.getType().asClassType().getClassType();
DexType primitiveType = factory.primitiveToBoxed.inverse().get(boxedType);
DexMethod boxPrimitiveMethod = factory.getBoxPrimitiveMethod(primitiveType);
if (!inValue.getAliasedValue().isPhi()) {
Instruction definition = inValue.getAliasedValue().getDefinition();
if (definition.isArgument()) {
int shift = BooleanUtils.intValue(!context.getDefinition().isStatic());
return ValueBoxingStatus.with(
new MethodArg(
definition.asArgument().getIndex() - shift,
representative(context.getReference())));
}
if (definition.isInvokeMethod()) {
if (boxPrimitiveMethod.isIdenticalTo(definition.asInvokeMethod().getInvokedMethod())) {
// The result of a boxing operation is non nullable.
if (!inValue.hasPhiUsers() && inValue.hasSingleUniqueUser()) {
// Unboxing would remove a boxing operation.
return ValueBoxingStatus.with(1);
}
// Unboxing would add and remove a boxing operation.
return ValueBoxingStatus.with(0);
}
InvokeMethod invoke = definition.asInvokeMethod();
ProgramMethod resolvedMethod =
appView
.appInfo()
.resolveMethodLegacy(invoke.getInvokedMethod(), invoke.getInterfaceBit())
.getResolvedProgramMethod();
if (resolvedMethod != null) {
return ValueBoxingStatus.with(
new MethodRet(representative(resolvedMethod.getReference())));
}
}
}
// TODO(b/307872552) We should support field reads as transitive dependencies.
if (inValue.getType().isNullable()) {
return NOT_UNBOXABLE;
}
// TODO(b/307872552): We could analyze simple phis, for example,
// Integer i = bool ? integer.valueOf(1) : Integer.valueOf(2);
// removes 2 operations and does not add 1.
// Since we cannot interpret the definition, unboxing adds a boxing operation.
return ValueBoxingStatus.with(-1);
}
// Outputs are method arguments, invoke return values and field reads.
private ValueBoxingStatus analyzeOutput(Value outValue) {
if (!shouldConsiderForUnboxing(outValue)) {
return NOT_UNBOXABLE;
}
DexType boxedType = outValue.getType().asClassType().getClassType();
DexMethod unboxPrimitiveMethod = factory.getUnboxPrimitiveMethod(boxedType);
boolean metUnboxingOperation = false;
boolean metOtherOperation = outValue.hasPhiUsers();
for (Instruction uniqueUser : outValue.aliasedUsers()) {
if (uniqueUser.isAssumeWithNonNullAssumption()) {
// Nothing to do, the assume will be removed by unboxing.
} else if (uniqueUser.isInvokeMethod()
&& unboxPrimitiveMethod.isIdenticalTo(uniqueUser.asInvokeMethod().getInvokedMethod())) {
metUnboxingOperation = true;
} else {
metOtherOperation = true;
}
}
return ValueBoxingStatus.with(computeBoxingDelta(metUnboxingOperation, metOtherOperation));
}
private int computeBoxingDelta(boolean metUnboxingOperation, boolean metOtherOperation) {
if (metUnboxingOperation) {
if (metOtherOperation) {
// Unboxing would add and remove a boxing operation.
return 0;
}
// Unboxing would remove a boxing operation.
return 1;
}
if (metOtherOperation) {
// Unboxing would add a boxing operation.
return -1;
}
// Unused, unboxing won't change the number of boxing operations.
return 0;
}
@Override
public void unboxNumbers(
PostMethodProcessor.Builder postMethodProcessorBuilder,
Timing timing,
ExecutorService executorService)
throws ExecutionException {
Map<DexMethod, MethodBoxingStatusResult> unboxingResult =
new NumberUnboxerBoxingStatusResolution(candidateBoxingStatus).resolve();
if (unboxingResult.isEmpty()) {
return;
}
NumberUnboxerLens numberUnboxerLens =
new NumberUnboxerTreeFixer(appView, unboxingResult, virtualMethodsRepresentative)
.fixupTree(executorService, timing);
appView.rewriteWithLens(numberUnboxerLens, executorService, timing);
new NumberUnboxerMethodReprocessingEnqueuer(appView, numberUnboxerLens)
.enqueueMethodsForReprocessing(postMethodProcessorBuilder, executorService, timing);
if (appView.testing().printNumberUnboxed) {
printNumberUnboxed(unboxingResult);
}
}
private void printNumberUnboxed(Map<DexMethod, MethodBoxingStatusResult> unboxingResult) {
StringBuilder stringBuilder = new StringBuilder();
unboxingResult.forEach(
(k, v) -> {
if (v.getRet() == UNBOX) {
stringBuilder
.append("Unboxing of return value of ")
.append(k)
.append(System.lineSeparator());
}
for (int i = 0; i < v.getArgs().length; i++) {
if (v.getArg(i) == UNBOX) {
stringBuilder
.append("Unboxing of arg ")
.append(i)
.append(" of ")
.append(k)
.append(System.lineSeparator());
}
}
});
appView.reporter().warning(stringBuilder.toString());
}
@Override
public void onMethodPruned(ProgramMethod method) {
// TODO(b/307872552): Should we do something about this? We might need to change the
// representative.
}
@Override
public void onMethodCodePruned(ProgramMethod method) {
// TODO(b/307872552): I don't think we should do anything here.
}
@Override
public void rewriteWithLens() {
// TODO(b/307872552): This needs to rewrite the methodBoxingStatus.
}
}