| // 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.conversion; |
| |
| import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull; |
| |
| import com.android.tools.r8.errors.CompilationError; |
| import com.android.tools.r8.graph.AppView; |
| import com.android.tools.r8.graph.DexCallSite; |
| import com.android.tools.r8.graph.DexClass; |
| import com.android.tools.r8.graph.DexClassAndMethod; |
| import com.android.tools.r8.graph.DexEncodedMethod; |
| import com.android.tools.r8.graph.DexField; |
| 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.FieldAccessInfo; |
| import com.android.tools.r8.graph.FieldAccessInfoCollection; |
| import com.android.tools.r8.graph.GraphLens.MethodLookupResult; |
| import com.android.tools.r8.graph.LookupResult; |
| import com.android.tools.r8.graph.ProgramField; |
| import com.android.tools.r8.graph.ProgramMethod; |
| import com.android.tools.r8.graph.ResolutionResult; |
| import com.android.tools.r8.graph.UseRegistry; |
| import com.android.tools.r8.ir.code.Invoke; |
| import com.android.tools.r8.ir.conversion.CallGraph.Node; |
| import com.android.tools.r8.ir.conversion.CallGraphBuilderBase.CycleEliminator.CycleEliminationResult; |
| import com.android.tools.r8.logging.Log; |
| import com.android.tools.r8.shaking.AppInfoWithLiveness; |
| import com.android.tools.r8.utils.Timing; |
| import com.android.tools.r8.utils.collections.ProgramMethodSet; |
| import com.google.common.collect.Iterators; |
| import com.google.common.collect.Sets; |
| import java.util.ArrayDeque; |
| import java.util.Collection; |
| import java.util.Deque; |
| import java.util.IdentityHashMap; |
| import java.util.Iterator; |
| import java.util.LinkedHashSet; |
| import java.util.LinkedList; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.ExecutorService; |
| import java.util.function.BiConsumer; |
| import java.util.function.Consumer; |
| import java.util.function.Predicate; |
| |
| abstract class CallGraphBuilderBase { |
| |
| final AppView<AppInfoWithLiveness> appView; |
| private final FieldAccessInfoCollection<?> fieldAccessInfoCollection; |
| final Map<DexMethod, Node> nodes = new IdentityHashMap<>(); |
| private final Map<DexMethod, ProgramMethodSet> possibleProgramTargetsCache = |
| new ConcurrentHashMap<>(); |
| |
| CallGraphBuilderBase(AppView<AppInfoWithLiveness> appView) { |
| this.appView = appView; |
| this.fieldAccessInfoCollection = appView.appInfo().getFieldAccessInfoCollection(); |
| } |
| |
| public CallGraph build(ExecutorService executorService, Timing timing) throws ExecutionException { |
| timing.begin("Build IR processing order constraints"); |
| timing.begin("Build call graph"); |
| populateGraph(executorService); |
| assert verifyNoRedundantFieldReadEdges(); |
| timing.end(); |
| assert verifyAllMethodsWithCodeExists(); |
| |
| appView.withGeneratedMessageLiteBuilderShrinker( |
| shrinker -> shrinker.preprocessCallGraphBeforeCycleElimination(nodes)); |
| |
| timing.begin("Cycle elimination"); |
| // Sort the nodes for deterministic cycle elimination. |
| Set<Node> nodesWithDeterministicOrder = Sets.newTreeSet(nodes.values()); |
| CycleEliminator cycleEliminator = new CycleEliminator(); |
| CycleEliminationResult cycleEliminationResult = |
| cycleEliminator.breakCycles(nodesWithDeterministicOrder); |
| timing.end(); |
| timing.end(); |
| assert cycleEliminator.breakCycles(nodesWithDeterministicOrder).numberOfRemovedCallEdges() |
| == 0; // The cycles should be gone. |
| |
| return new CallGraph(nodesWithDeterministicOrder, cycleEliminationResult); |
| } |
| |
| abstract void populateGraph(ExecutorService executorService) throws ExecutionException; |
| |
| /** Verify that there are no field read edges in the graph if there is also a call graph edge. */ |
| private boolean verifyNoRedundantFieldReadEdges() { |
| for (Node writer : nodes.values()) { |
| for (Node reader : writer.getReadersWithDeterministicOrder()) { |
| assert !writer.hasCaller(reader); |
| } |
| } |
| return true; |
| } |
| |
| Node getOrCreateNode(ProgramMethod method) { |
| synchronized (nodes) { |
| return nodes.computeIfAbsent(method.getReference(), ignore -> new Node(method)); |
| } |
| } |
| |
| abstract boolean verifyAllMethodsWithCodeExists(); |
| |
| class InvokeExtractor extends UseRegistry { |
| |
| private final Node currentMethod; |
| private final Predicate<ProgramMethod> targetTester; |
| |
| InvokeExtractor(Node currentMethod, Predicate<ProgramMethod> targetTester) { |
| super(appView.dexItemFactory()); |
| this.currentMethod = currentMethod; |
| this.targetTester = targetTester; |
| } |
| |
| private void addClassInitializerTarget(DexProgramClass clazz) { |
| assert clazz != null; |
| if (clazz.hasClassInitializer()) { |
| addCallEdge(clazz.getProgramClassInitializer(), false); |
| } |
| } |
| |
| private void addClassInitializerTarget(DexType type) { |
| assert type.isClassType(); |
| DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(type)); |
| if (clazz != null) { |
| addClassInitializerTarget(clazz); |
| } |
| } |
| |
| private void addCallEdge(ProgramMethod callee, boolean likelySpuriousCallEdge) { |
| if (!targetTester.test(callee)) { |
| return; |
| } |
| if (callee.getDefinition().isAbstract()) { |
| // Not a valid target. |
| return; |
| } |
| if (callee.getDefinition().isNative()) { |
| // We don't care about calls to native methods. |
| return; |
| } |
| if (appView.appInfo().isPinned(callee.getReference())) { |
| // Since the callee is kept, we cannot inline it into the caller, and we also cannot collect |
| // any optimization info for the method. Therefore, we drop the call edge to reduce the |
| // total number of call graph edges, which should lead to fewer call graph cycles. |
| return; |
| } |
| getOrCreateNode(callee).addCallerConcurrently(currentMethod, likelySpuriousCallEdge); |
| } |
| |
| private void addFieldReadEdge(DexEncodedMethod writer) { |
| addFieldReadEdge(writer.asProgramMethod(appView)); |
| } |
| |
| private void addFieldReadEdge(ProgramMethod writer) { |
| assert !writer.getDefinition().isAbstract(); |
| if (!targetTester.test(writer)) { |
| return; |
| } |
| getOrCreateNode(writer).addReaderConcurrently(currentMethod); |
| } |
| |
| private void processInvoke(Invoke.Type originalType, DexMethod originalMethod) { |
| ProgramMethod context = currentMethod.getProgramMethod(); |
| MethodLookupResult result = |
| appView.graphLens().lookupMethod(originalMethod, context.getReference(), originalType); |
| DexMethod method = result.getReference(); |
| Invoke.Type type = result.getType(); |
| if (type == Invoke.Type.INTERFACE || type == Invoke.Type.VIRTUAL) { |
| // For virtual and interface calls add all potential targets that could be called. |
| ResolutionResult resolutionResult = |
| appView.appInfo().resolveMethod(method, type == Invoke.Type.INTERFACE); |
| DexEncodedMethod target = resolutionResult.getSingleTarget(); |
| if (target != null) { |
| processInvokeWithDynamicDispatch(type, target, context); |
| } |
| } else { |
| ProgramMethod singleTarget = |
| appView.appInfo().lookupSingleProgramTarget(type, method, context, appView); |
| if (singleTarget != null) { |
| assert !context.getDefinition().isBridge() |
| || singleTarget.getDefinition() != context.getDefinition(); |
| // For static invokes, the class could be initialized. |
| if (type.isStatic()) { |
| addClassInitializerTarget(singleTarget.getHolder()); |
| } |
| addCallEdge(singleTarget, false); |
| } |
| } |
| } |
| |
| private void processInvokeWithDynamicDispatch( |
| Invoke.Type type, DexEncodedMethod encodedTarget, ProgramMethod context) { |
| DexMethod target = encodedTarget.getReference(); |
| DexClass clazz = appView.definitionFor(target.holder); |
| if (clazz == null) { |
| assert false : "Unable to lookup holder of `" + target.toSourceString() + "`"; |
| return; |
| } |
| |
| if (!appView.options().testing.addCallEdgesForLibraryInvokes) { |
| if (clazz.isLibraryClass()) { |
| // Likely to have many possible targets. |
| return; |
| } |
| } |
| |
| boolean isInterface = type == Invoke.Type.INTERFACE; |
| ProgramMethodSet possibleProgramTargets = |
| possibleProgramTargetsCache.computeIfAbsent( |
| target, |
| method -> { |
| ResolutionResult resolution = appView.appInfo().resolveMethod(method, isInterface); |
| if (resolution.isVirtualTarget()) { |
| LookupResult lookupResult = |
| resolution.lookupVirtualDispatchTargets( |
| context.getHolder(), appView.appInfo()); |
| if (lookupResult.isLookupResultSuccess()) { |
| ProgramMethodSet targets = ProgramMethodSet.create(); |
| lookupResult |
| .asLookupResultSuccess() |
| .forEach( |
| methodTarget -> { |
| if (methodTarget.isProgramMethod()) { |
| targets.add(methodTarget.asProgramMethod()); |
| } |
| }, |
| lambdaTarget -> { |
| // The call target will ultimately be the implementation method. |
| DexClassAndMethod implementationMethod = |
| lambdaTarget.getImplementationMethod(); |
| if (implementationMethod.isProgramMethod()) { |
| targets.add(implementationMethod.asProgramMethod()); |
| } |
| }); |
| return targets; |
| } |
| } |
| return null; |
| }); |
| if (possibleProgramTargets != null) { |
| boolean likelySpuriousCallEdge = |
| possibleProgramTargets.size() |
| >= appView.options().callGraphLikelySpuriousCallEdgeThreshold; |
| for (ProgramMethod possibleTarget : possibleProgramTargets) { |
| addCallEdge(possibleTarget, likelySpuriousCallEdge); |
| } |
| } |
| } |
| |
| private void processFieldRead(DexField reference) { |
| if (!reference.holder.isClassType()) { |
| return; |
| } |
| |
| ProgramField field = appView.appInfo().resolveField(reference).getProgramField(); |
| if (field == null || appView.appInfo().isPinned(field)) { |
| return; |
| } |
| |
| // Each static field access implicitly triggers the class initializer. |
| if (field.getAccessFlags().isStatic()) { |
| addClassInitializerTarget(field.getHolder()); |
| } |
| |
| FieldAccessInfo fieldAccessInfo = fieldAccessInfoCollection.get(field.getReference()); |
| if (fieldAccessInfo != null && fieldAccessInfo.hasKnownWriteContexts()) { |
| if (fieldAccessInfo.getNumberOfWriteContexts() == 1) { |
| fieldAccessInfo.forEachWriteContext(this::addFieldReadEdge); |
| } |
| } |
| } |
| |
| private void processFieldWrite(DexField reference) { |
| if (reference.getHolderType().isClassType()) { |
| ProgramField field = appView.appInfo().resolveField(reference).getProgramField(); |
| if (field != null && field.getAccessFlags().isStatic()) { |
| // Each static field access implicitly triggers the class initializer. |
| addClassInitializerTarget(field.getHolder()); |
| } |
| } |
| } |
| |
| private void processInitClass(DexType type) { |
| DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(type)); |
| if (clazz == null) { |
| assert false; |
| return; |
| } |
| addClassInitializerTarget(clazz); |
| } |
| |
| @Override |
| public void registerInitClass(DexType clazz) { |
| processInitClass(clazz); |
| } |
| |
| @Override |
| public void registerInvokeVirtual(DexMethod method) { |
| processInvoke(Invoke.Type.VIRTUAL, method); |
| } |
| |
| @Override |
| public void registerInvokeDirect(DexMethod method) { |
| processInvoke(Invoke.Type.DIRECT, method); |
| } |
| |
| @Override |
| public void registerInvokeStatic(DexMethod method) { |
| processInvoke(Invoke.Type.STATIC, method); |
| } |
| |
| @Override |
| public void registerInvokeInterface(DexMethod method) { |
| processInvoke(Invoke.Type.INTERFACE, method); |
| } |
| |
| @Override |
| public void registerInvokeSuper(DexMethod method) { |
| processInvoke(Invoke.Type.SUPER, method); |
| } |
| |
| @Override |
| public void registerInstanceFieldRead(DexField field) { |
| processFieldRead(field); |
| } |
| |
| @Override |
| public void registerInstanceFieldWrite(DexField field) { |
| processFieldWrite(field); |
| } |
| |
| @Override |
| public void registerNewInstance(DexType type) { |
| if (type.isClassType()) { |
| addClassInitializerTarget(type); |
| } |
| } |
| |
| @Override |
| public void registerStaticFieldRead(DexField field) { |
| processFieldRead(field); |
| } |
| |
| @Override |
| public void registerStaticFieldWrite(DexField field) { |
| processFieldWrite(field); |
| } |
| |
| @Override |
| public void registerTypeReference(DexType type) {} |
| |
| @Override |
| public void registerInstanceOf(DexType type) {} |
| |
| @Override |
| public void registerCallSite(DexCallSite callSite) { |
| registerMethodHandle( |
| callSite.bootstrapMethod, MethodHandleUse.NOT_ARGUMENT_TO_LAMBDA_METAFACTORY); |
| } |
| } |
| |
| static class CycleEliminator { |
| |
| static final String CYCLIC_FORCE_INLINING_MESSAGE = |
| "Unable to satisfy force inlining constraints due to cyclic force inlining"; |
| |
| private static class CallEdge { |
| |
| private final Node caller; |
| private final Node callee; |
| |
| CallEdge(Node caller, Node callee) { |
| this.caller = caller; |
| this.callee = callee; |
| } |
| } |
| |
| static class StackEntryInfo { |
| |
| final int index; |
| final Node predecessor; |
| |
| boolean processed; |
| |
| StackEntryInfo(int index, Node predecessor) { |
| this.index = index; |
| this.predecessor = predecessor; |
| } |
| } |
| |
| static class CycleEliminationResult { |
| |
| private Map<DexEncodedMethod, ProgramMethodSet> removedCallEdges; |
| |
| CycleEliminationResult(Map<DexEncodedMethod, ProgramMethodSet> removedCallEdges) { |
| this.removedCallEdges = removedCallEdges; |
| } |
| |
| void forEachRemovedCaller(ProgramMethod callee, Consumer<ProgramMethod> fn) { |
| removedCallEdges.getOrDefault(callee.getDefinition(), ProgramMethodSet.empty()).forEach(fn); |
| } |
| |
| int numberOfRemovedCallEdges() { |
| int numberOfRemovedCallEdges = 0; |
| for (ProgramMethodSet nodes : removedCallEdges.values()) { |
| numberOfRemovedCallEdges += nodes.size(); |
| } |
| return numberOfRemovedCallEdges; |
| } |
| } |
| |
| // DFS stack. |
| private Deque<Node> stack = new ArrayDeque<>(); |
| |
| // Nodes on the DFS stack. |
| private Map<Node, StackEntryInfo> stackEntryInfo = new IdentityHashMap<>(); |
| |
| // Subset of the DFS stack, where the nodes on the stack are class initializers. |
| // |
| // This stack is used to efficiently compute if there is a class initializer on the stack. |
| private Deque<Node> clinitStack = new ArrayDeque<>(); |
| |
| // Subset of the DFS stack, where the nodes on the stack satisfy that the edge from the |
| // predecessor to the node itself is a field read edge. |
| // |
| // This stack is used to efficiently compute if there is a field read edge inside a cycle when |
| // a cycle is found. |
| private Deque<Node> writerStack = new ArrayDeque<>(); |
| |
| // Set of nodes that have been visited entirely. |
| private Set<Node> marked = Sets.newIdentityHashSet(); |
| |
| // Call edges that should be removed when the caller has been processed. These are not removed |
| // directly since that would lead to ConcurrentModificationExceptions. |
| private Map<Node, Set<Node>> calleesToBeRemoved = new IdentityHashMap<>(); |
| |
| // Field read edges that should be removed when the reader has been processed. These are not |
| // removed directly since that would lead to ConcurrentModificationExceptions. |
| private Map<Node, Set<Node>> writersToBeRemoved = new IdentityHashMap<>(); |
| |
| // Mapping from callee to the set of callers that were removed from the callee. |
| private Map<DexEncodedMethod, ProgramMethodSet> removedCallEdges = new IdentityHashMap<>(); |
| |
| // Set of nodes from which cycle elimination must be rerun to ensure that all cycles will be |
| // removed. |
| private LinkedHashSet<Node> revisit = new LinkedHashSet<>(); |
| |
| CycleEliminationResult breakCycles(Collection<Node> roots) { |
| // Break cycles in this call graph by removing edges causing cycles. We do this in a fixpoint |
| // because the algorithm does not guarantee that all cycles will be removed from the graph |
| // when we remove an edge in the middle of a cycle that contains another cycle. |
| do { |
| traverse(roots); |
| roots = revisit; |
| prepareForNewTraversal(); |
| } while (!roots.isEmpty()); |
| |
| CycleEliminationResult result = new CycleEliminationResult(removedCallEdges); |
| if (Log.ENABLED) { |
| Log.info(getClass(), "# call graph cycles broken: %s", result.numberOfRemovedCallEdges()); |
| } |
| reset(); |
| return result; |
| } |
| |
| private void prepareForNewTraversal() { |
| assert calleesToBeRemoved.isEmpty(); |
| assert clinitStack.isEmpty(); |
| assert stack.isEmpty(); |
| assert stackEntryInfo.isEmpty(); |
| assert writersToBeRemoved.isEmpty(); |
| assert writerStack.isEmpty(); |
| marked.clear(); |
| revisit = new LinkedHashSet<>(); |
| } |
| |
| private void reset() { |
| assert clinitStack.isEmpty(); |
| assert marked.isEmpty(); |
| assert revisit.isEmpty(); |
| assert stack.isEmpty(); |
| assert stackEntryInfo.isEmpty(); |
| assert writerStack.isEmpty(); |
| removedCallEdges = new IdentityHashMap<>(); |
| } |
| |
| private static class WorkItem { |
| boolean isNode() { |
| return false; |
| } |
| |
| NodeWorkItem asNode() { |
| return null; |
| } |
| |
| boolean isIterator() { |
| return false; |
| } |
| |
| IteratorWorkItem asIterator() { |
| return null; |
| } |
| } |
| |
| private static class NodeWorkItem extends WorkItem { |
| private final Node node; |
| |
| NodeWorkItem(Node node) { |
| this.node = node; |
| } |
| |
| @Override |
| boolean isNode() { |
| return true; |
| } |
| |
| @Override |
| NodeWorkItem asNode() { |
| return this; |
| } |
| } |
| |
| private static class IteratorWorkItem extends WorkItem { |
| private final Node callerOrReader; |
| private final Iterator<Node> calleesAndWriters; |
| |
| IteratorWorkItem(Node callerOrReader, Iterator<Node> calleesAndWriters) { |
| this.callerOrReader = callerOrReader; |
| this.calleesAndWriters = calleesAndWriters; |
| } |
| |
| @Override |
| boolean isIterator() { |
| return true; |
| } |
| |
| @Override |
| IteratorWorkItem asIterator() { |
| return this; |
| } |
| } |
| |
| private void traverse(Collection<Node> roots) { |
| Deque<WorkItem> workItems = new ArrayDeque<>(roots.size()); |
| for (Node node : roots) { |
| workItems.addLast(new NodeWorkItem(node)); |
| } |
| while (!workItems.isEmpty()) { |
| WorkItem workItem = workItems.removeFirst(); |
| if (workItem.isNode()) { |
| Node node = workItem.asNode().node; |
| if (marked.contains(node)) { |
| // Already visited all nodes that can be reached from this node. |
| continue; |
| } |
| |
| Node predecessor = stack.isEmpty() ? null : stack.peek(); |
| push(node, predecessor); |
| |
| // The callees and writers must be sorted before calling traverse recursively. |
| // This ensures that cycles are broken the same way across multiple compilations. |
| Iterator<Node> calleesAndWriterIterator = |
| Iterators.concat( |
| node.getCalleesWithDeterministicOrder().iterator(), |
| node.getWritersWithDeterministicOrder().iterator()); |
| workItems.addFirst(new IteratorWorkItem(node, calleesAndWriterIterator)); |
| } else { |
| assert workItem.isIterator(); |
| IteratorWorkItem iteratorWorkItem = workItem.asIterator(); |
| Node newCallerOrReader = |
| iterateCalleesAndWriters( |
| iteratorWorkItem.calleesAndWriters, iteratorWorkItem.callerOrReader); |
| if (newCallerOrReader != null) { |
| // We did not finish the work on this iterator, so add it again. |
| workItems.addFirst(iteratorWorkItem); |
| workItems.addFirst(new NodeWorkItem(newCallerOrReader)); |
| } else { |
| assert !iteratorWorkItem.calleesAndWriters.hasNext(); |
| pop(iteratorWorkItem.callerOrReader); |
| marked.add(iteratorWorkItem.callerOrReader); |
| |
| Collection<Node> calleesToBeRemovedFromCaller = |
| calleesToBeRemoved.remove(iteratorWorkItem.callerOrReader); |
| if (calleesToBeRemovedFromCaller != null) { |
| calleesToBeRemovedFromCaller.forEach( |
| callee -> { |
| callee.removeCaller(iteratorWorkItem.callerOrReader); |
| recordCallEdgeRemoval(iteratorWorkItem.callerOrReader, callee); |
| }); |
| } |
| |
| Collection<Node> writersToBeRemovedFromReader = |
| writersToBeRemoved.remove(iteratorWorkItem.callerOrReader); |
| if (writersToBeRemovedFromReader != null) { |
| writersToBeRemovedFromReader.forEach( |
| writer -> writer.removeReader(iteratorWorkItem.callerOrReader)); |
| } |
| } |
| } |
| } |
| } |
| |
| private Node iterateCalleesAndWriters( |
| Iterator<Node> calleeOrWriterIterator, Node callerOrReader) { |
| while (calleeOrWriterIterator.hasNext()) { |
| Node calleeOrWriter = calleeOrWriterIterator.next(); |
| StackEntryInfo calleeOrWriterStackEntryInfo = stackEntryInfo.get(calleeOrWriter); |
| boolean foundCycle = calleeOrWriterStackEntryInfo != null; |
| if (!foundCycle) { |
| return calleeOrWriter; |
| } |
| |
| // Found a cycle that needs to be eliminated. If it is a field read edge, then remove it |
| // right away. |
| boolean isFieldReadEdge = calleeOrWriter.hasReader(callerOrReader); |
| if (isFieldReadEdge) { |
| removeFieldReadEdge(callerOrReader, calleeOrWriter); |
| continue; |
| } |
| |
| // Otherwise, it is a call edge. Check if there is a field read edge in the cycle, and if |
| // so, remove that edge. |
| if (!writerStack.isEmpty() |
| && removeIncomingEdgeOnStack( |
| writerStack.peek(), |
| calleeOrWriter, |
| calleeOrWriterStackEntryInfo, |
| this::removeFieldReadEdge)) { |
| continue; |
| } |
| |
| // It is a call edge and the cycle does not contain any field read edges. |
| // If it is a call edge to a <clinit>, then remove it. |
| if (calleeOrWriter.getMethod().isClassInitializer()) { |
| // Calls to class initializers are always safe to remove. |
| assert callEdgeRemovalIsSafe(callerOrReader, calleeOrWriter); |
| removeCallEdge(callerOrReader, calleeOrWriter); |
| continue; |
| } |
| |
| // Otherwise, check if there is a call edge to a <clinit> method in the cycle, and if so, |
| // remove that edge. |
| if (!clinitStack.isEmpty() |
| && removeIncomingEdgeOnStack( |
| clinitStack.peek(), |
| calleeOrWriter, |
| calleeOrWriterStackEntryInfo, |
| this::removeCallEdge)) { |
| continue; |
| } |
| |
| // Otherwise, we remove the call edge if it is safe according to force inlining. |
| if (callEdgeRemovalIsSafe(callerOrReader, calleeOrWriter)) { |
| // Break the cycle by removing the edge node->calleeOrWriter. |
| // Need to remove `calleeOrWriter` from `node.callees` using the iterator to prevent a |
| // ConcurrentModificationException. |
| removeCallEdge(callerOrReader, calleeOrWriter); |
| continue; |
| } |
| |
| // The call edge cannot be removed due to force inlining. Find another call edge in the |
| // cycle that can safely be removed instead. |
| LinkedList<Node> cycle = extractCycle(calleeOrWriter); |
| |
| // Break the cycle by finding an edge that can be removed without breaking force |
| // inlining. If that is not possible, this call fails with a compilation error. |
| CallEdge edge = findCallEdgeForRemoval(cycle); |
| |
| // The edge will be null if this cycle has already been eliminated as a result of |
| // another cycle elimination. |
| if (edge != null) { |
| assert callEdgeRemovalIsSafe(edge.caller, edge.callee); |
| |
| // Break the cycle by removing the edge caller->callee. |
| removeCallEdge(edge.caller, edge.callee); |
| } |
| |
| // Recover the stack. |
| recoverStack(cycle); |
| } |
| return null; |
| } |
| |
| private void push(Node node, Node predecessor) { |
| stack.push(node); |
| assert !stackEntryInfo.containsKey(node); |
| stackEntryInfo.put(node, new StackEntryInfo(stack.size() - 1, predecessor)); |
| if (predecessor != null) { |
| if (node.getMethod().isClassInitializer()) { |
| clinitStack.push(node); |
| } else if (predecessor.getWritersWithDeterministicOrder().contains(node)) { |
| writerStack.push(node); |
| } |
| } |
| } |
| |
| private void pop(Node node) { |
| Node popped = stack.pop(); |
| assert popped == node; |
| assert stackEntryInfo.containsKey(node); |
| stackEntryInfo.remove(node); |
| if (clinitStack.peek() == popped) { |
| assert writerStack.peek() != popped; |
| clinitStack.pop(); |
| } else if (writerStack.peek() == popped) { |
| writerStack.pop(); |
| } |
| } |
| |
| private void removeCallEdge(Node caller, Node callee) { |
| calleesToBeRemoved.computeIfAbsent(caller, ignore -> Sets.newIdentityHashSet()).add(callee); |
| } |
| |
| private void removeFieldReadEdge(Node reader, Node writer) { |
| writersToBeRemoved.computeIfAbsent(reader, ignore -> Sets.newIdentityHashSet()).add(writer); |
| } |
| |
| private boolean removeIncomingEdgeOnStack( |
| Node target, |
| Node currentCalleeOrWriter, |
| StackEntryInfo currentCalleeOrWriterStackEntryInfo, |
| BiConsumer<Node, Node> edgeRemover) { |
| StackEntryInfo targetStackEntryInfo = stackEntryInfo.get(target); |
| boolean cycleContainsTarget = |
| targetStackEntryInfo.index > currentCalleeOrWriterStackEntryInfo.index; |
| if (cycleContainsTarget) { |
| assert verifyCycleSatisfies( |
| currentCalleeOrWriter, |
| cycle -> cycle.contains(target) && cycle.contains(targetStackEntryInfo.predecessor)); |
| if (!targetStackEntryInfo.processed) { |
| edgeRemover.accept(targetStackEntryInfo.predecessor, target); |
| revisit.add(target); |
| targetStackEntryInfo.processed = true; |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| private LinkedList<Node> extractCycle(Node entry) { |
| LinkedList<Node> cycle = new LinkedList<>(); |
| do { |
| assert !stack.isEmpty(); |
| cycle.add(stack.pop()); |
| } while (cycle.getLast() != entry); |
| return cycle; |
| } |
| |
| private boolean verifyCycleSatisfies(Node entry, Predicate<LinkedList<Node>> predicate) { |
| LinkedList<Node> cycle = extractCycle(entry); |
| assert predicate.test(cycle); |
| recoverStack(cycle); |
| return true; |
| } |
| |
| private CallEdge findCallEdgeForRemoval(LinkedList<Node> extractedCycle) { |
| Node callee = extractedCycle.getLast(); |
| for (Node caller : extractedCycle) { |
| if (caller.hasWriter(callee)) { |
| // Not a call edge. |
| assert !caller.hasCallee(callee); |
| assert !callee.hasCaller(caller); |
| callee = caller; |
| continue; |
| } |
| if (!caller.hasCallee(callee)) { |
| // No need to break any edges since this cycle has already been broken previously. |
| assert !callee.hasCaller(caller); |
| return null; |
| } |
| if (callEdgeRemovalIsSafe(caller, callee)) { |
| return new CallEdge(caller, callee); |
| } |
| callee = caller; |
| } |
| throw new CompilationError(CYCLIC_FORCE_INLINING_MESSAGE); |
| } |
| |
| private static boolean callEdgeRemovalIsSafe(Node callerOrReader, Node calleeOrWriter) { |
| // All call edges where the callee is a method that should be force inlined must be kept, |
| // to guarantee that the IR converter will process the callee before the caller. |
| assert calleeOrWriter.hasCaller(callerOrReader); |
| return !calleeOrWriter.getMethod().getOptimizationInfo().forceInline(); |
| } |
| |
| private void recordCallEdgeRemoval(Node caller, Node callee) { |
| removedCallEdges |
| .computeIfAbsent(callee.getMethod(), ignore -> ProgramMethodSet.create(2)) |
| .add(caller.getProgramMethod()); |
| } |
| |
| private void recoverStack(LinkedList<Node> extractedCycle) { |
| Iterator<Node> descendingIt = extractedCycle.descendingIterator(); |
| while (descendingIt.hasNext()) { |
| stack.push(descendingIt.next()); |
| } |
| } |
| } |
| } |