| // 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.DexClass; |
| import com.android.tools.r8.graph.DexEncodedField; |
| import com.android.tools.r8.graph.DexEncodedMethod; |
| import com.android.tools.r8.graph.DexField; |
| import com.android.tools.r8.graph.DexProgramClass; |
| import com.android.tools.r8.graph.DexType; |
| import com.android.tools.r8.graph.ProgramMethod; |
| import com.android.tools.r8.ir.analysis.environmentdependence.ValueGraph; |
| import com.android.tools.r8.ir.analysis.environmentdependence.ValueGraph.Node; |
| import com.android.tools.r8.ir.analysis.value.AbstractValue; |
| import com.android.tools.r8.ir.code.ArrayPut; |
| import com.android.tools.r8.ir.code.BasicBlock; |
| 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.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.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.SetUtils; |
| import com.android.tools.r8.utils.WorkList; |
| import com.google.common.collect.Sets; |
| import java.util.ArrayDeque; |
| import java.util.Deque; |
| 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 ProgramMethod context; |
| |
| public ValueMayDependOnEnvironmentAnalysis(AppView<?> appView, IRCode code) { |
| this.appView = appView; |
| this.context = code.context(); |
| } |
| |
| public boolean anyValueMayDependOnEnvironment(Iterable<Value> values) { |
| ValueGraph graph = new ValueGraph(); |
| Set<Instruction> consumedInstructions = Sets.newIdentityHashSet(); |
| Set<Value> mutableValues = Sets.newIdentityHashSet(); |
| WorkList<Value> worklist = WorkList.newIdentityWorkList(values); |
| while (worklist.hasNext()) { |
| Value value = worklist.next(); |
| Value root = value.getAliasedValue(); |
| Node node = graph.createNodeIfAbsent(root); |
| if (root != value) { |
| // An alias depends on the environment if the aliased value depends on the environment, thus |
| // an edge is added from the alias to the aliased value. |
| graph.addDirectedEdge(graph.createNodeIfAbsent(value), node); |
| } |
| if (!addValueToValueGraph(root, node, graph, consumedInstructions, mutableValues, worklist)) { |
| return true; |
| } |
| } |
| |
| // At this point, the graph has been populated with a node for each value of interest, and edges |
| // have been added to reflect the dependency. We now attempt to prove that no values depend on |
| // environment, starting from the leaves of the graph. |
| // |
| // First we collapse strongly connected components in the graph. By doing so we will attempt to |
| // prove that all values in a strongly connected component are independent of the environment at |
| // once. |
| graph.mergeStronglyConnectedComponents(); |
| |
| Set<Node> nodesDependentOnEnvironment = SetUtils.newIdentityHashSet(graph.getNodes()); |
| while (!nodesDependentOnEnvironment.isEmpty()) { |
| Set<Node> newNodesIndependentOfEnvironment = Sets.newIdentityHashSet(); |
| for (Node node : nodesDependentOnEnvironment) { |
| boolean isDependentOfEnvironment = |
| node.hasSuccessorThatMatches( |
| successor -> |
| nodesDependentOnEnvironment.contains(successor) |
| && !newNodesIndependentOfEnvironment.contains(successor)); |
| if (!isDependentOfEnvironment) { |
| newNodesIndependentOfEnvironment.add(node); |
| } |
| } |
| if (newNodesIndependentOfEnvironment.isEmpty()) { |
| return true; |
| } |
| nodesDependentOnEnvironment.removeAll(newNodesIndependentOfEnvironment); |
| } |
| |
| // At this point, we have proved that all values in the graph are independent on the |
| // environment. However, we still need to prove that they are not mutated between the point |
| // where they are defined and all normal exits. |
| return anyValueMayBeMutatedBeforeMethodExit(mutableValues, consumedInstructions); |
| } |
| |
| private boolean addValueToValueGraph( |
| Value value, |
| Node node, |
| ValueGraph graph, |
| Set<Instruction> consumedInstructions, |
| Set<Value> mutableValues, |
| WorkList<Value> worklist) { |
| return addConstantValueToValueGraph(value) |
| || addArrayValueToValueGraph( |
| value, node, graph, consumedInstructions, mutableValues, worklist) |
| || addNewInstanceValueToValueGraph( |
| value, node, graph, consumedInstructions, mutableValues, worklist); |
| } |
| |
| private boolean addConstantValueToValueGraph(Value value) { |
| // Constants do not depend on any other values, thus no edges are added to the graph. |
| if (value.isConstant()) { |
| return true; |
| } |
| assert !value.getAliasedValue().isConstant(); |
| AbstractValue abstractValue = value.getAbstractValue(appView, context); |
| if (abstractValue.isSingleConstValue()) { |
| return true; |
| } |
| if (abstractValue.isSingleFieldValue()) { |
| DexField fieldReference = abstractValue.asSingleFieldValue().getField(); |
| DexClass holder = appView.definitionForHolder(fieldReference); |
| DexEncodedField field = fieldReference.lookupOnClass(holder); |
| if (field != null && field.isEnum()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private boolean addArrayValueToValueGraph( |
| Value value, |
| Node node, |
| ValueGraph graph, |
| Set<Instruction> consumedInstructions, |
| Set<Value> mutableValues, |
| WorkList<Value> worklist) { |
| if (value.isPhi()) { |
| // Would need to track the aliases, just give up. |
| return false; |
| } |
| |
| Instruction definition = value.definition; |
| |
| // Check that it is a constant array with a known size at this point in the IR. |
| if (definition.isInvokeNewArray()) { |
| InvokeNewArray invokeNewArray = definition.asInvokeNewArray(); |
| for (Value argument : invokeNewArray.arguments()) { |
| graph.addDirectedEdge(node, graph.createNodeIfAbsent(argument)); |
| worklist.addIfNotSeen(argument); |
| } |
| } else if (definition.isNewArrayEmpty()) { |
| NewArrayEmpty newArrayEmpty = definition.asNewArrayEmpty(); |
| Value sizeValue = newArrayEmpty.size(); |
| graph.addDirectedEdge(node, graph.createNodeIfAbsent(sizeValue)); |
| worklist.addIfNotSeen(sizeValue); |
| } else { |
| // Some other array creation. |
| return false; |
| } |
| |
| // Allow array-put and new-array-filled-data instructions that immediately follow the array |
| // creation. |
| for (Instruction instruction : definition.getBlock().instructionsAfter(definition)) { |
| if (instruction.isArrayPut()) { |
| ArrayPut arrayPut = instruction.asArrayPut(); |
| Value array = arrayPut.array().getAliasedValue(); |
| if (array != value) { |
| // This ends the chain of array-put instructions that are allowed immediately after the |
| // array creation. |
| break; |
| } |
| graph.addDirectedEdge(node, graph.createNodeIfAbsent(arrayPut.index())); |
| worklist.addIfNotSeen(arrayPut.index()); |
| graph.addDirectedEdge(node, graph.createNodeIfAbsent(arrayPut.value())); |
| worklist.addIfNotSeen(arrayPut.value()); |
| } else if (instruction.isNewArrayFilledData()) { |
| NewArrayFilledData newArrayFilledData = instruction.asNewArrayFilledData(); |
| Value array = newArrayFilledData.src(); |
| if (array != value) { |
| // This ends the chain of array-put instructions that are allowed immediately after the |
| // array creation. |
| break; |
| } |
| consumedInstructions.add(instruction); |
| } else if (instruction.instructionMayHaveSideEffects(appView, context)) { |
| // This ends the chain of array-put instructions that are allowed immediately after the |
| // array creation. |
| break; |
| } |
| consumedInstructions.add(instruction); |
| } |
| mutableValues.add(value); |
| return true; |
| } |
| |
| private boolean addNewInstanceValueToValueGraph( |
| Value value, |
| Node node, |
| ValueGraph graph, |
| Set<Instruction> consumedInstructions, |
| Set<Value> mutableValues, |
| WorkList<Value> worklist) { |
| if (!value.isDefinedByInstructionSatisfying(Instruction::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. |
| InvokeDirect constructorInvoke = |
| newInstance.getUniqueConstructorInvoke(appView.dexItemFactory()); |
| if (constructorInvoke == null || constructorInvoke.getInvokedMethod().holder != clazz.type) { |
| // Didn't find a (valid) constructor invocation, give up. |
| return false; |
| } |
| |
| // Check that it is a trivial initializer (otherwise, the constructor could do anything). |
| DexEncodedMethod constructor = clazz.lookupMethod(constructorInvoke.getInvokedMethod()); |
| if (constructor == null) { |
| return false; |
| } |
| |
| InstanceInitializerInfo initializerInfo = |
| constructor.getOptimizationInfo().getInstanceInitializerInfo(constructorInvoke); |
| |
| List<DexEncodedField> fields = clazz.getDirectAndIndirectInstanceFields(appView); |
| if (!fields.isEmpty()) { |
| 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.getArgument(i); |
| graph.addDirectedEdge(node, graph.createNodeIfAbsent(argument)); |
| worklist.addIfNotSeen(argument); |
| } |
| |
| // Mark this value as mutable if it has a non-final field. |
| boolean hasNonFinalField = false; |
| for (DexEncodedField field : fields) { |
| if (!field.isFinal()) { |
| hasNonFinalField = true; |
| break; |
| } |
| } |
| if (hasNonFinalField) { |
| mutableValues.add(value); |
| } |
| } |
| |
| if (!initializerInfo.mayHaveOtherSideEffectsThanInstanceFieldAssignments()) { |
| consumedInstructions.add(constructorInvoke); |
| } |
| |
| return true; |
| } |
| |
| private boolean anyValueMayBeMutatedBeforeMethodExit( |
| Set<Value> values, Set<Instruction> whitelist) { |
| Set<BasicBlock> initialBlocks = Sets.newIdentityHashSet(); |
| for (Value value : values) { |
| assert !value.isPhi(); |
| initialBlocks.add(value.definition.getBlock()); |
| } |
| Map<BasicBlock, TrackedValuesState> blockExitStates = new IdentityHashMap<>(); |
| Deque<BasicBlock> worklist = new ArrayDeque<>(initialBlocks); |
| while (!worklist.isEmpty()) { |
| BasicBlock block = worklist.removeFirst(); |
| TrackedValuesState state = computeBlockEntryState(block, blockExitStates); |
| boolean changed = false; |
| for (Instruction instruction : block.getInstructions()) { |
| if (whitelist.contains(instruction)) { |
| continue; |
| } |
| if (instruction.isStaticPut()) { |
| StaticPut staticPut = instruction.asStaticPut(); |
| if (state.isTrackingValue(staticPut.value())) { |
| changed |= state.recordTrackedValueHasEscaped(); |
| } |
| if (state.hasTrackedValueEscaped()) { |
| DexType holder = staticPut.getField().holder; |
| if (holder.classInitializationMayHaveSideEffectsInContext(appView, context)) { |
| return true; |
| } |
| } |
| continue; |
| } |
| if (instruction.instructionMayTriggerMethodInvocation(appView, context)) { |
| if (instruction.hasInValueThatMatches(state::isTrackingValue)) { |
| changed |= state.recordTrackedValueHasEscaped(); |
| } |
| if (state.hasTrackedValueEscaped() |
| && instruction.instructionMayHaveSideEffects(appView, context)) { |
| return true; |
| } |
| } |
| if (instruction.hasOutValue() && values.contains(instruction.outValue())) { |
| changed |= state.startTrackingValue(instruction.outValue()); |
| } |
| } |
| blockExitStates.put(block, state); |
| if (changed) { |
| worklist.addAll(block.getSuccessors()); |
| } |
| } |
| return false; |
| } |
| |
| private TrackedValuesState computeBlockEntryState( |
| BasicBlock block, Map<BasicBlock, TrackedValuesState> states) { |
| TrackedValuesState state = new TrackedValuesState(); |
| for (BasicBlock predecessor : block.getPredecessors()) { |
| state.add(states.getOrDefault(predecessor, TrackedValuesState.empty())); |
| } |
| return state; |
| } |
| |
| static class TrackedValuesState { |
| |
| private static final TrackedValuesState EMPTY = new TrackedValuesState(); |
| |
| boolean hasTrackedValueEscaped; |
| Set<Value> trackedValues = Sets.newIdentityHashSet(); |
| |
| public static TrackedValuesState empty() { |
| return EMPTY; |
| } |
| |
| public void add(TrackedValuesState state) { |
| hasTrackedValueEscaped |= state.hasTrackedValueEscaped; |
| trackedValues.addAll(state.trackedValues); |
| } |
| |
| public boolean hasTrackedValueEscaped() { |
| return hasTrackedValueEscaped; |
| } |
| |
| public boolean isTrackingValue(Value value) { |
| return trackedValues.contains(value); |
| } |
| |
| public boolean recordTrackedValueHasEscaped() { |
| if (hasTrackedValueEscaped) { |
| return false; |
| } |
| hasTrackedValueEscaped = true; |
| return true; |
| } |
| |
| public boolean startTrackingValue(Value value) { |
| return trackedValues.add(value); |
| } |
| } |
| } |