| // 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; |
| } |
| } |