blob: 05416df7178cc4f20e384b98ef4633ffb7f89359 [file] [log] [blame]
// Copyright (c) 2019, 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.analysis;
import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedMethod;
import com.android.tools.r8.graph.DexProgramClass;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.ir.code.ArrayPut;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InvokeDirect;
import com.android.tools.r8.ir.code.InvokeMethod;
import com.android.tools.r8.ir.code.InvokeNewArray;
import com.android.tools.r8.ir.code.NewArrayEmpty;
import com.android.tools.r8.ir.code.NewArrayFilledData;
import com.android.tools.r8.ir.code.NewInstance;
import com.android.tools.r8.ir.code.StaticGet;
import com.android.tools.r8.ir.code.StaticPut;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
import com.android.tools.r8.utils.ListUtils;
import com.android.tools.r8.utils.LongInterval;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* An analysis that is used to determine if a value may depend on the runtime environment. The
* primary use case of this analysis is to determine if a class initializer can safely be postponed:
*
* <p>If a class initializer does not have any other side effects than the field assignments to the
* static fields of the enclosing class, and all the values that are being stored into the static
* fields do not depend on the runtime environment (i.e., the heap, which can be accessed via
* static-get instructions), then the class initializer can safely be postponed.
*
* <p>All compile time constants are independent of the runtime environment. Furthermore, all newly
* instantiated arrays and objects are independent of the environment, as long as their content
* (i.e., array elements and field values) does not depend on the runtime environment.
*
* <p>Example: Values that are considered to be independent of the runtime environment:
*
* <ul>
* <li>{@code true}
* <li>{@code 42}
* <li>{@code "Hello world!"}
* <li>{@code new Object()}
* <li>{@code new MyClassWithTrivialInitializer("ABC")}
* <li>{@code new MyStandardEnum(42, "A")}
* <li>{@code new int[] {0, 1, 2}}
* <li>{@code new Object[] {new Object(), new MyClassWithTrivialInitializer()}}
* </ul>
*
* <p>Example: Values that are considered to be dependent on the runtime environment:
*
* <ul>
* <li>{@code argX} (for methods with arguments)
* <li>{@code OtherClass.STATIC_FIELD} (reads a value from the runtime environment)
* <li>{@code new MyClassWithTrivialInitializer(OtherClass.FIELD)}
* </ul>
*/
public class ValueMayDependOnEnvironmentAnalysis {
private final AppView<?> appView;
private final IRCode code;
private final DexType context;
private final Set<Value> knownNotToDependOnEnvironment = Sets.newIdentityHashSet();
private final Set<Value> visited = Sets.newIdentityHashSet();
// Lazily computed mapping from final field definitions of the enclosing class to the static-put
// instructions in the class initializer that assigns these final fields.
private Map<DexEncodedField, List<StaticPut>> finalFieldPuts;
public ValueMayDependOnEnvironmentAnalysis(AppView<?> appView, IRCode code) {
this.appView = appView;
this.code = code;
this.context = code.method.holder();
}
public boolean valueMayDependOnEnvironment(Value value) {
boolean result = valueMayDependOnEnvironment(value, Sets.newIdentityHashSet());
assert visited.isEmpty();
return result;
}
private boolean valueMayDependOnEnvironment(
Value value, Set<Value> assumedNotToDependOnEnvironment) {
Value root = value.getAliasedValue();
if (assumedNotToDependOnEnvironment.contains(root)) {
return false;
}
if (knownNotToDependOnEnvironment.contains(root)) {
return false;
}
if (!visited.add(root)) {
// Guard against cycle by conservatively returning true.
return true;
}
try {
if (root.isConstant()) {
return false;
}
if (isConstantArrayThroughoutMethod(root, assumedNotToDependOnEnvironment)) {
return false;
}
if (root.getAbstractValue(appView, context).isSingleEnumValue()) {
return false;
}
if (isNewInstanceWithoutEnvironmentDependentFields(root, assumedNotToDependOnEnvironment)) {
return false;
}
if (isAliasOfValueThatIsIndependentOfEnvironment(root, assumedNotToDependOnEnvironment)) {
return false;
}
return true;
} finally {
boolean changed = visited.remove(root);
assert changed;
}
}
private boolean valueMayNotDependOnEnvironmentAssumingArrayDoesNotDependOnEnvironment(
Value value, Value array, Set<Value> assumedNotToDependOnEnvironment) {
assert !value.hasAliasedValue();
assert !array.hasAliasedValue();
if (assumedNotToDependOnEnvironment.add(array)) {
boolean valueMayDependOnEnvironment =
valueMayDependOnEnvironment(value, assumedNotToDependOnEnvironment);
boolean changed = assumedNotToDependOnEnvironment.remove(array);
assert changed;
return !valueMayDependOnEnvironment;
}
return !valueMayDependOnEnvironment(value, assumedNotToDependOnEnvironment);
}
/**
* Used to identify if an array is "constant" in the sense that none of the values written into
* the array may depend on the environment.
*
* <p>Examples include {@code new int[] {1,2,3}} and {@code new Object[]{new Object()}}.
*/
public boolean isConstantArrayThroughoutMethod(Value value) {
boolean result = isConstantArrayThroughoutMethod(value, Sets.newIdentityHashSet());
assert visited.isEmpty();
return result;
}
private boolean isConstantArrayThroughoutMethod(
Value value, Set<Value> assumedNotToDependOnEnvironment) {
Value root = value.getAliasedValue();
if (root.isPhi()) {
// Would need to track the aliases, just give up.
return false;
}
Instruction definition = root.definition;
// Check that it is a constant array with a known size at this point in the IR.
long size;
if (definition.isInvokeNewArray()) {
InvokeNewArray invokeNewArray = definition.asInvokeNewArray();
for (Value argument : invokeNewArray.arguments()) {
if (!argument.isConstant()) {
return false;
}
}
size = invokeNewArray.arguments().size();
} else if (definition.isNewArrayEmpty()) {
NewArrayEmpty newArrayEmpty = definition.asNewArrayEmpty();
Value sizeValue = newArrayEmpty.size().getAliasedValue();
if (!sizeValue.hasValueRange()) {
return false;
}
LongInterval sizeRange = sizeValue.getValueRange();
if (!sizeRange.isSingleValue()) {
return false;
}
size = sizeRange.getSingleValue();
} else {
// Some other array creation.
return false;
}
if (size < 0) {
// Check for NegativeArraySizeException.
return false;
}
if (size == 0) {
// Empty arrays are always constant.
return true;
}
// Allow array-put and new-array-filled-data instructions that immediately follow the array
// creation.
Set<Value> arrayValues = Sets.newIdentityHashSet();
Set<Instruction> consumedInstructions = Sets.newIdentityHashSet();
for (Instruction instruction : definition.getBlock().instructionsAfter(definition)) {
if (instruction.isArrayPut()) {
ArrayPut arrayPut = instruction.asArrayPut();
Value array = arrayPut.array().getAliasedValue();
if (array != root) {
// This ends the chain of array-put instructions that are allowed immediately after the
// array creation.
break;
}
LongInterval indexRange = arrayPut.index().getValueRange();
if (!indexRange.isSingleValue()) {
return false;
}
long index = indexRange.getSingleValue();
if (index < 0 || index >= size) {
return false;
}
// Check if the value being written into the array may depend on the environment.
//
// When analyzing if the value may depend on the environment, we assume that the current
// array does not depend on the environment. Otherwise, we would classify the value as
// possibly depending on the environment since it could escape via the array and then
// be mutated indirectly.
Value rhs = arrayPut.value().getAliasedValue();
if (!valueMayNotDependOnEnvironmentAssumingArrayDoesNotDependOnEnvironment(
rhs, root, assumedNotToDependOnEnvironment)) {
return false;
}
arrayValues.add(rhs);
consumedInstructions.add(arrayPut);
continue;
}
if (instruction.isNewArrayFilledData()) {
NewArrayFilledData newArrayFilledData = instruction.asNewArrayFilledData();
Value array = newArrayFilledData.src();
if (array != root) {
break;
}
consumedInstructions.add(newArrayFilledData);
continue;
}
if (instruction.instructionMayHaveSideEffects(appView, context)) {
// This ends the chain of array-put instructions that are allowed immediately after the
// array creation.
break;
}
}
// Check that the array is not mutated before the end of this method.
//
// Currently, we only allow the array to flow into static-put instructions that are not
// followed by an instruction that may have side effects. Instructions that do not have any
// side effects are ignored because they cannot mutate the array.
if (valueMayBeMutatedBeforeMethodExit(
root, assumedNotToDependOnEnvironment, consumedInstructions)) {
return false;
}
if (assumedNotToDependOnEnvironment.isEmpty()) {
knownNotToDependOnEnvironment.add(root);
knownNotToDependOnEnvironment.addAll(arrayValues);
}
return true;
}
private boolean isNewInstanceWithoutEnvironmentDependentFields(
Value value, Set<Value> assumedNotToDependOnEnvironment) {
assert !value.hasAliasedValue();
if (value.isPhi() || !value.definition.isNewInstance()) {
return false;
}
NewInstance newInstance = value.definition.asNewInstance();
DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(newInstance.clazz));
if (clazz == null) {
return false;
}
// Find the single constructor invocation.
InvokeMethod constructorInvoke = null;
for (Instruction instruction : value.uniqueUsers()) {
if (!instruction.isInvokeDirect()) {
continue;
}
InvokeDirect invoke = instruction.asInvokeDirect();
if (!appView.dexItemFactory().isConstructor(invoke.getInvokedMethod())) {
continue;
}
if (invoke.getReceiver().getAliasedValue() != value) {
continue;
}
if (constructorInvoke == null) {
constructorInvoke = invoke;
} else {
// Not a single constructor invocation, give up.
return false;
}
}
if (constructorInvoke == null) {
// Didn't find a constructor invocation, give up.
return false;
}
// Check that it is a trivial initializer (otherwise, the constructor could do anything).
DexEncodedMethod constructor = appView.definitionFor(constructorInvoke.getInvokedMethod());
if (constructor == null) {
return false;
}
if (clazz.hasInstanceFieldsDirectlyOrIndirectly(appView)) {
InstanceInitializerInfo initializerInfo =
constructor.getOptimizationInfo().getInstanceInitializerInfo();
if (initializerInfo.instanceFieldInitializationMayDependOnEnvironment()) {
return false;
}
// Check that none of the arguments to the constructor depend on the environment.
for (int i = 1; i < constructorInvoke.arguments().size(); i++) {
Value argument = constructorInvoke.arguments().get(i);
if (valueMayDependOnEnvironment(argument, assumedNotToDependOnEnvironment)) {
return false;
}
}
// Finally, check that the object does not escape.
if (valueMayBeMutatedBeforeMethodExit(
value, assumedNotToDependOnEnvironment, ImmutableSet.of(constructorInvoke))) {
return false;
}
}
if (assumedNotToDependOnEnvironment.isEmpty()) {
knownNotToDependOnEnvironment.add(value);
}
return true;
}
private boolean valueMayBeMutatedBeforeMethodExit(
Value value, Set<Value> assumedNotToDependOnEnvironment, Set<Instruction> whitelist) {
assert !value.hasAliasedValue();
if (value.numberOfPhiUsers() > 0) {
// Could be mutated indirectly.
return true;
}
Set<Instruction> visited = Sets.newIdentityHashSet();
for (Instruction user : value.uniqueUsers()) {
if (whitelist.contains(user)) {
continue;
}
if (user.isArrayPut()) {
ArrayPut arrayPut = user.asArrayPut();
if (value == arrayPut.value()
&& !valueMayDependOnEnvironment(arrayPut.array(), assumedNotToDependOnEnvironment)) {
continue;
}
}
if (user.isStaticPut()) {
StaticPut staticPut = user.asStaticPut();
if (visited.contains(staticPut)) {
// Already visited previously.
continue;
}
for (Instruction instruction : code.getInstructionsReachableFrom(staticPut)) {
if (!visited.add(instruction)) {
// Already visited previously.
continue;
}
if (instruction.isStaticPut()) {
StaticPut otherStaticPut = instruction.asStaticPut();
if (otherStaticPut.getField().holder == staticPut.getField().holder
&& instruction.instructionInstanceCanThrow(appView, context).cannotThrow()) {
continue;
}
return true;
}
if (instruction.instructionMayTriggerMethodInvocation(appView, context)
&& instruction.instructionMayHaveSideEffects(appView, context)) {
return true;
}
}
continue;
}
// Other user than array-put or static-put, just give up.
return false;
}
return false;
}
private boolean isAliasOfValueThatIsIndependentOfEnvironment(
Value value, Set<Value> assumedNotToDependOnEnvironment) {
// If we are inside a class initializer, and we are reading a final field of the enclosing
// class, then check if there is a single write to that field in the class initializer, and that
// the value being written into that field does not depend on the environment.
//
// The reason why we do not currently treat final fields that are written in multiple places is
// that the value of the field could then be dependent on the environment due to the control
// flow.
if (code.method.isClassInitializer()) {
assert !value.hasAliasedValue();
if (value.isPhi()) {
return false;
}
Instruction definition = value.definition;
if (definition.isStaticGet()) {
StaticGet staticGet = definition.asStaticGet();
DexEncodedField field = appView.appInfo().resolveField(staticGet.getField());
if (field != null && field.holder() == context) {
List<StaticPut> finalFieldPuts = computeFinalFieldPuts().get(field);
if (finalFieldPuts == null || finalFieldPuts.size() != 1) {
return false;
}
StaticPut staticPut = ListUtils.first(finalFieldPuts);
return !valueMayDependOnEnvironment(staticPut.value(), assumedNotToDependOnEnvironment);
}
}
}
return false;
}
private Map<DexEncodedField, List<StaticPut>> computeFinalFieldPuts() {
assert code.method.isClassInitializer();
if (finalFieldPuts == null) {
finalFieldPuts = new IdentityHashMap<>();
for (StaticPut staticPut : code.<StaticPut>instructions(Instruction::isStaticPut)) {
DexEncodedField field = appView.appInfo().resolveField(staticPut.getField());
if (field != null && field.holder() == context && field.isFinal()) {
finalFieldPuts.computeIfAbsent(field, ignore -> new ArrayList<>()).add(staticPut);
}
}
}
return finalFieldPuts;
}
}