blob: 7a0a731a68d487025eaa10f801fecd513be0a700 [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 static com.android.tools.r8.ir.code.Opcodes.CONST_CLASS;
import static com.android.tools.r8.ir.code.Opcodes.CONST_NUMBER;
import static com.android.tools.r8.ir.code.Opcodes.CONST_STRING;
import static com.android.tools.r8.ir.code.Opcodes.DEX_ITEM_BASED_CONST_STRING;
import static com.android.tools.r8.ir.code.Opcodes.INSTANCE_GET;
import static com.android.tools.r8.ir.code.Opcodes.STATIC_GET;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.FieldAccessFlags;
import com.android.tools.r8.graph.FieldResolutionResult.SingleFieldResolutionResult;
import com.android.tools.r8.graph.ProgramField;
import com.android.tools.r8.graph.ProgramMethod;
import com.android.tools.r8.ir.analysis.value.AbstractValue;
import com.android.tools.r8.ir.analysis.value.SingleFieldValue;
import com.android.tools.r8.ir.code.BasicBlock;
import com.android.tools.r8.ir.code.ConstClass;
import com.android.tools.r8.ir.code.ConstNumber;
import com.android.tools.r8.ir.code.ConstString;
import com.android.tools.r8.ir.code.DexItemBasedConstString;
import com.android.tools.r8.ir.code.FieldGet;
import com.android.tools.r8.ir.code.FieldInstruction;
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.InstructionListIterator;
import com.android.tools.r8.ir.code.StaticGet;
import com.android.tools.r8.ir.code.Value;
import it.unimi.dsi.fastutil.Hash.Strategy;
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenCustomHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectSortedMap.FastSortedEntrySet;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Canonicalize constants.
*/
public class ConstantCanonicalizer {
// Threshold to limit the number of constant canonicalization.
private static final int MAX_CANONICALIZED_CONSTANT = 22;
private final AppView<?> appView;
private final CodeRewriter codeRewriter;
private final ProgramMethod context;
private final IRCode code;
private final boolean isAccessingVolatileField;
public ConstantCanonicalizer(
AppView<?> appView, CodeRewriter codeRewriter, ProgramMethod context, IRCode code) {
this.appView = appView;
this.codeRewriter = codeRewriter;
this.context = context;
this.code = code;
this.isAccessingVolatileField = computeIsAccessingVolatileField(appView, code);
}
private static boolean computeIsAccessingVolatileField(AppView<?> appView, IRCode code) {
if (!appView.hasClassHierarchy()) {
// Conservatively return true.
return true;
}
AppInfoWithClassHierarchy appInfo = appView.appInfoWithClassHierarchy();
for (FieldInstruction fieldGet :
code.<FieldInstruction>instructions(Instruction::isFieldInstruction)) {
SingleFieldResolutionResult<?> resolutionResult =
appInfo.resolveField(fieldGet.getField()).asSingleFieldResolutionResult();
if (resolutionResult == null
|| resolutionResult.getResolvedField().getAccessFlags().isVolatile()) {
return true;
}
}
return false;
}
public void canonicalize() {
Object2ObjectLinkedOpenCustomHashMap<Instruction, List<Value>> valuesDefinedByConstant =
new Object2ObjectLinkedOpenCustomHashMap<>(
new Strategy<Instruction>() {
@Override
public int hashCode(Instruction candidate) {
assert candidate.instructionTypeCanBeCanonicalized();
switch (candidate.opcode()) {
case CONST_CLASS:
return candidate.asConstClass().getValue().hashCode();
case CONST_NUMBER:
return Long.hashCode(candidate.asConstNumber().getRawValue())
+ 13 * candidate.outType().hashCode();
case CONST_STRING:
return candidate.asConstString().getValue().hashCode();
case DEX_ITEM_BASED_CONST_STRING:
return candidate.asDexItemBasedConstString().getItem().hashCode();
case INSTANCE_GET:
case STATIC_GET:
return candidate.asFieldGet().getField().hashCode();
default:
throw new Unreachable();
}
}
@Override
public boolean equals(Instruction a, Instruction b) {
if (a == b) {
return true;
}
if (a == null || b == null || a.getClass() != b.getClass()) {
return false;
}
if (a.isInstanceGet() && a.getFirstOperand() != b.getFirstOperand()) {
return false;
}
return a.identicalNonValueNonPositionParts(b);
}
});
// Collect usages of constants that can be canonicalized.
for (Instruction current : code.instructions()) {
if (isConstantCanonicalizationCandidate(current)) {
List<Value> oldValuesDefinedByConstant =
valuesDefinedByConstant.computeIfAbsent(current, k -> new ArrayList<>());
oldValuesDefinedByConstant.add(current.outValue());
}
}
if (valuesDefinedByConstant.isEmpty()) {
return;
}
// Double-check the entry block does not have catch handlers.
// Otherwise, we need to split it before moving canonicalized const-string, which may throw.
assert !code.entryBlock().hasCatchHandlers();
FastSortedEntrySet<Instruction, List<Value>> entries =
valuesDefinedByConstant.object2ObjectEntrySet();
// Sort the most frequently used constant first and exclude constant use only one time, such
// as the {@code MAX_CANONICALIZED_CONSTANT} will be canonicalized into the entry block.
Iterator<Object2ObjectMap.Entry<Instruction, List<Value>>> iterator =
entries.stream()
.filter(a -> a.getValue().size() > 1)
.sorted((a, b) -> Integer.compare(b.getValue().size(), a.getValue().size()))
.limit(MAX_CANONICALIZED_CONSTANT)
.iterator();
if (!iterator.hasNext()) {
return;
}
boolean shouldSimplifyIfs = false;
do {
Object2ObjectMap.Entry<Instruction, List<Value>> entry = iterator.next();
Instruction canonicalizedConstant = entry.getKey();
assert canonicalizedConstant.instructionTypeCanBeCanonicalized();
Instruction newConst;
if (canonicalizedConstant.getBlock().isEntry()) {
newConst = canonicalizedConstant;
} else {
switch (canonicalizedConstant.opcode()) {
case CONST_CLASS:
newConst = ConstClass.copyOf(code, canonicalizedConstant.asConstClass());
break;
case CONST_NUMBER:
newConst = ConstNumber.copyOf(code, canonicalizedConstant.asConstNumber());
break;
case CONST_STRING:
newConst = ConstString.copyOf(code, canonicalizedConstant.asConstString());
break;
case DEX_ITEM_BASED_CONST_STRING:
newConst =
DexItemBasedConstString.copyOf(
code, canonicalizedConstant.asDexItemBasedConstString());
break;
case INSTANCE_GET:
newConst = InstanceGet.copyOf(code, canonicalizedConstant.asInstanceGet());
break;
case STATIC_GET:
newConst = StaticGet.copyOf(code, canonicalizedConstant.asStaticGet());
break;
default:
throw new Unreachable();
}
insertCanonicalizedConstant(newConst);
}
for (Value outValue : entry.getValue()) {
outValue.replaceUsers(newConst.outValue());
}
shouldSimplifyIfs |= newConst.outValue().hasUserThatMatches(Instruction::isIf);
} while (iterator.hasNext());
shouldSimplifyIfs |= code.removeAllDeadAndTrivialPhis();
if (shouldSimplifyIfs) {
codeRewriter.simplifyIf(code);
}
assert code.isConsistentSSA(appView);
}
public boolean isConstantCanonicalizationCandidate(Instruction instruction) {
// Interested only in instructions types that can be canonicalized, i.e., ConstClass,
// ConstNumber, (DexItemBased)?ConstString, InstanceGet and StaticGet.
switch (instruction.opcode()) {
case CONST_CLASS:
// Do not canonicalize ConstClass that may have side effects. Its original instructions
// will not be removed by dead code remover due to the side effects.
if (instruction.instructionMayHaveSideEffects(appView, context)) {
return false;
}
break;
case CONST_NUMBER:
break;
case CONST_STRING:
case DEX_ITEM_BASED_CONST_STRING:
// Do not canonicalize ConstString instructions if there are monitor operations in the code.
// That could lead to unbalanced locking and could lead to situations where OOM exceptions
// could leave a synchronized method without unlocking the monitor.
if (code.metadata().mayHaveMonitorInstruction()) {
return false;
}
break;
case INSTANCE_GET:
{
InstanceGet instanceGet = instruction.asInstanceGet();
if (!instanceGet.object().isThis()) {
// TODO(b/236661949): Generalize this to more receivers. For canonicalization we need
// the receiver to be non-null (or the instruction can throw) and we also currently
// require the receiver to be defined on-entry, since we always hoist constants to the
// entry block.
return false;
}
if (!isReadOfFinalFieldOutsideInitializer(instanceGet)) {
return false;
}
break;
}
case STATIC_GET:
{
// Canonicalize effectively final fields that are guaranteed to be written before they are
// read. This is only OK if the instruction cannot have side effects.
StaticGet staticGet = instruction.asStaticGet();
if (staticGet.instructionMayHaveSideEffects(appView, context)) {
return false;
}
if (!isReadOfFinalFieldOutsideInitializer(staticGet)
&& !isEffectivelyFinalField(staticGet)) {
return false;
}
break;
}
default:
assert !instruction.instructionTypeCanBeCanonicalized() : instruction.toString();
return false;
}
// Constants with local info must not be canonicalized and must be filtered.
if (instruction.outValue().hasLocalInfo()) {
return false;
}
// Constants that are used by invoke range are not canonicalized to be compliant with the
// optimization splitRangeInvokeConstant that gives the register allocator more freedom in
// assigning register to ranged invokes which can greatly reduce the number of register
// needed (and thereby code size as well). Thus no need to do a transformation that should
// be removed later by another optimization.
if (constantUsedByInvokeRange(instruction)) {
return false;
}
return true;
}
private boolean isReadOfFinalFieldOutsideInitializer(FieldGet fieldGet) {
if (isAccessingVolatileField) {
// A final field may be initialized concurrently. A requirement for this is that the field is
// volatile. However, the reading or writing of another volatile field also allows for
// concurrently initializing a non-volatile field. See also redundant field load elimination.
return false;
}
AppView<? extends AppInfoWithClassHierarchy> appViewWithClassHierarchy =
appView.withClassHierarchy();
SingleFieldResolutionResult<?> resolutionResult =
appViewWithClassHierarchy
.appInfo()
.resolveField(fieldGet.getField())
.asSingleFieldResolutionResult();
if (resolutionResult == null) {
// Not known to be final.
return false;
}
if (!resolutionResult.isSingleProgramFieldResolutionResult()) {
// We can't rely on the final flag of non-program fields.
return false;
}
ProgramField resolvedField = resolutionResult.getSingleProgramField();
FieldAccessFlags accessFlags = resolvedField.getAccessFlags();
assert !accessFlags.isVolatile();
// TODO(b/236661949): Add support for effectively final fields so that this also works well
// without -allowaccessmodification.
if (!accessFlags.isFinal()) {
return false;
}
if (appView.getKeepInfo(resolvedField).isPinned(appView.options())) {
// The final flag could be unset using reflection.
return false;
}
if (context.getDefinition().isInitializer()
&& context.getAccessFlags().isStatic() == fieldGet.isStaticGet()) {
if (context.getHolder() == resolvedField.getHolder()) {
// If this is an initializer on the field's holder, then bail out, since the field value is
// only known to be final after object/class creation.
return false;
}
if (fieldGet.isInstanceGet()
&& appViewWithClassHierarchy
.appInfo()
.isSubtype(context.getHolder(), resolvedField.getHolder())) {
// If an instance initializer reads a final instance field declared in a super class, we
// cannot hoist the read above the parent constructor call.
return false;
}
}
if (!resolutionResult.getInitialResolutionHolder().isResolvable(appView)) {
// If this field read is guarded by an API level check, hoisting of this field could lead to
// a ClassNotFoundException on some API levels.
return false;
}
return true;
}
private boolean isEffectivelyFinalField(StaticGet staticGet) {
AbstractValue abstractValue = staticGet.outValue().getAbstractValue(appView, context);
if (!abstractValue.isSingleFieldValue()) {
return false;
}
SingleFieldValue singleFieldValue = abstractValue.asSingleFieldValue();
DexType fieldHolderType = singleFieldValue.getField().getHolderType();
if (context.getDefinition().isClassInitializer()
&& context.getHolderType() == fieldHolderType) {
// Avoid that canonicalization inserts a read before the unique write in the class
// initializer, as that would change the program behavior.
return false;
}
DexClass fieldHolder = appView.definitionFor(fieldHolderType);
return singleFieldValue.getField().lookupOnClass(fieldHolder) != null;
}
private void insertCanonicalizedConstant(Instruction canonicalizedConstant) {
BasicBlock entryBlock = code.entryBlock();
// Insert the constant instruction at the start of the block right after the argument
// instructions. It is important that the const instruction is put before any instruction
// that can throw exceptions (since the value could be used on the exceptional edge).
InstructionListIterator it = entryBlock.listIterator(code);
while (it.hasNext()) {
Instruction next = it.next();
if (!next.isArgument()) {
canonicalizedConstant.setPosition(code.getEntryPosition());
it.previous();
break;
}
}
it.add(canonicalizedConstant);
}
private static boolean constantUsedByInvokeRange(Instruction constant) {
for (Instruction user : constant.outValue().uniqueUsers()) {
if (user.isInvoke() && user.asInvoke().requiredArgumentRegisters() > 5) {
return true;
}
}
return false;
}
}