Merge commit 'c04eb1d97999bd1208d389ddab8534928305a141' into dev-release
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java index 073cc8d..3d5f387 100644 --- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java +++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -325,8 +325,7 @@ List<DexString> forcedStrings, Timing timing) throws ExecutionException { - TimingMerger merger = - timing.beginMerger("Write files", ThreadUtils.getNumberOfThreads(executorService)); + TimingMerger merger = timing.beginMerger("Write files", executorService); Collection<Timing> timings = ThreadUtils.processItemsWithResults( virtualFiles, @@ -395,8 +394,7 @@ { // Compute offsets and rewrite jumbo strings so that code offsets are fixed. - TimingMerger merger = - timing.beginMerger("Pre-write phase", ThreadUtils.getNumberOfThreads(executorService)); + TimingMerger merger = timing.beginMerger("Pre-write phase", executorService); Collection<Timing> timings = rewriteJumboStringsAndComputeDebugRepresentation( executorService, virtualFiles, lazyDexStrings);
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriterExperimental.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriterExperimental.java index 853d01e..8324c91 100644 --- a/src/main/java/com/android/tools/r8/dex/ApplicationWriterExperimental.java +++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriterExperimental.java
@@ -96,8 +96,7 @@ List<VirtualFile> virtualFiles, List<DexString> forcedStrings, Timing timing) { - TimingMerger merger = - timing.beginMerger("Write files", ThreadUtils.getNumberOfThreads(executorService)); + TimingMerger merger = timing.beginMerger("Write files", executorService); Collection<Timing> timings; // TODO(b/249922554): Current limitations for the experimental flag. assert globalsSyntheticsConsumer == null;
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java index d72358a..9b3ea7c 100644 --- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java +++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -833,7 +833,8 @@ .forEach( (key, value) -> { assert value == 1; - assert value <= basicBlockNumberGenerator.peek(); + assert key >= 0; + assert key <= basicBlockNumberGenerator.peek(); }); return true; }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Inc.java b/src/main/java/com/android/tools/r8/ir/code/Inc.java index 72b2bf2..5d3a877 100644 --- a/src/main/java/com/android/tools/r8/ir/code/Inc.java +++ b/src/main/java/com/android/tools/r8/ir/code/Inc.java
@@ -33,6 +33,10 @@ return Opcodes.INC; } + public int getIncrement() { + return increment; + } + @Override public <T> T accept(InstructionVisitor<T> visitor) { return visitor.visit(this);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java index 81f5c14..16fa00c 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -52,7 +52,7 @@ import com.android.tools.r8.ir.code.UninitializedThisLocalRead; import com.android.tools.r8.ir.code.Value; import com.android.tools.r8.ir.code.Xor; -import com.android.tools.r8.ir.optimize.CodeRewriter; +import com.android.tools.r8.ir.conversion.passes.TrivialGotosCollapser; import com.android.tools.r8.ir.optimize.DeadCodeRemover; import com.android.tools.r8.ir.optimize.PeepholeOptimizer; import com.android.tools.r8.ir.optimize.PhiOptimizations; @@ -193,10 +193,11 @@ loadStoreHelper.insertPhiMoves(registerAllocator); timing.end(); + TrivialGotosCollapser trivialGotosCollapser = new TrivialGotosCollapser(appView); timing.begin("BasicBlock peephole optimizations"); if (code.getConversionOptions().isPeepholeOptimizationsEnabled()) { for (int i = 0; i < PEEPHOLE_OPTIMIZATION_PASSES; i++) { - CodeRewriter.collapseTrivialGotos(appView, code); + trivialGotosCollapser.run(code.context(), code, timing); PeepholeOptimizer.removeIdenticalPredecessorBlocks(code, registerAllocator); PeepholeOptimizer.shareIdenticalBlockSuffix( code, registerAllocator, SUFFIX_SHARING_OVERHEAD); @@ -206,7 +207,7 @@ timing.time("Rewrite Iinc patterns", this::rewriteIincPatterns); - CodeRewriter.collapseTrivialGotos(appView, code); + trivialGotosCollapser.run(code.context(), code, timing); timing.begin("Remove redundant debug positions"); DexBuilder.removeRedundantDebugPositions(code); timing.end();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java index cd7b5b9..1ddfeac 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
@@ -63,7 +63,7 @@ import com.android.tools.r8.ir.code.Position; import com.android.tools.r8.ir.code.Return; import com.android.tools.r8.ir.code.Value; -import com.android.tools.r8.ir.optimize.CodeRewriter; +import com.android.tools.r8.ir.conversion.passes.TrivialGotosCollapser; import com.android.tools.r8.ir.regalloc.RegisterAllocator; import com.android.tools.r8.utils.InternalOptions; import com.android.tools.r8.utils.InternalOutputMode; @@ -401,7 +401,7 @@ } if (allMatch) { currentBlock.removeInstruction(debugPosition); - CodeRewriter.unlinkTrivialGotoBlock(currentBlock, exit.getTarget()); + TrivialGotosCollapser.unlinkTrivialGotoBlock(currentBlock, exit.getTarget()); code.removeBlocks(Collections.singleton(currentBlock)); // Having removed the block at blockIndex, the previous block may now be a trivial // fallthrough from an if/switch. Rewind to that point and retry. This avoids iterating to
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java index 7636cbd..55c6a06 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -27,10 +27,14 @@ import com.android.tools.r8.ir.code.IRCode; import com.android.tools.r8.ir.code.InstructionIterator; import com.android.tools.r8.ir.code.Value; +import com.android.tools.r8.ir.conversion.passes.ArrayConstructionSimplifier; import com.android.tools.r8.ir.conversion.passes.BinopRewriter; +import com.android.tools.r8.ir.conversion.passes.BranchSimplifier; import com.android.tools.r8.ir.conversion.passes.CommonSubexpressionElimination; import com.android.tools.r8.ir.conversion.passes.ParentConstructorHoistingCodeRewriter; import com.android.tools.r8.ir.conversion.passes.SplitBranch; +import com.android.tools.r8.ir.conversion.passes.TrivialCheckCastAndInstanceOfRemover; +import com.android.tools.r8.ir.conversion.passes.TrivialPhiSimplifier; import com.android.tools.r8.ir.desugar.CfInstructionDesugaringCollection; import com.android.tools.r8.ir.desugar.CovariantReturnTypeAnnotationTransformer; import com.android.tools.r8.ir.optimize.AssertionErrorTwoArgsConstructorRewriter; @@ -538,7 +542,7 @@ if (options.canHaveArtStringNewInitBug()) { timing.begin("Check for new-init issue"); - CodeRewriter.ensureDirectStringNewToInit(code, appView.dexItemFactory()); + TrivialPhiSimplifier.ensureDirectStringNewToInit(code, appView.dexItemFactory()); timing.end(); } @@ -726,8 +730,8 @@ assert code.verifyTypes(appView); timing.begin("Remove trivial type checks/casts"); - codeRewriter.removeTrivialCheckCastAndInstanceOfInstructions( - code, context, methodProcessor, methodProcessingContext); + new TrivialCheckCastAndInstanceOfRemover(appView) + .run(code, context, methodProcessor, methodProcessingContext); timing.end(); if (enumValueOptimizer != null) { @@ -750,9 +754,7 @@ timing.end(); } commonSubexpressionElimination.run(context, code, timing); - timing.begin("Simplify arrays"); - codeRewriter.simplifyArrayConstruction(code); - timing.end(); + new ArrayConstructionSimplifier(appView).run(context, code, timing); timing.begin("Rewrite move result"); codeRewriter.rewriteMoveResult(code); timing.end(); @@ -771,10 +773,10 @@ codeRewriter.optimizeAlwaysThrowingInstructions(code); timing.end(); timing.begin("Simplify control flow"); - if (codeRewriter.simplifyControlFlow(code)) { + if (new BranchSimplifier(appView).simplifyBranches(code)) { timing.begin("Remove trivial type checks/casts"); - codeRewriter.removeTrivialCheckCastAndInstanceOfInstructions( - code, context, methodProcessor, methodProcessingContext); + new TrivialCheckCastAndInstanceOfRemover(appView) + .run(code, context, methodProcessor, methodProcessingContext); timing.end(); } timing.end(); @@ -829,7 +831,6 @@ assert options.inlinerOptions().enableInlining && inliner != null; classInliner.processMethodCode( appView.withLiveness(), - codeRewriter, stringOptimizer, enumValueOptimizer, code.context(), @@ -877,7 +878,7 @@ if (!options.isGeneratingClassFiles()) { timing.begin("Canonicalize constants"); ConstantCanonicalizer constantCanonicalizer = - new ConstantCanonicalizer(appView, codeRewriter, context, code); + new ConstantCanonicalizer(appView, context, code); constantCanonicalizer.canonicalize(); timing.end(); previous = printMethod(code, "IR after constant canonicalization (SSA)", previous);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRToDexFinalizer.java b/src/main/java/com/android/tools/r8/ir/conversion/IRToDexFinalizer.java index 06f3da8..ddcd9ac 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/IRToDexFinalizer.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/IRToDexFinalizer.java
@@ -9,6 +9,7 @@ import com.android.tools.r8.graph.DexEncodedMethod; import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider; import com.android.tools.r8.ir.code.IRCode; +import com.android.tools.r8.ir.conversion.passes.TrivialGotosCollapser; import com.android.tools.r8.ir.desugar.nest.D8NestBasedAccessDesugaring; import com.android.tools.r8.ir.optimize.CodeRewriter; import com.android.tools.r8.ir.optimize.DeadCodeRemover; @@ -22,13 +23,10 @@ public class IRToDexFinalizer extends IRFinalizer<DexCode> { private static final int PEEPHOLE_OPTIMIZATION_PASSES = 2; - - private final CodeRewriter codeRewriter; private final InternalOptions options; public IRToDexFinalizer(AppView<?> appView, DeadCodeRemover deadCodeRemover) { super(appView, deadCodeRemover); - this.codeRewriter = deadCodeRemover.getCodeRewriter(); this.options = appView.options(); } @@ -44,7 +42,7 @@ // Workaround massive dex2oat memory use for self-recursive methods. RuntimeWorkaroundCodeRewriter.workaroundDex2OatInliningIssue(appView, code); // Workaround MAX_INT switch issue. - RuntimeWorkaroundCodeRewriter.workaroundSwitchMaxIntBug(code, codeRewriter, options); + RuntimeWorkaroundCodeRewriter.workaroundSwitchMaxIntBug(code, appView); RuntimeWorkaroundCodeRewriter.workaroundDex2OatLinkedListBug(code, options); RuntimeWorkaroundCodeRewriter.workaroundForwardingInitializerBug(code, options); RuntimeWorkaroundCodeRewriter.workaroundExceptionTargetingLoopHeaderBug(code, options); @@ -62,17 +60,18 @@ LinearScanRegisterAllocator registerAllocator = new LinearScanRegisterAllocator(appView, code); registerAllocator.allocateRegisters(); timing.end(); + TrivialGotosCollapser trivialGotosCollapser = new TrivialGotosCollapser(appView); if (code.getConversionOptions().isPeepholeOptimizationsEnabled()) { timing.begin("Peephole optimize"); for (int i = 0; i < PEEPHOLE_OPTIMIZATION_PASSES; i++) { - CodeRewriter.collapseTrivialGotos(appView, code); + trivialGotosCollapser.run(code.context(), code, timing); PeepholeOptimizer.optimize(appView, code, registerAllocator); } timing.end(); } timing.begin("Clean up"); CodeRewriter.removeUnneededMovesOnExitingPaths(code, registerAllocator); - CodeRewriter.collapseTrivialGotos(appView, code); + trivialGotosCollapser.run(code.context(), code, timing); timing.end(); return registerAllocator; }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java index 1efe772..4c15125 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
@@ -104,7 +104,7 @@ import com.android.tools.r8.ir.code.UnusedArgument; import com.android.tools.r8.ir.code.Value; import com.android.tools.r8.ir.code.ValueType; -import com.android.tools.r8.ir.optimize.CodeRewriter; +import com.android.tools.r8.ir.conversion.passes.TrivialPhiSimplifier; import com.android.tools.r8.ir.optimize.enums.EnumUnboxer; import com.android.tools.r8.optimize.MemberRebindingAnalysis; import com.android.tools.r8.optimize.argumentpropagation.lenscoderewriter.NullCheckInserter; @@ -231,7 +231,7 @@ if (unusedArgument.outValue().hasPhiUsers()) { // See b/240282988: We can end up in situations where the second round of IR processing // introduce phis for irreducible control flow, we need to resolve them. - CodeRewriter.replaceUnusedArgumentTrivialPhis(unusedArgument); + TrivialPhiSimplifier.replaceUnusedArgumentTrivialPhis(unusedArgument); } } }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java index fe3085a..ab073dd 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
@@ -178,8 +178,7 @@ ExecutorService executorService, Timing timing) throws ExecutionException { - TimingMerger merger = - timing.beginMerger("secondary-processor", ThreadUtils.getNumberOfThreads(executorService)); + TimingMerger merger = timing.beginMerger("secondary-processor", executorService); while (!waves.isEmpty()) { wave = waves.removeFirst(); assert !wave.isEmpty();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryMethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryMethodProcessor.java index 9da34d4..45450f9 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/PrimaryMethodProcessor.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/PrimaryMethodProcessor.java
@@ -129,8 +129,7 @@ Timing timing, ExecutorService executorService) throws ExecutionException { - TimingMerger merger = - timing.beginMerger("primary-processor", ThreadUtils.getNumberOfThreads(executorService)); + TimingMerger merger = timing.beginMerger("primary-processor", executorService); while (!waves.isEmpty()) { processorContext = appView.createProcessorContext(); wave = waves.removeFirst();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallSiteInformation.java b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallSiteInformation.java index c2e13af..e412ed7 100644 --- a/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallSiteInformation.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/callgraph/CallSiteInformation.java
@@ -13,6 +13,8 @@ import com.android.tools.r8.utils.classhierarchy.MethodOverridesCollector; import com.android.tools.r8.utils.collections.ProgramMethodSet; import com.google.common.collect.Sets; +import java.util.IdentityHashMap; +import java.util.Map; import java.util.Set; public abstract class CallSiteInformation { @@ -23,6 +25,14 @@ * <p>For pinned methods (methods kept through Proguard keep rules) this will always answer <code> * false</code>. */ + public abstract boolean hasSingleCallSite(ProgramMethod method, ProgramMethod context); + + /** + * Checks if the given method only has a single call without considering context. + * + * <p>For pinned methods (methods kept through Proguard keep rules) and methods that override a + * library method this always returns false. + */ public abstract boolean hasSingleCallSite(ProgramMethod method); public abstract boolean isMultiCallerInlineCandidate(ProgramMethod method); @@ -38,6 +48,11 @@ private static final EmptyCallSiteInformation EMPTY_INFO = new EmptyCallSiteInformation(); @Override + public boolean hasSingleCallSite(ProgramMethod method, ProgramMethod context) { + return false; + } + + @Override public boolean hasSingleCallSite(ProgramMethod method) { return false; } @@ -55,7 +70,9 @@ static class CallGraphBasedCallSiteInformation extends CallSiteInformation { - private final Set<DexMethod> singleCallerMethods = Sets.newIdentityHashSet(); + // Single callers track their calling context to ensure that the predicate is stable after + // inlining of the caller. + private final Map<DexMethod, DexMethod> singleCallerMethods = new IdentityHashMap<>(); private final Set<DexMethod> multiCallerInlineCandidates = Sets.newIdentityHashSet(); CallGraphBasedCallSiteInformation(AppView<AppInfoWithLiveness> appView, CallGraph graph) { @@ -94,7 +111,16 @@ int numberOfCallSites = node.getNumberOfCallSites(); if (numberOfCallSites == 1) { - singleCallerMethods.add(reference); + Set<Node> callersWithDeterministicOrder = node.getCallersWithDeterministicOrder(); + DexMethod caller = reference; + // We can have recursive methods where the recursive call is the only call site. We do + // not track callers for these. + if (!callersWithDeterministicOrder.isEmpty()) { + assert callersWithDeterministicOrder.size() == 1; + caller = callersWithDeterministicOrder.iterator().next().getMethod().getReference(); + } + DexMethod existing = singleCallerMethods.put(reference, caller); + assert existing == null; } else if (numberOfCallSites > 1) { multiCallerInlineCandidates.add(reference); } @@ -102,14 +128,25 @@ } /** - * Checks if the given method only has a single call site. + * Checks if the given method only has a single call site with the given context. + * + * <p>For pinned methods (methods kept through Proguard keep rules) and methods that override a + * library method this always returns false. + */ + @Override + public boolean hasSingleCallSite(ProgramMethod method, ProgramMethod context) { + return singleCallerMethods.get(method.getReference()) == context.getReference(); + } + + /** + * Checks if the given method only has a single call without considering context. * * <p>For pinned methods (methods kept through Proguard keep rules) and methods that override a * library method this always returns false. */ @Override public boolean hasSingleCallSite(ProgramMethod method) { - return singleCallerMethods.contains(method.getReference()); + return singleCallerMethods.containsKey(method.getReference()); } /**
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java new file mode 100644 index 0000000..bc8f8fb --- /dev/null +++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ArrayConstructionSimplifier.java
@@ -0,0 +1,474 @@ +// Copyright (c) 2023, 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.passes; + +import com.android.tools.r8.graph.AppInfo; +import com.android.tools.r8.graph.AppView; +import com.android.tools.r8.graph.DexClass; +import com.android.tools.r8.graph.DexItemFactory; +import com.android.tools.r8.graph.DexType; +import com.android.tools.r8.graph.ProgramMethod; +import com.android.tools.r8.ir.analysis.type.ArrayTypeElement; +import com.android.tools.r8.ir.analysis.type.TypeElement; +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.InstructionListIterator; +import com.android.tools.r8.ir.code.InvokeNewArray; +import com.android.tools.r8.ir.code.LinearFlowInstructionListIterator; +import com.android.tools.r8.ir.code.NewArrayEmpty; +import com.android.tools.r8.ir.code.NewArrayFilledData; +import com.android.tools.r8.ir.code.Value; +import com.android.tools.r8.utils.InternalOptions; +import com.android.tools.r8.utils.InternalOptions.RewriteArrayOptions; +import com.android.tools.r8.utils.SetUtils; +import com.android.tools.r8.utils.WorkList; +import com.google.common.collect.Sets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +/** + * Replace new-array followed by stores of constants to all entries with new-array and + * fill-array-data / filled-new-array. + * + * <p>The format of the new-array and its puts must be of the form: + * + * <pre> + * v0 <- new-array T vSize + * ... + * array-put v0 vValue1 vIndex1 + * ... + * array-put v0 vValueN vIndexN + * </pre> + * + * <p>The flow between the array v0 and its puts must be linear with no other uses of v0 besides the + * array-put instructions, thus any no intermediate instruction (... above) must use v0 and also + * cannot have catch handlers that would transfer out control (those could then have uses of v0). + * + * <p>The allocation of the new-array can itself have catch handlers, in which case, those are to + * remain active on the translated code. Translated code can have two forms. + * + * <p>The first is using the original array allocation and filling in its data if it can be encoded: + * + * <pre> + * v0 <- new-array T vSize + * filled-array-data v0 + * ... + * ... + * </pre> + * + * The data payload is encoded directly in the instruction so no dependencies are needed for filling + * the data array. Thus, the fill is inserted at the point of the allocation. If the allocation has + * catch handlers its block must be split and the handlers put on the fill instruction too. This is + * correct only because there are no exceptional transfers in (...) that could observe the early + * initialization of the data. + * + * <p>The second is using filled-new-array and has the form: + * + * <pre> + * ... + * ... + * v0 <- filled-new-array T vValue1 ... vValueN + * </pre> + * + * Here the correctness relies on no exceptional transfers in (...) that could observe the missing + * allocation of the array. The late allocation ensures that the values are available at allocation + * time. If the original allocation has catch handlers then the new allocation needs to link those + * too. In general that may require splitting the block twice so that the new allocation is the + * single throwing instruction in its block. + */ +public class ArrayConstructionSimplifier extends CodeRewriterPass<AppInfo> { + + private final DexItemFactory dexItemFactory; + + public ArrayConstructionSimplifier(AppView<?> appView) { + super(appView); + this.dexItemFactory = appView.dexItemFactory(); + } + + @Override + String getTimingId() { + return "ArrayConstructionSimplifier"; + } + + @Override + void rewriteCode(ProgramMethod method, IRCode code) { + WorkList<BasicBlock> worklist = WorkList.newIdentityWorkList(code.blocks); + while (worklist.hasNext()) { + BasicBlock block = worklist.next(); + simplifyArrayConstructionBlock(block, worklist, code, appView.options()); + } + } + + @Override + boolean shouldRewriteCode(ProgramMethod method, IRCode code) { + return appView.options().isGeneratingDex(); + } + + private void simplifyArrayConstructionBlock( + BasicBlock block, WorkList<BasicBlock> worklist, IRCode code, InternalOptions options) { + RewriteArrayOptions rewriteOptions = options.rewriteArrayOptions(); + InstructionListIterator it = block.listIterator(code); + while (it.hasNext()) { + FilledArrayCandidate candidate = computeFilledArrayCandidate(it.next(), rewriteOptions); + if (candidate == null) { + continue; + } + FilledArrayConversionInfo info = + computeConversionInfo( + candidate, new LinearFlowInstructionListIterator(code, block, it.nextIndex())); + if (info == null) { + continue; + } + + Instruction instructionAfterCandidate = it.peekNext(); + NewArrayEmpty newArrayEmpty = candidate.newArrayEmpty; + DexType arrayType = newArrayEmpty.type; + int size = candidate.size; + Set<Instruction> instructionsToRemove = SetUtils.newIdentityHashSet(size + 1); + if (candidate.useFilledNewArray()) { + assert newArrayEmpty.getLocalInfo() == null; + Instruction lastArrayPut = info.lastArrayPutIterator.peekPrevious(); + Value invokeValue = code.createValue(newArrayEmpty.getOutType(), null); + InvokeNewArray invoke = + new InvokeNewArray(arrayType, invokeValue, Arrays.asList(info.values)); + invoke.setPosition(lastArrayPut.getPosition()); + for (Value value : newArrayEmpty.inValues()) { + value.removeUser(newArrayEmpty); + } + newArrayEmpty.outValue().replaceUsers(invokeValue); + instructionsToRemove.add(newArrayEmpty); + + boolean originalAllocationPointHasHandlers = block.hasCatchHandlers(); + boolean insertionPointHasHandlers = lastArrayPut.getBlock().hasCatchHandlers(); + + if (!insertionPointHasHandlers && !originalAllocationPointHasHandlers) { + info.lastArrayPutIterator.add(invoke); + } else { + BasicBlock insertionBlock = info.lastArrayPutIterator.split(code); + if (originalAllocationPointHasHandlers) { + if (!insertionBlock.isTrivialGoto()) { + BasicBlock blockAfterInsertion = insertionBlock.listIterator(code).split(code); + assert insertionBlock.isTrivialGoto(); + worklist.addIfNotSeen(blockAfterInsertion); + } + insertionBlock.moveCatchHandlers(block); + } else { + worklist.addIfNotSeen(insertionBlock); + } + insertionBlock.listIterator(code).add(invoke); + } + } else { + assert candidate.useFilledArrayData(); + int elementSize = arrayType.elementSizeForPrimitiveArrayType(); + short[] contents = computeArrayFilledData(info.values, size, elementSize); + if (contents == null) { + continue; + } + // fill-array-data requires the new-array-empty instruction to remain, as it does not + // itself create an array. + NewArrayFilledData fillArray = + new NewArrayFilledData(newArrayEmpty.outValue(), elementSize, size, contents); + fillArray.setPosition(newArrayEmpty.getPosition()); + BasicBlock newBlock = + it.addThrowingInstructionToPossiblyThrowingBlock(code, null, fillArray, options); + if (newBlock != null) { + worklist.addIfNotSeen(newBlock); + } + } + + instructionsToRemove.addAll(info.arrayPutsToRemove); + Set<BasicBlock> visitedBlocks = Sets.newIdentityHashSet(); + for (Instruction instruction : instructionsToRemove) { + BasicBlock ownerBlock = instruction.getBlock(); + // If owner block is null, then the instruction has been removed already. We can't rely on + // just having the block pointer nulled, so the visited blocks guards reprocessing. + if (ownerBlock != null && visitedBlocks.add(ownerBlock)) { + InstructionListIterator removeIt = ownerBlock.listIterator(code); + while (removeIt.hasNext()) { + if (instructionsToRemove.contains(removeIt.next())) { + removeIt.removeOrReplaceByDebugLocalRead(); + } + } + } + } + + // The above has invalidated the block iterator so reset it and continue. + it = block.listIterator(code, instructionAfterCandidate); + } + } + + private short[] computeArrayFilledData(Value[] values, int size, int elementSize) { + for (Value v : values) { + if (!v.isConstant()) { + return null; + } + } + if (elementSize == 1) { + short[] result = new short[(size + 1) / 2]; + for (int i = 0; i < size; i += 2) { + short value = + (short) (values[i].getConstInstruction().asConstNumber().getIntValue() & 0xFF); + if (i + 1 < size) { + value |= + (short) + ((values[i + 1].getConstInstruction().asConstNumber().getIntValue() & 0xFF) << 8); + } + result[i / 2] = value; + } + return result; + } + assert elementSize == 2 || elementSize == 4 || elementSize == 8; + int shortsPerConstant = elementSize / 2; + short[] result = new short[size * shortsPerConstant]; + for (int i = 0; i < size; i++) { + long value = values[i].getConstInstruction().asConstNumber().getRawValue(); + for (int part = 0; part < shortsPerConstant; part++) { + result[i * shortsPerConstant + part] = (short) ((value >> (16 * part)) & 0xFFFFL); + } + } + return result; + } + + private static class FilledArrayConversionInfo { + + Value[] values; + List<ArrayPut> arrayPutsToRemove; + LinearFlowInstructionListIterator lastArrayPutIterator; + + public FilledArrayConversionInfo(int size) { + values = new Value[size]; + arrayPutsToRemove = new ArrayList<>(size); + } + } + + private FilledArrayConversionInfo computeConversionInfo( + FilledArrayCandidate candidate, LinearFlowInstructionListIterator it) { + NewArrayEmpty newArrayEmpty = candidate.newArrayEmpty; + assert it.peekPrevious() == newArrayEmpty; + Value arrayValue = newArrayEmpty.outValue(); + int size = candidate.size; + + // aput-object allows any object for arrays of interfaces, but new-filled-array fails to verify + // if types require a cast. + // TODO(b/246971330): Check if adding a checked-cast would have the same observable result. E.g. + // if aput-object throws a ClassCastException if given an object that does not implement the + // desired interface, then we could add check-cast instructions for arguments we're not sure + // about. + DexType elementType = newArrayEmpty.type.toDimensionMinusOneType(dexItemFactory); + boolean needsTypeCheck = + !elementType.isPrimitiveType() && elementType != dexItemFactory.objectType; + + FilledArrayConversionInfo info = new FilledArrayConversionInfo(size); + Value[] values = info.values; + int remaining = size; + Set<Instruction> users = newArrayEmpty.outValue().uniqueUsers(); + while (it.hasNext()) { + Instruction instruction = it.next(); + BasicBlock block = instruction.getBlock(); + // If we encounter an instruction that can throw an exception we need to bail out of the + // optimization so that we do not transform half-initialized arrays into fully initialized + // arrays on exceptional edges. If the block has no handlers it is not observable so + // we perform the rewriting. + if (block.hasCatchHandlers() && instruction.instructionInstanceCanThrow()) { + return null; + } + if (!users.contains(instruction)) { + // If any instruction can transfer control between the new-array and the last array put + // then it is not safe to move the new array to the point of the last put. + if (block.hasCatchHandlers() && instruction.instructionTypeCanThrow()) { + return null; + } + continue; + } + ArrayPut arrayPut = instruction.asArrayPut(); + // If the initialization sequence is broken by another use we cannot use a fill-array-data + // instruction. + if (arrayPut == null || arrayPut.array() != arrayValue) { + return null; + } + if (!arrayPut.index().isConstNumber()) { + return null; + } + if (arrayPut.instructionInstanceCanThrow()) { + assert false; + return null; + } + int index = arrayPut.index().getConstInstruction().asConstNumber().getIntValue(); + if (index < 0 || index >= values.length) { + return null; + } + if (values[index] != null) { + return null; + } + Value value = arrayPut.value(); + if (needsTypeCheck && !value.isAlwaysNull(appView)) { + DexType valueDexType = value.getType().asReferenceType().toDexType(dexItemFactory); + if (elementType.isArrayType()) { + if (elementType != valueDexType) { + return null; + } + } else if (valueDexType.isArrayType()) { + // isSubtype asserts for this case. + return null; + } else if (valueDexType.isNullValueType()) { + // Assume instructions can cause value.isAlwaysNull() == false while the DexType is null. + // TODO(b/246971330): Figure out how to write a test in SimplifyArrayConstructionTest + // that hits this case. + } else { + // TODO(b/246971330): When in d8 mode, we might still be able to see if this is true for + // library types (which this helper does not do). + if (appView.isSubtype(valueDexType, elementType).isPossiblyFalse()) { + return null; + } + } + } + info.arrayPutsToRemove.add(arrayPut); + values[index] = value; + --remaining; + if (remaining == 0) { + info.lastArrayPutIterator = it; + return info; + } + } + return null; + } + + private static class FilledArrayCandidate { + + final NewArrayEmpty newArrayEmpty; + final int size; + final boolean encodeAsFilledNewArray; + + public FilledArrayCandidate( + NewArrayEmpty newArrayEmpty, int size, boolean encodeAsFilledNewArray) { + assert size > 0; + this.newArrayEmpty = newArrayEmpty; + this.size = size; + this.encodeAsFilledNewArray = encodeAsFilledNewArray; + } + + public boolean useFilledNewArray() { + return encodeAsFilledNewArray; + } + + public boolean useFilledArrayData() { + return !useFilledNewArray(); + } + } + + private FilledArrayCandidate computeFilledArrayCandidate( + Instruction instruction, RewriteArrayOptions options) { + NewArrayEmpty newArrayEmpty = instruction.asNewArrayEmpty(); + if (newArrayEmpty == null) { + return null; + } + if (instruction.getLocalInfo() != null) { + return null; + } + if (!newArrayEmpty.size().isConstant()) { + return null; + } + int size = newArrayEmpty.size().getConstInstruction().asConstNumber().getIntValue(); + if (!options.isPotentialSize(size)) { + return null; + } + DexType arrayType = newArrayEmpty.type; + boolean encodeAsFilledNewArray = canUseFilledNewArray(arrayType, size, options); + if (!encodeAsFilledNewArray && !canUseFilledArrayData(arrayType, size, options)) { + return null; + } + // Check that all arguments to the array is the array type or that the array is type Object[]. + if (!options.canUseSubTypesInFilledNewArray() + && arrayType != dexItemFactory.objectArrayType + && !arrayType.isPrimitiveArrayType()) { + DexType elementType = arrayType.toArrayElementType(dexItemFactory); + for (Instruction uniqueUser : newArrayEmpty.outValue().uniqueUsers()) { + if (uniqueUser.isArrayPut() + && uniqueUser.asArrayPut().array() == newArrayEmpty.outValue() + && !checkTypeOfArrayPut(uniqueUser.asArrayPut(), elementType)) { + return null; + } + } + } + return new FilledArrayCandidate(newArrayEmpty, size, encodeAsFilledNewArray); + } + + private boolean checkTypeOfArrayPut(ArrayPut arrayPut, DexType elementType) { + TypeElement valueType = arrayPut.value().getType(); + if (!valueType.isPrimitiveType() && elementType == dexItemFactory.objectType) { + return true; + } + if (valueType.isNullType() && !elementType.isPrimitiveType()) { + return true; + } + if (elementType.isArrayType()) { + if (valueType.isNullType()) { + return true; + } + ArrayTypeElement arrayTypeElement = valueType.asArrayType(); + if (arrayTypeElement == null + || arrayTypeElement.getNesting() != elementType.getNumberOfLeadingSquareBrackets()) { + return false; + } + valueType = arrayTypeElement.getBaseType(); + elementType = elementType.toBaseType(dexItemFactory); + } + assert !valueType.isArrayType(); + assert !elementType.isArrayType(); + if (valueType.isPrimitiveType() && !elementType.isPrimitiveType()) { + return false; + } + if (valueType.isPrimitiveType()) { + return true; + } + DexClass clazz = appView.definitionFor(elementType); + if (clazz == null) { + return false; + } + return clazz.isInterface() || valueType.isClassType(elementType); + } + + private boolean canUseFilledNewArray(DexType arrayType, int size, RewriteArrayOptions options) { + if (size < options.minSizeForFilledNewArray) { + return false; + } + // filled-new-array is implemented only for int[] and Object[]. + // For int[], using filled-new-array is usually smaller than filled-array-data. + // filled-new-array supports up to 5 registers before it's filled-new-array/range. + if (!arrayType.isPrimitiveArrayType()) { + if (size > options.maxSizeForFilledNewArrayOfReferences) { + return false; + } + if (arrayType == dexItemFactory.stringArrayType) { + return options.canUseFilledNewArrayOfStrings(); + } + if (!options.canUseFilledNewArrayOfNonStringObjects()) { + return false; + } + if (!options.canUseFilledNewArrayOfArrays() + && arrayType.getNumberOfLeadingSquareBrackets() > 1) { + return false; + } + return true; + } + if (arrayType == dexItemFactory.intArrayType) { + return size <= options.maxSizeForFilledNewArrayOfInts; + } + return false; + } + + private boolean canUseFilledArrayData(DexType arrayType, int size, RewriteArrayOptions options) { + // If there is only one element it is typically smaller to generate the array put + // instruction instead of fill array data. + if (size < options.minSizeForFilledArrayData || size > options.maxSizeForFilledArrayData) { + return false; + } + return arrayType.isPrimitiveArrayType(); + } +}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java new file mode 100644 index 0000000..199df62 --- /dev/null +++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java
@@ -0,0 +1,1231 @@ +// Copyright (c) 2023, 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.passes; + +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.FieldResolutionResult.SingleProgramFieldResolutionResult; +import com.android.tools.r8.graph.ProgramMethod; +import com.android.tools.r8.horizontalclassmerging.HorizontalClassMergerUtils; +import com.android.tools.r8.ir.analysis.equivalence.BasicBlockBehavioralSubsumption; +import com.android.tools.r8.ir.analysis.type.TypeAnalysis; +import com.android.tools.r8.ir.analysis.value.AbstractValue; +import com.android.tools.r8.ir.analysis.value.ConstantOrNonConstantNumberValue; +import com.android.tools.r8.ir.analysis.value.SingleConstClassValue; +import com.android.tools.r8.ir.analysis.value.SingleFieldValue; +import com.android.tools.r8.ir.code.BasicBlock; +import com.android.tools.r8.ir.code.ConstNumber; +import com.android.tools.r8.ir.code.Goto; +import com.android.tools.r8.ir.code.IRCode; +import com.android.tools.r8.ir.code.IRMetadata; +import com.android.tools.r8.ir.code.If; +import com.android.tools.r8.ir.code.IfType; +import com.android.tools.r8.ir.code.InstanceGet; +import com.android.tools.r8.ir.code.Instruction; +import com.android.tools.r8.ir.code.InstructionIterator; +import com.android.tools.r8.ir.code.InstructionListIterator; +import com.android.tools.r8.ir.code.IntSwitch; +import com.android.tools.r8.ir.code.NumericType; +import com.android.tools.r8.ir.code.Phi; +import com.android.tools.r8.ir.code.Position; +import com.android.tools.r8.ir.code.Switch; +import com.android.tools.r8.ir.code.Value; +import com.android.tools.r8.ir.code.ValueType; +import com.android.tools.r8.ir.code.Xor; +import com.android.tools.r8.ir.optimize.controlflow.SwitchCaseAnalyzer; +import com.android.tools.r8.shaking.AppInfoWithLiveness; +import com.android.tools.r8.utils.BooleanUtils; +import com.android.tools.r8.utils.InternalOptions; +import com.android.tools.r8.utils.InternalOutputMode; +import com.android.tools.r8.utils.LongInterval; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap; +import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntIterator; +import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.PriorityQueue; +import java.util.Set; + +public class BranchSimplifier { + + private final AppView<?> appView; + private final InternalOptions options; + + public BranchSimplifier(AppView<?> appView) { + this.appView = appView; + this.options = appView.options(); + } + + public boolean simplifyBranches(IRCode code) { + boolean anyAffectedValues = rewriteSwitch(code); + anyAffectedValues |= simplifyIf(code).anyAffectedValues(); + return anyAffectedValues; + } + + public ControlFlowSimplificationResult simplifyIf(IRCode code) { + BasicBlockBehavioralSubsumption behavioralSubsumption = + new BasicBlockBehavioralSubsumption(appView, code); + boolean simplified = false; + for (BasicBlock block : code.blocks) { + // Skip removed (= unreachable) blocks. + if (block.getNumber() != 0 && block.getPredecessors().isEmpty()) { + continue; + } + if (block.exit().isIf()) { + flipIfBranchesIfNeeded(code, block); + if (rewriteIfWithConstZero(code, block)) { + simplified = true; + } + + if (simplifyKnownBooleanCondition(code, block)) { + simplified = true; + if (!block.exit().isIf()) { + continue; + } + } + + // Simplify if conditions when possible. + If theIf = block.exit().asIf(); + if (theIf.isZeroTest()) { + if (simplifyIfZeroTest(code, block, theIf)) { + simplified = true; + continue; + } + } else { + if (simplifyNonIfZeroTest(code, block, theIf)) { + simplified = true; + continue; + } + } + + // Unable to determine which branch will be taken. Check if the true target can safely be + // rewritten to the false target. + if (behavioralSubsumption.isSubsumedBy( + theIf.inValues().get(0), theIf.getTrueTarget(), theIf.fallthroughBlock())) { + simplifyIfWithKnownCondition(code, block, theIf, theIf.fallthroughBlock()); + simplified = true; + } + } + } + Set<Value> affectedValues = code.removeUnreachableBlocks(); + if (!affectedValues.isEmpty()) { + new TypeAnalysis(appView).narrowing(affectedValues); + } + assert code.isConsistentSSA(appView); + return new ControlFlowSimplificationResult(!affectedValues.isEmpty(), simplified); + } + + public static class ControlFlowSimplificationResult { + + private final boolean anyAffectedValues; + private final boolean anySimplifications; + + private ControlFlowSimplificationResult(boolean anyAffectedValues, boolean anySimplifications) { + assert !anyAffectedValues || anySimplifications; + this.anyAffectedValues = anyAffectedValues; + this.anySimplifications = anySimplifications; + } + + public boolean anyAffectedValues() { + return anyAffectedValues; + } + + public boolean anySimplifications() { + return anySimplifications; + } + } + + private boolean simplifyIfZeroTest(IRCode code, BasicBlock block, If theIf) { + Value lhs = theIf.lhs(); + Value lhsRoot = lhs.getAliasedValue(); + if (lhsRoot.isConstNumber()) { + ConstNumber cond = lhsRoot.getConstInstruction().asConstNumber(); + BasicBlock target = theIf.targetFromCondition(cond); + simplifyIfWithKnownCondition(code, block, theIf, target); + return true; + } + + if (theIf.isNullTest()) { + assert theIf.getType() == IfType.EQ || theIf.getType() == IfType.NE; + + if (lhs.isAlwaysNull(appView)) { + simplifyIfWithKnownCondition(code, block, theIf, theIf.targetFromNullObject()); + return true; + } + + if (lhs.isNeverNull()) { + simplifyIfWithKnownCondition(code, block, theIf, theIf.targetFromNonNullObject()); + return true; + } + } + + if (theIf.getType() == IfType.EQ || theIf.getType() == IfType.NE) { + AbstractValue lhsAbstractValue = lhs.getAbstractValue(appView, code.context()); + if (lhsAbstractValue.isConstantOrNonConstantNumberValue() + && !lhsAbstractValue.asConstantOrNonConstantNumberValue().containsInt(0)) { + // Value doesn't contain zero at all. + simplifyIfWithKnownCondition(code, block, theIf, theIf.targetFromCondition(1)); + return true; + } + } + + if (lhs.hasValueRange()) { + LongInterval interval = lhs.getValueRange(); + if (!interval.containsValue(0)) { + // Interval doesn't contain zero at all. + int sign = Long.signum(interval.getMin()); + simplifyIfWithKnownCondition(code, block, theIf, sign); + return true; + } + + // Interval contains zero. + switch (theIf.getType()) { + case GE: + case LT: + // [a, b] >= 0 is always true if a >= 0. + // [a, b] < 0 is always false if a >= 0. + // In both cases a zero condition takes the right branch. + if (interval.getMin() == 0) { + simplifyIfWithKnownCondition(code, block, theIf, 0); + return true; + } + break; + + case LE: + case GT: + // [a, b] <= 0 is always true if b <= 0. + // [a, b] > 0 is always false if b <= 0. + // In both cases a zero condition takes the right branch. + if (interval.getMax() == 0) { + simplifyIfWithKnownCondition(code, block, theIf, 0); + return true; + } + break; + + case EQ: + case NE: + // Only a single element interval [0, 0] can be dealt with here. + // Such intervals should have been replaced by constants. + assert !interval.isSingleValue(); + break; + } + } + return false; + } + + private boolean simplifyNonIfZeroTest(IRCode code, BasicBlock block, If theIf) { + Value lhs = theIf.lhs(); + Value lhsRoot = lhs.getAliasedValue(); + Value rhs = theIf.rhs(); + Value rhsRoot = rhs.getAliasedValue(); + if (lhsRoot == rhsRoot) { + // Comparing the same value. + simplifyIfWithKnownCondition(code, block, theIf, theIf.targetFromCondition(0)); + return true; + } + + if (lhsRoot.isDefinedByInstructionSatisfying(Instruction::isCreatingInstanceOrArray) + && rhsRoot.isDefinedByInstructionSatisfying(Instruction::isCreatingInstanceOrArray)) { + // Comparing two newly created objects. + assert theIf.getType() == IfType.EQ || theIf.getType() == IfType.NE; + simplifyIfWithKnownCondition(code, block, theIf, theIf.targetFromCondition(1)); + return true; + } + + if (lhsRoot.isConstNumber() && rhsRoot.isConstNumber()) { + // Zero test with a constant of comparison between between two constants. + ConstNumber left = lhsRoot.getConstInstruction().asConstNumber(); + ConstNumber right = rhsRoot.getConstInstruction().asConstNumber(); + BasicBlock target = theIf.targetFromCondition(left, right); + simplifyIfWithKnownCondition(code, block, theIf, target); + return true; + } + + if (theIf.getType() == IfType.EQ || theIf.getType() == IfType.NE) { + AbstractValue lhsAbstractValue = lhs.getAbstractValue(appView, code.context()); + AbstractValue rhsAbstractValue = rhs.getAbstractValue(appView, code.context()); + if (lhsAbstractValue.isConstantOrNonConstantNumberValue() + && rhsAbstractValue.isConstantOrNonConstantNumberValue()) { + ConstantOrNonConstantNumberValue lhsNumberValue = + lhsAbstractValue.asConstantOrNonConstantNumberValue(); + ConstantOrNonConstantNumberValue rhsNumberValue = + rhsAbstractValue.asConstantOrNonConstantNumberValue(); + if (!lhsNumberValue.mayOverlapWith(rhsNumberValue)) { + // No overlap. + simplifyIfWithKnownCondition(code, block, theIf, 1); + return true; + } + } + } + + if (lhs.hasValueRange() && rhs.hasValueRange()) { + // Zero test with a value range, or comparison between between two values, + // each with a value ranges. + LongInterval leftRange = lhs.getValueRange(); + LongInterval rightRange = rhs.getValueRange(); + // Two overlapping ranges. Check for single point overlap. + if (!leftRange.overlapsWith(rightRange)) { + // No overlap. + int cond = Long.signum(leftRange.getMin() - rightRange.getMin()); + simplifyIfWithKnownCondition(code, block, theIf, cond); + return true; + } + + // The two intervals overlap. We can simplify if they overlap at the end points. + switch (theIf.getType()) { + case LT: + case GE: + // [a, b] < [c, d] is always false when a == d. + // [a, b] >= [c, d] is always true when a == d. + // In both cases 0 condition will choose the right branch. + if (leftRange.getMin() == rightRange.getMax()) { + simplifyIfWithKnownCondition(code, block, theIf, 0); + return true; + } + break; + case GT: + case LE: + // [a, b] > [c, d] is always false when b == c. + // [a, b] <= [c, d] is always true when b == c. + // In both cases 0 condition will choose the right branch. + if (leftRange.getMax() == rightRange.getMin()) { + simplifyIfWithKnownCondition(code, block, theIf, 0); + return true; + } + break; + case EQ: + case NE: + // Since there is overlap EQ and NE cannot be determined. + break; + } + } + + if (theIf.getType() == IfType.EQ || theIf.getType() == IfType.NE) { + ProgramMethod context = code.context(); + AbstractValue abstractValue = lhs.getAbstractValue(appView, context); + if (abstractValue.isSingleConstClassValue()) { + AbstractValue otherAbstractValue = rhs.getAbstractValue(appView, context); + if (otherAbstractValue.isSingleConstClassValue()) { + SingleConstClassValue singleConstClassValue = abstractValue.asSingleConstClassValue(); + SingleConstClassValue otherSingleConstClassValue = + otherAbstractValue.asSingleConstClassValue(); + simplifyIfWithKnownCondition( + code, + block, + theIf, + BooleanUtils.intValue( + singleConstClassValue.getType() != otherSingleConstClassValue.getType())); + return true; + } + return false; + } + + if (abstractValue.isSingleFieldValue()) { + AbstractValue otherAbstractValue = rhs.getAbstractValue(appView, context); + if (otherAbstractValue.isSingleFieldValue()) { + SingleFieldValue singleFieldValue = abstractValue.asSingleFieldValue(); + SingleFieldValue otherSingleFieldValue = otherAbstractValue.asSingleFieldValue(); + if (singleFieldValue.getField() == otherSingleFieldValue.getField()) { + simplifyIfWithKnownCondition(code, block, theIf, 0); + return true; + } + + DexClass holder = appView.definitionForHolder(singleFieldValue.getField()); + DexEncodedField field = singleFieldValue.getField().lookupOnClass(holder); + if (field != null && field.isEnum()) { + DexClass otherHolder = appView.definitionForHolder(otherSingleFieldValue.getField()); + DexEncodedField otherField = + otherSingleFieldValue.getField().lookupOnClass(otherHolder); + if (otherField != null && otherField.isEnum()) { + simplifyIfWithKnownCondition(code, block, theIf, 1); + return true; + } + } + } + } + } + + return false; + } + + private void simplifyIfWithKnownCondition( + IRCode code, BasicBlock block, If theIf, BasicBlock target) { + BasicBlock deadTarget = + target == theIf.getTrueTarget() ? theIf.fallthroughBlock() : theIf.getTrueTarget(); + rewriteIfToGoto(code, block, theIf, target, deadTarget); + } + + private void simplifyIfWithKnownCondition(IRCode code, BasicBlock block, If theIf, int cond) { + simplifyIfWithKnownCondition(code, block, theIf, theIf.targetFromCondition(cond)); + } + + /* Identify simple diamond shapes converting boolean true/false to 1/0. We consider the forms: + * + * (1) + * + * [dbg pos x] [dbg pos x] + * ifeqz booleanValue ifnez booleanValue + * / \ / \ + * [dbg pos x][dbg pos x] [dbg pos x][dbg pos x] + * [const 0] [const 1] [const 1] [const 0] + * goto goto goto goto + * \ / \ / + * phi(0, 1) phi(1, 0) + * + * which can be replaced by a fallthrough and the phi value can be replaced + * with the boolean value itself. + * + * (2) + * + * [dbg pos x] [dbg pos x] + * ifeqz booleanValue ifnez booleanValue + * / \ / \ + * [dbg pos x][dbg pos x] [dbg pos x][dbg pos x] + * [const 1] [const 0] [const 0] [const 1] + * goto goto goto goto + * \ / \ / + * phi(1, 0) phi(0, 1) + * + * which can be replaced by a fallthrough and the phi value can be replaced + * by an xor instruction which is smaller. + */ + private boolean simplifyKnownBooleanCondition(IRCode code, BasicBlock block) { + If theIf = block.exit().asIf(); + Value testValue = theIf.inValues().get(0); + if (theIf.isZeroTest() && testValue.knownToBeBoolean()) { + BasicBlock trueBlock = theIf.getTrueTarget(); + BasicBlock falseBlock = theIf.fallthroughBlock(); + if (isBlockSupportedBySimplifyKnownBooleanCondition(trueBlock) + && isBlockSupportedBySimplifyKnownBooleanCondition(falseBlock) + && trueBlock.getSuccessors().get(0) == falseBlock.getSuccessors().get(0)) { + BasicBlock targetBlock = trueBlock.getSuccessors().get(0); + if (targetBlock.getPredecessors().size() == 2) { + int trueIndex = targetBlock.getPredecessors().indexOf(trueBlock); + int falseIndex = trueIndex == 0 ? 1 : 0; + int deadPhis = 0; + // Locate the phis that have the same value as the boolean and replace them + // by the boolean in all users. + for (Phi phi : targetBlock.getPhis()) { + Value trueValue = phi.getOperand(trueIndex); + Value falseValue = phi.getOperand(falseIndex); + if (trueValue.isConstNumber() && falseValue.isConstNumber()) { + ConstNumber trueNumber = trueValue.getConstInstruction().asConstNumber(); + ConstNumber falseNumber = falseValue.getConstInstruction().asConstNumber(); + if ((theIf.getType() == IfType.EQ + && trueNumber.isIntegerZero() + && falseNumber.isIntegerOne()) + || (theIf.getType() == IfType.NE + && trueNumber.isIntegerOne() + && falseNumber.isIntegerZero())) { + phi.replaceUsers(testValue); + deadPhis++; + } else if ((theIf.getType() == IfType.NE + && trueNumber.isIntegerZero() + && falseNumber.isIntegerOne()) + || (theIf.getType() == IfType.EQ + && trueNumber.isIntegerOne() + && falseNumber.isIntegerZero())) { + Value newOutValue = code.createValue(phi.getType(), phi.getLocalInfo()); + ConstNumber cstToUse = trueNumber.isIntegerOne() ? trueNumber : falseNumber; + BasicBlock phiBlock = phi.getBlock(); + Position phiPosition = phiBlock.getPosition(); + int insertIndex = 0; + if (cstToUse.getBlock() == trueBlock || cstToUse.getBlock() == falseBlock) { + // The constant belongs to the block to remove, create a new one. + cstToUse = ConstNumber.copyOf(code, cstToUse); + cstToUse.setBlock(phiBlock); + cstToUse.setPosition(phiPosition); + phiBlock.getInstructions().add(insertIndex++, cstToUse); + } + phi.replaceUsers(newOutValue); + Instruction newInstruction = + Xor.create(NumericType.INT, newOutValue, testValue, cstToUse.outValue()); + newInstruction.setBlock(phiBlock); + // The xor is replacing a phi so it does not have an actual position. + newInstruction.setPosition(phiPosition); + phiBlock.listIterator(code, insertIndex).add(newInstruction); + deadPhis++; + } + } + } + // If all phis were removed, there is no need for the diamond shape anymore + // and it can be rewritten to a goto to one of the branches. + if (deadPhis == targetBlock.getPhis().size()) { + rewriteIfToGoto(code, block, theIf, trueBlock, falseBlock); + return true; + } + return deadPhis > 0; + } + } + } + return false; + } + + private boolean isBlockSupportedBySimplifyKnownBooleanCondition(BasicBlock b) { + if (b.isTrivialGoto()) { + return true; + } + + int instructionSize = b.getInstructions().size(); + if (b.exit().isGoto() && (instructionSize == 2 || instructionSize == 3)) { + Instruction constInstruction = b.getInstructions().get(instructionSize - 2); + if (constInstruction.isConstNumber()) { + if (!constInstruction.asConstNumber().isIntegerOne() + && !constInstruction.asConstNumber().isIntegerZero()) { + return false; + } + if (instructionSize == 2) { + return true; + } + Instruction firstInstruction = b.getInstructions().getFirst(); + if (firstInstruction.isDebugPosition()) { + assert b.getPredecessors().size() == 1; + BasicBlock predecessorBlock = b.getPredecessors().get(0); + InstructionIterator it = predecessorBlock.iterator(predecessorBlock.exit()); + Instruction previousPosition = null; + while (it.hasPrevious() && !(previousPosition = it.previous()).isDebugPosition()) { + // Intentionally empty. + } + if (previousPosition != null) { + return previousPosition.getPosition() == firstInstruction.getPosition(); + } + } + } + } + + return false; + } + + private void rewriteIfToGoto( + IRCode code, BasicBlock block, If theIf, BasicBlock target, BasicBlock deadTarget) { + deadTarget.unlinkSinglePredecessorSiblingsAllowed(); + assert theIf == block.exit(); + block.replaceLastInstruction(new Goto(), code); + assert block.exit().isGoto(); + assert block.exit().asGoto().getTarget() == target; + } + + private boolean rewriteIfWithConstZero(IRCode code, BasicBlock block) { + If theIf = block.exit().asIf(); + if (theIf.isZeroTest()) { + return false; + } + + Value leftValue = theIf.lhs(); + Value rightValue = theIf.rhs(); + if (leftValue.isConstNumber() || rightValue.isConstNumber()) { + if (leftValue.isConstNumber()) { + if (leftValue.getConstInstruction().asConstNumber().isZero()) { + If ifz = new If(theIf.getType().forSwappedOperands(), rightValue); + block.replaceLastInstruction(ifz, code); + assert block.exit() == ifz; + return true; + } + } else if (rightValue.getConstInstruction().asConstNumber().isZero()) { + If ifz = new If(theIf.getType(), leftValue); + block.replaceLastInstruction(ifz, code); + assert block.exit() == ifz; + return true; + } + } + + return false; + } + + private boolean flipIfBranchesIfNeeded(IRCode code, BasicBlock block) { + If theIf = block.exit().asIf(); + BasicBlock trueTarget = theIf.getTrueTarget(); + BasicBlock fallthrough = theIf.fallthroughBlock(); + assert trueTarget != fallthrough; + + if (!fallthrough.isSimpleAlwaysThrowingPath() || trueTarget.isSimpleAlwaysThrowingPath()) { + return false; + } + + // In case fall-through block always throws there is a good chance that it + // is created for error checks and 'trueTarget' represents most more common + // non-error case. Flipping the if in this case may result in faster code + // on older Android versions. + List<Value> inValues = theIf.inValues(); + If newIf = new If(theIf.getType().inverted(), inValues); + block.replaceLastInstruction(newIf, code); + block.swapSuccessors(trueTarget, fallthrough); + return true; + } + + private boolean rewriteSwitch(IRCode code) { + return rewriteSwitch(code, SwitchCaseAnalyzer.getInstance()); + } + + private boolean rewriteSwitch(IRCode code, SwitchCaseAnalyzer switchCaseAnalyzer) { + if (!options.isSwitchRewritingEnabled()) { + return false; + } + if (!code.metadata().mayHaveSwitch()) { + return false; + } + return rewriteSwitchFull(code, switchCaseAnalyzer); + } + + private boolean rewriteSwitchFull(IRCode code, SwitchCaseAnalyzer switchCaseAnalyzer) { + boolean needToRemoveUnreachableBlocks = false; + ListIterator<BasicBlock> blocksIterator = code.listIterator(); + while (blocksIterator.hasNext()) { + BasicBlock block = blocksIterator.next(); + InstructionListIterator iterator = block.listIterator(code); + while (iterator.hasNext()) { + Instruction instruction = iterator.next(); + if (instruction.isSwitch()) { + Switch theSwitch = instruction.asSwitch(); + if (options.testing.enableDeadSwitchCaseElimination) { + SwitchCaseEliminator eliminator = + removeUnnecessarySwitchCases(code, theSwitch, iterator, switchCaseAnalyzer); + if (eliminator.mayHaveIntroducedUnreachableBlocks()) { + needToRemoveUnreachableBlocks = true; + } + + iterator.previous(); + instruction = iterator.next(); + if (instruction.isGoto()) { + continue; + } + + assert instruction.isSwitch(); + theSwitch = instruction.asSwitch(); + } + if (theSwitch.isIntSwitch()) { + rewriteIntSwitch(code, blocksIterator, block, iterator, theSwitch.asIntSwitch()); + } + } + } + } + + // Rewriting of switches introduces new branching structure. It relies on critical edges + // being split on the way in but does not maintain this property. We therefore split + // critical edges at exit. + code.splitCriticalEdges(); + + Set<Value> affectedValues = + needToRemoveUnreachableBlocks ? code.removeUnreachableBlocks() : ImmutableSet.of(); + if (!affectedValues.isEmpty()) { + new TypeAnalysis(appView).narrowing(affectedValues); + } + assert code.isConsistentSSA(appView); + return !affectedValues.isEmpty(); + } + + public void rewriteSingleKeySwitchToIf( + IRCode code, BasicBlock block, InstructionListIterator iterator, IntSwitch theSwitch) { + // Rewrite the switch to an if. + int fallthroughBlockIndex = theSwitch.getFallthroughBlockIndex(); + int caseBlockIndex = theSwitch.targetBlockIndices()[0]; + if (fallthroughBlockIndex < caseBlockIndex) { + block.swapSuccessorsByIndex(fallthroughBlockIndex, caseBlockIndex); + } + If replacement; + if (theSwitch.isIntSwitch() && theSwitch.asIntSwitch().getFirstKey() == 0) { + replacement = new If(IfType.EQ, theSwitch.value()); + } else { + Instruction labelConst = theSwitch.materializeFirstKey(appView, code); + labelConst.setPosition(theSwitch.getPosition()); + iterator.previous(); + iterator.add(labelConst); + Instruction dummy = iterator.next(); + assert dummy == theSwitch; + replacement = new If(IfType.EQ, ImmutableList.of(theSwitch.value(), labelConst.outValue())); + } + iterator.replaceCurrentInstruction(replacement); + } + + private void rewriteIntSwitch( + IRCode code, + ListIterator<BasicBlock> blockIterator, + BasicBlock block, + InstructionListIterator iterator, + IntSwitch theSwitch) { + if (disableSwitchToIfRewritingForClassIdComparisons(theSwitch)) { + return; + } + + if (theSwitch.numberOfKeys() == 1) { + rewriteSingleKeySwitchToIf(code, block, iterator, theSwitch); + return; + } + + // If there are more than 1 key, we use the following algorithm to find keys to combine. + // First, scan through the keys forward and combine each packed interval with the + // previous interval if it gives a net saving. + // Secondly, go through all created intervals and combine the ones without a saving into + // a single interval and keep a max number of packed switches. + // Finally, go through all intervals and check if the switch or part of the switch + // should be transformed to ifs. + + // Phase 1: Combine packed intervals. + InternalOutputMode mode = options.getInternalOutputMode(); + int[] keys = theSwitch.getKeys(); + int maxNumberOfIfsOrSwitches = 10; + PriorityQueue<Interval> biggestPackedSavings = + new PriorityQueue<>((x, y) -> Long.compare(y.packedSavings(mode), x.packedSavings(mode))); + Set<Interval> biggestPackedSet = new HashSet<>(); + List<Interval> intervals = new ArrayList<>(); + int previousKey = keys[0]; + IntList currentKeys = new IntArrayList(); + currentKeys.add(previousKey); + Interval previousInterval = null; + for (int i = 1; i < keys.length; i++) { + int key = keys[i]; + if (((long) key - (long) previousKey) > 1) { + Interval current = new Interval(currentKeys); + Interval added = combineOrAddInterval(intervals, previousInterval, current); + if (added != current && biggestPackedSet.contains(previousInterval)) { + biggestPackedSet.remove(previousInterval); + biggestPackedSavings.remove(previousInterval); + } + tryAddToBiggestSavings( + biggestPackedSet, biggestPackedSavings, added, maxNumberOfIfsOrSwitches); + previousInterval = added; + currentKeys = new IntArrayList(); + } + currentKeys.add(key); + previousKey = key; + } + Interval current = new Interval(currentKeys); + Interval added = combineOrAddInterval(intervals, previousInterval, current); + if (added != current && biggestPackedSet.contains(previousInterval)) { + biggestPackedSet.remove(previousInterval); + biggestPackedSavings.remove(previousInterval); + } + tryAddToBiggestSavings(biggestPackedSet, biggestPackedSavings, added, maxNumberOfIfsOrSwitches); + + // Phase 2: combine sparse intervals into a single bin. + // Check if we should save a space for a sparse switch, if so, remove the switch with + // the smallest savings. + if (biggestPackedSet.size() == maxNumberOfIfsOrSwitches + && maxNumberOfIfsOrSwitches < intervals.size()) { + biggestPackedSet.remove(biggestPackedSavings.poll()); + } + Interval sparse = null; + List<Interval> newSwitches = new ArrayList<>(maxNumberOfIfsOrSwitches); + for (int i = 0; i < intervals.size(); i++) { + Interval interval = intervals.get(i); + if (biggestPackedSet.contains(interval)) { + newSwitches.add(interval); + } else if (sparse == null) { + sparse = interval; + newSwitches.add(sparse); + } else { + sparse.addInterval(interval); + } + } + + // Phase 3: at this point we are guaranteed to have the biggest saving switches + // in newIntervals, potentially with a switch combining the remaining intervals. + // Now we check to see if we can create any if's to reduce size. + IntList outliers = new IntArrayList(); + int outliersAsIfSize = + options.testing.enableSwitchToIfRewriting + ? findIfsForCandidates(newSwitches, theSwitch, outliers) + : 0; + + long newSwitchesSize = 0; + List<IntList> newSwitchSequences = new ArrayList<>(newSwitches.size()); + for (Interval interval : newSwitches) { + newSwitchesSize += interval.estimatedSize(mode); + newSwitchSequences.add(interval.keys); + } + + long currentSize = IntSwitch.estimatedSize(mode, theSwitch.getKeys()); + if (newSwitchesSize + outliersAsIfSize + codeUnitMargin() < currentSize) { + convertSwitchToSwitchAndIfs( + code, blockIterator, block, iterator, theSwitch, newSwitchSequences, outliers); + } + } + + // TODO(b/181732463): We currently disable switch-to-if rewritings for switches on $r8$classId + // field values (from horizontal class merging. See bug for more details. + private boolean disableSwitchToIfRewritingForClassIdComparisons(IntSwitch theSwitch) { + Value switchValue = theSwitch.value().getAliasedValue(); + if (!switchValue.isDefinedByInstructionSatisfying(Instruction::isInstanceGet)) { + return false; + } + AppInfoWithLiveness appInfo = appView.appInfoWithLiveness(); + if (appInfo == null) { + return false; + } + InstanceGet instanceGet = switchValue.getDefinition().asInstanceGet(); + SingleProgramFieldResolutionResult resolutionResult = + appInfo.resolveField(instanceGet.getField()).asSingleProgramFieldResolutionResult(); + if (resolutionResult == null) { + return false; + } + DexEncodedField resolvedField = resolutionResult.getResolvedField(); + return HorizontalClassMergerUtils.isClassIdField(appView, resolvedField); + } + + private SwitchCaseEliminator removeUnnecessarySwitchCases( + IRCode code, + Switch theSwitch, + InstructionListIterator iterator, + SwitchCaseAnalyzer switchCaseAnalyzer) { + BasicBlock defaultTarget = theSwitch.fallthroughBlock(); + SwitchCaseEliminator eliminator = new SwitchCaseEliminator(theSwitch, iterator); + BasicBlockBehavioralSubsumption behavioralSubsumption = + new BasicBlockBehavioralSubsumption(appView, code); + + // Compute the set of switch cases that can be removed. + boolean hasSwitchCaseToDefaultRewrite = false; + AbstractValue switchAbstractValue = theSwitch.value().getAbstractValue(appView, code.context()); + for (int i = 0; i < theSwitch.numberOfKeys(); i++) { + BasicBlock targetBlock = theSwitch.targetBlock(i); + + if (switchCaseAnalyzer.switchCaseIsAlwaysHit(theSwitch, i)) { + eliminator.markSwitchCaseAsAlwaysHit(i); + break; + } + + // This switch case can be removed if the behavior of the target block is equivalent to the + // behavior of the default block, or if the switch case is unreachable. + if (switchCaseAnalyzer.switchCaseIsUnreachable(theSwitch, switchAbstractValue, i)) { + eliminator.markSwitchCaseForRemoval(i); + } else if (behavioralSubsumption.isSubsumedBy( + theSwitch.value(), targetBlock, defaultTarget)) { + eliminator.markSwitchCaseForRemoval(i); + hasSwitchCaseToDefaultRewrite = true; + } + } + + if (eliminator.isFallthroughLive() + && !hasSwitchCaseToDefaultRewrite + && switchCaseAnalyzer.switchFallthroughIsNeverHit(theSwitch, switchAbstractValue)) { + eliminator.markSwitchFallthroughAsNeverHit(); + } + + eliminator.optimize(); + return eliminator; + } + + private static class Interval { + + private final IntList keys = new IntArrayList(); + + public Interval(IntList... allKeys) { + assert allKeys.length > 0; + for (IntList keys : allKeys) { + assert keys.size() > 0; + this.keys.addAll(keys); + } + } + + public int getMin() { + return keys.getInt(0); + } + + public int getMax() { + return keys.getInt(keys.size() - 1); + } + + public void addInterval(Interval other) { + assert getMax() < other.getMin(); + keys.addAll(other.keys); + } + + public long packedSavings(InternalOutputMode mode) { + long packedTargets = (long) getMax() - (long) getMin() + 1; + if (!IntSwitch.canBePacked(mode, packedTargets)) { + return Long.MIN_VALUE + 1; + } + long sparseCost = + IntSwitch.baseSparseSize(mode) + IntSwitch.sparsePayloadSize(mode, keys.size()); + long packedCost = + IntSwitch.basePackedSize(mode) + IntSwitch.packedPayloadSize(mode, packedTargets); + return sparseCost - packedCost; + } + + public long estimatedSize(InternalOutputMode mode) { + return IntSwitch.estimatedSize(mode, keys.toIntArray()); + } + } + + private Interval combineOrAddInterval( + List<Interval> intervals, Interval previous, Interval current) { + // As a first iteration, we only combine intervals if their packed size is less than their + // sparse counterpart. In CF we will have to add a load and a jump which add to the + // stack map table (1 is the size of a same entry). + InternalOutputMode mode = options.getInternalOutputMode(); + int penalty = mode.isGeneratingClassFiles() ? 3 + 1 : 0; + if (previous == null) { + intervals.add(current); + return current; + } + Interval combined = new Interval(previous.keys, current.keys); + long packedSavings = combined.packedSavings(mode); + if (packedSavings <= 0 + || packedSavings < previous.estimatedSize(mode) + current.estimatedSize(mode) - penalty) { + intervals.add(current); + return current; + } else { + intervals.set(intervals.size() - 1, combined); + return combined; + } + } + + private void tryAddToBiggestSavings( + Set<Interval> biggestPackedSet, + PriorityQueue<Interval> intervals, + Interval toAdd, + int maximumNumberOfSwitches) { + assert !biggestPackedSet.contains(toAdd); + long savings = toAdd.packedSavings(options.getInternalOutputMode()); + if (savings <= 0) { + return; + } + if (intervals.size() < maximumNumberOfSwitches) { + intervals.add(toAdd); + biggestPackedSet.add(toAdd); + } else if (savings > intervals.peek().packedSavings(options.getInternalOutputMode())) { + intervals.add(toAdd); + biggestPackedSet.add(toAdd); + biggestPackedSet.remove(intervals.poll()); + } + } + + private int codeUnitMargin() { + return options.getInternalOutputMode().isGeneratingClassFiles() ? 3 : 1; + } + + private int findIfsForCandidates( + List<Interval> newSwitches, IntSwitch theSwitch, IntList outliers) { + Set<Interval> switchesToRemove = new HashSet<>(); + InternalOutputMode mode = options.getInternalOutputMode(); + int outliersAsIfSize = 0; + // The candidateForIfs is either an index to a switch that can be eliminated totally or a sparse + // where removing a key may produce a greater saving. It is only if keys are small in the packed + // switch that removing the keys makes sense (size wise). + for (Interval candidate : newSwitches) { + int maxIfBudget = 10; + long switchSize = candidate.estimatedSize(mode); + int sizeOfAllKeysAsIf = sizeForKeysWrittenAsIfs(theSwitch.value().outType(), candidate.keys); + if (candidate.keys.size() <= maxIfBudget + && sizeOfAllKeysAsIf < switchSize - codeUnitMargin()) { + outliersAsIfSize += sizeOfAllKeysAsIf; + switchesToRemove.add(candidate); + outliers.addAll(candidate.keys); + continue; + } + // One could do something clever here, but we use a simple algorithm that use the fact that + // all keys are sorted in ascending order and that the smallest absolute value will give the + // best saving. + IntList candidateKeys = candidate.keys; + int smallestPosition = -1; + long smallest = Long.MAX_VALUE; + for (int i = 0; i < candidateKeys.size(); i++) { + long current = Math.abs((long) candidateKeys.getInt(i)); + if (current < smallest) { + smallestPosition = i; + smallest = current; + } + } + // Add as many keys forward and backward as we have budget and we decrease in size. + IntList ifKeys = new IntArrayList(); + ifKeys.add(candidateKeys.getInt(smallestPosition)); + long previousSavings = 0; + long currentSavings = + switchSize + - sizeForKeysWrittenAsIfs(theSwitch.value().outType(), ifKeys) + - IntSwitch.estimatedSparseSize(mode, candidateKeys.size() - ifKeys.size()); + int minIndex = smallestPosition - 1; + int maxIndex = smallestPosition + 1; + while (ifKeys.size() < maxIfBudget && currentSavings > previousSavings) { + if (minIndex >= 0 && maxIndex < candidateKeys.size()) { + long valMin = Math.abs((long) candidateKeys.getInt(minIndex)); + int valMax = Math.abs(candidateKeys.getInt(maxIndex)); + if (valMax <= valMin) { + ifKeys.add(candidateKeys.getInt(maxIndex++)); + } else { + ifKeys.add(candidateKeys.getInt(minIndex--)); + } + } else if (minIndex >= 0) { + ifKeys.add(candidateKeys.getInt(minIndex--)); + } else if (maxIndex < candidateKeys.size()) { + ifKeys.add(candidateKeys.getInt(maxIndex++)); + } else { + // No more elements to add as if's. + break; + } + previousSavings = currentSavings; + currentSavings = + switchSize + - sizeForKeysWrittenAsIfs(theSwitch.value().outType(), ifKeys) + - IntSwitch.estimatedSparseSize(mode, candidateKeys.size() - ifKeys.size()); + } + if (previousSavings >= currentSavings) { + // Remove the last added key since it did not contribute to savings. + int lastKey = ifKeys.getInt(ifKeys.size() - 1); + ifKeys.removeInt(ifKeys.size() - 1); + if (lastKey == candidateKeys.getInt(minIndex + 1)) { + minIndex++; + } else { + maxIndex--; + } + } + // Adjust pointers into the candidate keys. + minIndex++; + maxIndex--; + if (ifKeys.size() > 0) { + int ifsSize = sizeForKeysWrittenAsIfs(theSwitch.value().outType(), ifKeys); + long newSwitchSize = + IntSwitch.estimatedSparseSize(mode, candidateKeys.size() - ifKeys.size()); + if (newSwitchSize + ifsSize + codeUnitMargin() < switchSize) { + candidateKeys.removeElements(minIndex, maxIndex); + outliers.addAll(ifKeys); + outliersAsIfSize += ifsSize; + } + } + } + newSwitches.removeAll(switchesToRemove); + return outliersAsIfSize; + } + + private int sizeForKeysWrittenAsIfs(ValueType type, Collection<Integer> keys) { + int ifsSize = If.estimatedSize(options.getInternalOutputMode()) * keys.size(); + // In Cf we also require a load as well (and a stack map entry) + if (options.getInternalOutputMode().isGeneratingClassFiles()) { + ifsSize += keys.size() * (3 + 1); + } + for (int k : keys) { + if (k != 0) { + ifsSize += ConstNumber.estimatedSize(options.getInternalOutputMode(), type, k); + } + } + return ifsSize; + } + + /** + * Covert the switch instruction to a sequence of if instructions checking for a specified set of + * keys, followed by a new switch with the remaining keys. + */ + // TODO(b/270398965): Replace LinkedList. + @SuppressWarnings("JdkObsolete") + public void convertSwitchToSwitchAndIfs( + IRCode code, + ListIterator<BasicBlock> blocksIterator, + BasicBlock originalBlock, + InstructionListIterator iterator, + IntSwitch theSwitch, + List<IntList> switches, + IntList keysToRemove) { + + Position position = theSwitch.getPosition(); + + // Extract the information from the switch before removing it. + Int2ReferenceSortedMap<BasicBlock> keyToTarget = theSwitch.getKeyToTargetMap(); + + // Keep track of the current fallthrough, starting with the original. + BasicBlock fallthroughBlock = theSwitch.fallthroughBlock(); + + // Split the switch instruction into its own block and remove it. + iterator.previous(); + BasicBlock originalSwitchBlock = iterator.split(code, blocksIterator); + assert !originalSwitchBlock.hasCatchHandlers(); + assert originalSwitchBlock.getInstructions().size() == 1; + assert originalBlock.exit().isGoto(); + theSwitch.moveDebugValues(originalBlock.exit()); + blocksIterator.remove(); + theSwitch.getBlock().detachAllSuccessors(); + BasicBlock block = theSwitch.getBlock().unlinkSinglePredecessor(); + assert theSwitch.getBlock().getPredecessors().size() == 0; + assert theSwitch.getBlock().getSuccessors().size() == 0; + assert block == originalBlock; + + // Collect the new blocks for adding to the block list. + LinkedList<BasicBlock> newBlocks = new LinkedList<>(); + + // Build the switch-blocks backwards, to always have the fallthrough block in hand. + for (int i = switches.size() - 1; i >= 0; i--) { + SwitchBuilder switchBuilder = new SwitchBuilder(position); + switchBuilder.setValue(theSwitch.value()); + IntList keys = switches.get(i); + for (int j = 0; j < keys.size(); j++) { + int key = keys.getInt(j); + switchBuilder.addKeyAndTarget(key, keyToTarget.get(key)); + } + switchBuilder.setFallthrough(fallthroughBlock).setBlockNumber(code.getNextBlockNumber()); + BasicBlock newSwitchBlock = switchBuilder.build(code.metadata()); + newBlocks.addFirst(newSwitchBlock); + fallthroughBlock = newSwitchBlock; + } + + // Build the if-blocks backwards, to always have the fallthrough block in hand. + for (int i = keysToRemove.size() - 1; i >= 0; i--) { + int key = keysToRemove.getInt(i); + BasicBlock peeledOffTarget = keyToTarget.get(key); + IfBuilder ifBuilder = new IfBuilder(position, code); + ifBuilder + .setLeft(theSwitch.value()) + .setRight(key) + .setTarget(peeledOffTarget) + .setFallthrough(fallthroughBlock) + .setBlockNumber(code.getNextBlockNumber()); + BasicBlock ifBlock = ifBuilder.build(); + newBlocks.addFirst(ifBlock); + fallthroughBlock = ifBlock; + } + + // Finally link the block before the original switch to the new block sequence. + originalBlock.link(fallthroughBlock); + + // Finally add the blocks. + newBlocks.forEach(blocksIterator::add); + } + + // TODO(sgjesse); Move this somewhere else, and reuse it for some of the other switch rewritings. + private abstract static class InstructionBuilder<T> { + + int blockNumber; + final Position position; + + InstructionBuilder(Position position) { + this.position = position; + } + + abstract T self(); + + T setBlockNumber(int blockNumber) { + this.blockNumber = blockNumber; + return self(); + } + } + + private static class SwitchBuilder extends InstructionBuilder<SwitchBuilder> { + + private Value value; + private final Int2ReferenceSortedMap<BasicBlock> keyToTarget = new Int2ReferenceAVLTreeMap<>(); + private BasicBlock fallthrough; + + SwitchBuilder(Position position) { + super(position); + } + + @Override + SwitchBuilder self() { + return this; + } + + SwitchBuilder setValue(Value value) { + this.value = value; + return this; + } + + SwitchBuilder addKeyAndTarget(int key, BasicBlock target) { + keyToTarget.put(key, target); + return this; + } + + SwitchBuilder setFallthrough(BasicBlock fallthrough) { + this.fallthrough = fallthrough; + return this; + } + + BasicBlock build(IRMetadata metadata) { + final int NOT_FOUND = -1; + Object2IntMap<BasicBlock> targetToSuccessorIndex = new Object2IntLinkedOpenHashMap<>(); + targetToSuccessorIndex.defaultReturnValue(NOT_FOUND); + + int[] keys = new int[keyToTarget.size()]; + int[] targetBlockIndices = new int[keyToTarget.size()]; + // Sort keys descending. + int count = 0; + IntIterator iter = keyToTarget.keySet().iterator(); + while (iter.hasNext()) { + int key = iter.nextInt(); + BasicBlock target = keyToTarget.get(key); + Integer targetIndex = + targetToSuccessorIndex.computeIfAbsent(target, b -> targetToSuccessorIndex.size()); + keys[count] = key; + targetBlockIndices[count] = targetIndex; + count++; + } + Integer fallthroughIndex = + targetToSuccessorIndex.computeIfAbsent(fallthrough, b -> targetToSuccessorIndex.size()); + IntSwitch newSwitch = new IntSwitch(value, keys, targetBlockIndices, fallthroughIndex); + newSwitch.setPosition(position); + BasicBlock newSwitchBlock = BasicBlock.createSwitchBlock(blockNumber, newSwitch, metadata); + for (BasicBlock successor : targetToSuccessorIndex.keySet()) { + newSwitchBlock.link(successor); + } + return newSwitchBlock; + } + } + + private static class IfBuilder extends InstructionBuilder<IfBuilder> { + + private final IRCode code; + private Value left; + private int right; + private BasicBlock target; + private BasicBlock fallthrough; + + IfBuilder(Position position, IRCode code) { + super(position); + this.code = code; + } + + @Override + IfBuilder self() { + return this; + } + + IfBuilder setLeft(Value left) { + this.left = left; + return this; + } + + IfBuilder setRight(int right) { + this.right = right; + return this; + } + + IfBuilder setTarget(BasicBlock target) { + this.target = target; + return this; + } + + IfBuilder setFallthrough(BasicBlock fallthrough) { + this.fallthrough = fallthrough; + return this; + } + + BasicBlock build() { + assert target != null; + assert fallthrough != null; + If newIf; + BasicBlock ifBlock; + if (right != 0) { + ConstNumber rightConst = code.createIntConstant(right); + rightConst.setPosition(position); + newIf = new If(IfType.EQ, ImmutableList.of(left, rightConst.dest())); + ifBlock = BasicBlock.createIfBlock(blockNumber, newIf, code.metadata(), rightConst); + } else { + newIf = new If(IfType.EQ, left); + ifBlock = BasicBlock.createIfBlock(blockNumber, newIf, code.metadata()); + } + newIf.setPosition(position); + ifBlock.link(target); + ifBlock.link(fallthrough); + return ifBlock; + } + } +}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/SwitchCaseEliminator.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/SwitchCaseEliminator.java similarity index 99% rename from src/main/java/com/android/tools/r8/ir/optimize/SwitchCaseEliminator.java rename to src/main/java/com/android/tools/r8/ir/conversion/passes/SwitchCaseEliminator.java index 11e0b51..cbda630 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/SwitchCaseEliminator.java +++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/SwitchCaseEliminator.java
@@ -2,7 +2,7 @@ // 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; +package com.android.tools.r8.ir.conversion.passes; import com.android.tools.r8.graph.DexString; import com.android.tools.r8.ir.code.BasicBlock;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialCheckCastAndInstanceOfRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialCheckCastAndInstanceOfRemover.java new file mode 100644 index 0000000..e19b963 --- /dev/null +++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialCheckCastAndInstanceOfRemover.java
@@ -0,0 +1,384 @@ +// Copyright (c) 2023, 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.passes; + +import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull; + +import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext; +import com.android.tools.r8.graph.AccessControl; +import com.android.tools.r8.graph.AppView; +import com.android.tools.r8.graph.DexClass; +import com.android.tools.r8.graph.DexItemFactory; +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.type.DynamicTypeWithUpperBound; +import com.android.tools.r8.ir.analysis.type.Nullability; +import com.android.tools.r8.ir.analysis.type.TypeAnalysis; +import com.android.tools.r8.ir.analysis.type.TypeElement; +import com.android.tools.r8.ir.analysis.type.TypeUtils; +import com.android.tools.r8.ir.code.CheckCast; +import com.android.tools.r8.ir.code.ConstNumber; +import com.android.tools.r8.ir.code.IRCode; +import com.android.tools.r8.ir.code.IRMetadata; +import com.android.tools.r8.ir.code.InstanceOf; +import com.android.tools.r8.ir.code.Instruction; +import com.android.tools.r8.ir.code.InstructionListIterator; +import com.android.tools.r8.ir.code.InvokeStatic; +import com.android.tools.r8.ir.code.Value; +import com.android.tools.r8.ir.conversion.MethodProcessor; +import com.android.tools.r8.ir.optimize.CodeRewriter; +import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations; +import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations.UtilityMethodForCodeOptimizations; +import com.android.tools.r8.shaking.AppInfoWithLiveness; +import com.android.tools.r8.utils.InternalOptions; +import com.google.common.collect.Sets; +import java.util.Set; + +public class TrivialCheckCastAndInstanceOfRemover { + + private final AppView<?> appView; + private final InternalOptions options; + private final DexItemFactory dexItemFactory; + + public TrivialCheckCastAndInstanceOfRemover(AppView<?> appView) { + this.appView = appView; + this.options = appView.options(); + this.dexItemFactory = appView.dexItemFactory(); + } + + public void run( + IRCode code, + ProgramMethod context, + MethodProcessor methodProcessor, + MethodProcessingContext methodProcessingContext) { + if (!appView.enableWholeProgramOptimizations()) { + return; + } + + assert appView.appInfo().hasLiveness(); + AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness(); + + if (!appView.options().testing.enableCheckCastAndInstanceOfRemoval) { + return; + } + + IRMetadata metadata = code.metadata(); + if (!metadata.mayHaveCheckCast() && !metadata.mayHaveInstanceOf()) { + return; + } + + // If we can remove a CheckCast it is due to us having at least as much information about the + // type as the CheckCast gives. We then need to propagate that information to the users of + // the CheckCast to ensure further optimizations and removals of CheckCast: + // + // : 1: NewArrayEmpty v2 <- v1(1) java.lang.String[] <-- v2 = String[] + // ... + // : 2: CheckCast v5 <- v2; java.lang.Object[] <-- v5 = Object[] + // ... + // : 3: ArrayGet v7 <- v5, v6(0) <-- v7 = Object + // : 4: CheckCast v8 <- v7; java.lang.String <-- v8 = String + // ... + // + // When looking at line 2 we can conclude that the CheckCast is trivial because v2 is String[] + // and remove it. However, v7 is still only known to be Object and we cannot remove the + // CheckCast at line 4 unless we update v7 with the most precise information by narrowing the + // affected values of v5. We therefore have to run the type analysis after each CheckCast + // removal. + TypeAnalysis typeAnalysis = new TypeAnalysis(appView); + Set<Value> affectedValues = Sets.newIdentityHashSet(); + InstructionListIterator it = code.instructionListIterator(); + boolean needToRemoveTrivialPhis = false; + while (it.hasNext()) { + Instruction current = it.next(); + if (current.isCheckCast()) { + boolean hasPhiUsers = current.outValue().hasPhiUsers(); + RemoveCheckCastInstructionIfTrivialResult removeResult = + removeCheckCastInstructionIfTrivial( + appViewWithLiveness, + current.asCheckCast(), + it, + code, + context, + affectedValues, + methodProcessor, + methodProcessingContext); + if (removeResult != RemoveCheckCastInstructionIfTrivialResult.NO_REMOVALS) { + assert removeResult == RemoveCheckCastInstructionIfTrivialResult.REMOVED_CAST_DO_NARROW; + needToRemoveTrivialPhis |= hasPhiUsers; + typeAnalysis.narrowing(affectedValues); + affectedValues.clear(); + } + } else if (current.isInstanceOf()) { + boolean hasPhiUsers = current.outValue().hasPhiUsers(); + if (removeInstanceOfInstructionIfTrivial( + appViewWithLiveness, current.asInstanceOf(), it, code)) { + needToRemoveTrivialPhis |= hasPhiUsers; + } + } + } + // ... v1 + // ... + // v2 <- check-cast v1, T + // v3 <- phi(v1, v2) + // Removing check-cast may result in a trivial phi: + // v3 <- phi(v1, v1) + if (needToRemoveTrivialPhis) { + code.removeAllDeadAndTrivialPhis(affectedValues); + if (!affectedValues.isEmpty()) { + typeAnalysis.narrowing(affectedValues); + } + } + assert code.isConsistentSSA(appView); + } + + enum RemoveCheckCastInstructionIfTrivialResult { + NO_REMOVALS, + REMOVED_CAST_DO_NARROW + } + + private enum InstanceOfResult { + UNKNOWN, + TRUE, + FALSE + } + + // Returns true if the given check-cast instruction was removed. + private RemoveCheckCastInstructionIfTrivialResult removeCheckCastInstructionIfTrivial( + AppView<AppInfoWithLiveness> appViewWithLiveness, + CheckCast checkCast, + InstructionListIterator it, + IRCode code, + ProgramMethod context, + Set<Value> affectedValues, + MethodProcessor methodProcessor, + MethodProcessingContext methodProcessingContext) { + Value inValue = checkCast.object(); + Value outValue = checkCast.outValue(); + DexType castType = checkCast.getType(); + DexType baseCastType = castType.toBaseType(dexItemFactory); + + // If the cast type is not accessible in the current context, we should not remove the cast + // in order to preserve runtime errors. Note that JVM and ART behave differently: see + // {@link com.android.tools.r8.ir.optimize.checkcast.IllegalAccessErrorTest}. + if (baseCastType.isClassType()) { + DexClass baseCastClass = appView.definitionFor(baseCastType); + if (baseCastClass == null + || AccessControl.isClassAccessible(baseCastClass, code.context(), appViewWithLiveness) + .isPossiblyFalse()) { + return RemoveCheckCastInstructionIfTrivialResult.NO_REMOVALS; + } + } + + if (!appView + .getOpenClosedInterfacesCollection() + .isDefinitelyInstanceOfStaticType(appViewWithLiveness, inValue)) { + return RemoveCheckCastInstructionIfTrivialResult.NO_REMOVALS; + } + + // If the in-value is `null` and the cast-type is a float-array type, then trivial check-cast + // elimination may lead to verification errors. See b/123269162. + if (options.canHaveArtCheckCastVerifierBug()) { + if (inValue.getType().isNullType() + && castType.isArrayType() + && castType.toBaseType(dexItemFactory).isFloatType()) { + return RemoveCheckCastInstructionIfTrivialResult.NO_REMOVALS; + } + } + + // If casting to an array of an interface type elimination may lead to verification errors. + // See b/132420510 and b/223424356. + if (options.canHaveIncorrectJoinForArrayOfInterfacesBug()) { + if (castType.isArrayType()) { + DexType baseType = castType.toBaseType(dexItemFactory); + if (baseType.isClassType() && baseType.isInterface(appViewWithLiveness)) { + return RemoveCheckCastInstructionIfTrivialResult.NO_REMOVALS; + } + } + } + + TypeElement inTypeLattice = inValue.getType(); + TypeElement outTypeLattice = outValue.getType(); + TypeElement castTypeLattice = castType.toTypeElement(appView, inTypeLattice.nullability()); + + assert inTypeLattice.nullability().lessThanOrEqual(outTypeLattice.nullability()); + + if (inTypeLattice.lessThanOrEqual(castTypeLattice, appView)) { + // 1) Trivial cast. + // A a = ... + // A a' = (A) a; + // 2) Up-cast: we already have finer type info. + // A < B + // A a = ... + // B b = (B) a; + assert inTypeLattice.lessThanOrEqual(outTypeLattice, appView); + // The removeOrReplaceByDebugLocalWrite will propagate the incoming value for the CheckCast + // to the users of the CheckCast's out value. + // + // v2 = CheckCast A v1 ~~> DebugLocalWrite $v0 <- v1 + // + // The DebugLocalWrite is not a user of the outvalue, we therefore have to wait and take the + // CheckCast invalue users that includes the potential DebugLocalWrite. + CodeRewriter.removeOrReplaceByDebugLocalWrite(checkCast, it, inValue, outValue); + affectedValues.addAll(inValue.affectedValues()); + return RemoveCheckCastInstructionIfTrivialResult.REMOVED_CAST_DO_NARROW; + } + + // If values of cast type are guaranteed to be null, then the out-value must be null if the cast + // succeeds. After removing all usages of the out-value, the check-cast instruction is replaced + // by a call to throwClassCastExceptionIfNotNull() to allow dead code elimination of the cast + // type. + if (castType.isClassType() + && castType.isAlwaysNull(appViewWithLiveness) + && !outValue.hasDebugUsers()) { + // Replace all usages of the out-value by null. + it.previous(); + Value nullValue = it.insertConstNullInstruction(code, options); + it.next(); + checkCast.outValue().replaceUsers(nullValue); + affectedValues.addAll(nullValue.affectedValues()); + + // Replace the check-cast instruction by throwClassCastExceptionIfNotNull(). + UtilityMethodForCodeOptimizations throwClassCastExceptionIfNotNullMethod = + UtilityMethodsForCodeOptimizations.synthesizeThrowClassCastExceptionIfNotNullMethod( + appView, methodProcessor.getEventConsumer(), methodProcessingContext); + throwClassCastExceptionIfNotNullMethod.optimize(methodProcessor); + InvokeStatic replacement = + InvokeStatic.builder() + .setMethod(throwClassCastExceptionIfNotNullMethod.getMethod()) + .setSingleArgument(checkCast.object()) + .setPosition(checkCast) + .build(); + it.replaceCurrentInstruction(replacement); + assert replacement.lookupSingleTarget(appView, context) != null; + return RemoveCheckCastInstructionIfTrivialResult.REMOVED_CAST_DO_NARROW; + } + + // If the cast is guaranteed to succeed and only there to ensure the program type checks, then + // check if the program would still type check after removing the cast. + if (checkCast.isSafeCheckCast() + || checkCast + .getFirstOperand() + .getDynamicType(appViewWithLiveness) + .getDynamicUpperBoundType() + .lessThanOrEqualUpToNullability(castTypeLattice, appView)) { + TypeElement useType = + TypeUtils.computeUseType(appViewWithLiveness, context, checkCast.outValue()); + if (inTypeLattice.lessThanOrEqualUpToNullability(useType, appView)) { + return RemoveCheckCastInstructionIfTrivialResult.REMOVED_CAST_DO_NARROW; + } + } + + // Otherwise, keep the checkcast to preserve verification errors. E.g., down-cast: + // A < B < C + // c = ... // Even though we know c is of type A, + // a' = (B) c; // (this could be removed, since chained below.) + // a'' = (A) a'; // this should remain for runtime verification. + assert !inTypeLattice.isDefinitelyNull() || (inValue.isPhi() && !inTypeLattice.isNullType()); + assert outTypeLattice.equalUpToNullability(castTypeLattice); + return RemoveCheckCastInstructionIfTrivialResult.NO_REMOVALS; + } + + // Returns true if the given instance-of instruction was removed. + private boolean removeInstanceOfInstructionIfTrivial( + AppView<AppInfoWithLiveness> appViewWithLiveness, + InstanceOf instanceOf, + InstructionListIterator it, + IRCode code) { + ProgramMethod context = code.context(); + + // If the instance-of type is not accessible in the current context, we should not remove the + // instance-of instruction in order to preserve IllegalAccessError. + DexType instanceOfBaseType = instanceOf.type().toBaseType(dexItemFactory); + if (instanceOfBaseType.isClassType()) { + DexClass instanceOfClass = appView.definitionFor(instanceOfBaseType); + if (instanceOfClass == null + || AccessControl.isClassAccessible(instanceOfClass, context, appViewWithLiveness) + .isPossiblyFalse()) { + return false; + } + } + + Value inValue = instanceOf.value(); + if (!appView + .getOpenClosedInterfacesCollection() + .isDefinitelyInstanceOfStaticType(appViewWithLiveness, inValue)) { + return false; + } + + TypeElement inType = inValue.getType(); + TypeElement instanceOfType = + TypeElement.fromDexType(instanceOf.type(), inType.nullability(), appView); + Value aliasValue = inValue.getAliasedValue(); + + InstanceOfResult result = InstanceOfResult.UNKNOWN; + if (inType.isDefinitelyNull()) { + result = InstanceOfResult.FALSE; + } else if (inType.lessThanOrEqual(instanceOfType, appView) && !inType.isNullable()) { + result = InstanceOfResult.TRUE; + } else if (!aliasValue.isPhi() + && aliasValue.definition.isCreatingInstanceOrArray() + && instanceOfType.strictlyLessThan(inType, appView)) { + result = InstanceOfResult.FALSE; + } else if (appView.appInfo().hasLiveness()) { + if (instanceOf.type().isClassType() + && isNeverInstantiatedDirectlyOrIndirectly(instanceOf.type())) { + // The type of the instance-of instruction is a program class, and is never instantiated + // directly or indirectly. Thus, the in-value must be null, meaning that the instance-of + // instruction will always evaluate to false. + result = InstanceOfResult.FALSE; + } + + if (result == InstanceOfResult.UNKNOWN) { + if (inType.isClassType() + && isNeverInstantiatedDirectlyOrIndirectly(inType.asClassType().getClassType())) { + // The type of the in-value is a program class, and is never instantiated directly or + // indirectly. This, the in-value must be null, meaning that the instance-of instruction + // will always evaluate to false. + result = InstanceOfResult.FALSE; + } + } + + if (result == InstanceOfResult.UNKNOWN) { + Value aliasedValue = + inValue.getSpecificAliasedValue( + value -> + value.isDefinedByInstructionSatisfying( + Instruction::isAssumeWithDynamicTypeAssumption)); + if (aliasedValue != null) { + DynamicTypeWithUpperBound dynamicType = + aliasedValue.getDefinition().asAssume().getDynamicTypeAssumption().getDynamicType(); + Nullability nullability = dynamicType.getNullability(); + if (nullability.isDefinitelyNull()) { + result = InstanceOfResult.FALSE; + } else if (dynamicType.getDynamicUpperBoundType().lessThanOrEqual(instanceOfType, appView) + && (!inType.isNullable() || !nullability.isNullable())) { + result = InstanceOfResult.TRUE; + } + } + } + } + if (result != InstanceOfResult.UNKNOWN) { + ConstNumber newInstruction = + new ConstNumber( + new Value( + code.valueNumberGenerator.next(), + TypeElement.getInt(), + instanceOf.outValue().getLocalInfo()), + result == InstanceOfResult.TRUE ? 1 : 0); + it.replaceCurrentInstruction(newInstruction); + return true; + } + return false; + } + + private boolean isNeverInstantiatedDirectlyOrIndirectly(DexType type) { + assert appView.appInfo().hasLiveness(); + assert type.isClassType(); + DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(type)); + return clazz != null + && !appView.appInfo().withLiveness().isInstantiatedDirectlyOrIndirectly(clazz); + } +}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialGotosCollapser.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialGotosCollapser.java new file mode 100644 index 0000000..7c9b4d0 --- /dev/null +++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialGotosCollapser.java
@@ -0,0 +1,204 @@ +// Copyright (c) 2023, 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.passes; + +import com.android.tools.r8.graph.AppInfo; +import com.android.tools.r8.graph.AppView; +import com.android.tools.r8.graph.ProgramMethod; +import com.android.tools.r8.ir.code.BasicBlock; +import com.android.tools.r8.ir.code.IRCode; +import com.android.tools.r8.ir.code.If; +import com.android.tools.r8.ir.code.Switch; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.ListIterator; +import java.util.Set; + +/** + * Rewrite all branch targets to the destination of trivial goto chains when possible. Does not + * rewrite fallthrough targets as that would require block reordering and the transformation only + * makes sense after SSA destruction where there are no phis. + */ +public class TrivialGotosCollapser extends CodeRewriterPass<AppInfo> { + + public TrivialGotosCollapser(AppView<?> appView) { + super(appView); + } + + @Override + String getTimingId() { + return "TrivialGotosCollapser"; + } + + @Override + void rewriteCode(ProgramMethod method, IRCode code) { + assert code.isConsistentGraph(appView); + List<BasicBlock> blocksToRemove = new ArrayList<>(); + // Rewrite all non-fallthrough targets to the end of trivial goto chains and remove + // first round of trivial goto blocks. + ListIterator<BasicBlock> iterator = code.listIterator(); + assert iterator.hasNext(); + BasicBlock block = iterator.next(); + BasicBlock nextBlock; + + do { + nextBlock = iterator.hasNext() ? iterator.next() : null; + if (block.isTrivialGoto()) { + collapseTrivialGoto(code, block, nextBlock, blocksToRemove); + } + if (block.exit().isIf()) { + collapseIfTrueTarget(block); + } + if (block.exit().isSwitch()) { + collapseNonFallthroughSwitchTargets(block); + } + block = nextBlock; + } while (nextBlock != null); + code.removeBlocks(blocksToRemove); + // Get rid of gotos to the next block. + while (!blocksToRemove.isEmpty()) { + blocksToRemove = new ArrayList<>(); + iterator = code.listIterator(); + block = iterator.next(); + do { + nextBlock = iterator.hasNext() ? iterator.next() : null; + if (block.isTrivialGoto()) { + collapseTrivialGoto(code, block, nextBlock, blocksToRemove); + } + block = nextBlock; + } while (block != null); + code.removeBlocks(blocksToRemove); + } + assert removedTrivialGotos(code); + assert code.isConsistentGraph(appView); + } + + @Override + boolean shouldRewriteCode(ProgramMethod method, IRCode code) { + return true; + } + + public static void unlinkTrivialGotoBlock(BasicBlock block, BasicBlock target) { + assert block.isTrivialGoto(); + for (BasicBlock pred : block.getPredecessors()) { + pred.replaceSuccessor(block, target); + } + for (BasicBlock succ : block.getSuccessors()) { + succ.getMutablePredecessors().remove(block); + } + for (BasicBlock pred : block.getPredecessors()) { + if (!target.getPredecessors().contains(pred)) { + target.getMutablePredecessors().add(pred); + } + } + } + + private boolean isFallthroughBlock(BasicBlock block) { + for (BasicBlock pred : block.getPredecessors()) { + if (pred.exit().fallthroughBlock() == block) { + return true; + } + } + return false; + } + + private boolean removedTrivialGotos(IRCode code) { + ListIterator<BasicBlock> iterator = code.listIterator(); + assert iterator.hasNext(); + BasicBlock block = iterator.next(); + BasicBlock nextBlock; + do { + nextBlock = iterator.hasNext() ? iterator.next() : null; + // Trivial goto block are only kept if they are self-targeting or are targeted by + // fallthroughs. + BasicBlock blk = block; // Additional local for lambda below. + assert !block.isTrivialGoto() + || block.exit().asGoto().getTarget() == block + || code.entryBlock() == block + || block.getPredecessors().stream().anyMatch((b) -> b.exit().fallthroughBlock() == blk); + // Trivial goto blocks never target the next block (in that case there should just be a + // fallthrough). + assert !block.isTrivialGoto() || block.exit().asGoto().getTarget() != nextBlock; + block = nextBlock; + } while (block != null); + return true; + } + + private void collapseTrivialGoto( + IRCode code, BasicBlock block, BasicBlock nextBlock, List<BasicBlock> blocksToRemove) { + + // This is the base case for GOTO loops. + if (block.exit().asGoto().getTarget() == block) { + return; + } + + BasicBlock target = block.endOfGotoChain(); + + boolean needed = false; + + if (target == null) { + // This implies we are in a loop of GOTOs. In that case, we will iteratively remove each + // trivial GOTO one-by-one until the above base case (one block targeting itself) is left. + target = block.exit().asGoto().getTarget(); + } + + if (target != nextBlock) { + // Not targeting the fallthrough block, determine if we need this goto. We need it if + // a fallthrough can hit this block. That is the case if the block is the entry block + // or if one of the predecessors fall through to the block. + needed = code.entryBlock() == block || isFallthroughBlock(block); + } + + if (!needed) { + blocksToRemove.add(block); + unlinkTrivialGotoBlock(block, target); + } + } + + private void collapseIfTrueTarget(BasicBlock block) { + If insn = block.exit().asIf(); + BasicBlock target = insn.getTrueTarget(); + BasicBlock newTarget = target.endOfGotoChain(); + BasicBlock fallthrough = insn.fallthroughBlock(); + BasicBlock newFallthrough = fallthrough.endOfGotoChain(); + if (newTarget != null && target != newTarget) { + insn.getBlock().replaceSuccessor(target, newTarget); + target.getMutablePredecessors().remove(block); + if (!newTarget.getPredecessors().contains(block)) { + newTarget.getMutablePredecessors().add(block); + } + } + if (block.exit().isIf()) { + insn = block.exit().asIf(); + if (insn.getTrueTarget() == newFallthrough) { + // Replace if with the same true and fallthrough target with a goto to the fallthrough. + block.replaceSuccessor(insn.getTrueTarget(), fallthrough); + assert block.exit().isGoto(); + assert block.exit().asGoto().getTarget() == fallthrough; + } + } + } + + private void collapseNonFallthroughSwitchTargets(BasicBlock block) { + Switch insn = block.exit().asSwitch(); + BasicBlock fallthroughBlock = insn.fallthroughBlock(); + Set<BasicBlock> replacedBlocks = new HashSet<>(); + for (int j = 0; j < insn.targetBlockIndices().length; j++) { + BasicBlock target = insn.targetBlock(j); + if (target != fallthroughBlock) { + BasicBlock newTarget = target.endOfGotoChain(); + if (newTarget != null && target != newTarget && !replacedBlocks.contains(target)) { + insn.getBlock().replaceSuccessor(target, newTarget); + target.getMutablePredecessors().remove(block); + if (!newTarget.getPredecessors().contains(block)) { + newTarget.getMutablePredecessors().add(block); + } + replacedBlocks.add(target); + } + } + } + } +}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialPhiSimplifier.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialPhiSimplifier.java new file mode 100644 index 0000000..6c4c66f --- /dev/null +++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialPhiSimplifier.java
@@ -0,0 +1,113 @@ +// Copyright (c) 2023, 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.passes; + +import com.android.tools.r8.algorithms.scc.SCC; +import com.android.tools.r8.errors.CompilationError; +import com.android.tools.r8.graph.DexItemFactory; +import com.android.tools.r8.graph.DexMethod; +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.NewInstance; +import com.android.tools.r8.ir.code.Phi; +import com.android.tools.r8.ir.code.UnusedArgument; +import com.android.tools.r8.ir.code.Value; +import com.google.common.collect.Sets; +import java.util.List; +import java.util.Set; + +public class TrivialPhiSimplifier { + + public static void replaceUnusedArgumentTrivialPhis(UnusedArgument unusedArgument) { + replaceTrivialPhis(unusedArgument.outValue()); + for (Phi phiUser : unusedArgument.outValue().uniquePhiUsers()) { + phiUser.removeTrivialPhi(); + } + assert !unusedArgument.outValue().hasPhiUsers(); + } + + public static void ensureDirectStringNewToInit(IRCode code, DexItemFactory dexItemFactory) { + for (Instruction instruction : code.instructions()) { + if (instruction.isInvokeDirect()) { + InvokeDirect invoke = instruction.asInvokeDirect(); + DexMethod method = invoke.getInvokedMethod(); + if (dexItemFactory.isConstructor(method) + && method.holder == dexItemFactory.stringType + && invoke.getReceiver().isPhi()) { + NewInstance newInstance = findNewInstance(invoke.getReceiver().asPhi()); + replaceTrivialPhis(newInstance.outValue()); + if (invoke.getReceiver().isPhi()) { + throw new CompilationError( + "Failed to remove trivial phis between new-instance and <init>"); + } + newInstance.markNoSpilling(); + } + } + } + } + + private static NewInstance findNewInstance(Phi phi) { + Set<Phi> seen = Sets.newIdentityHashSet(); + Set<Value> values = Sets.newIdentityHashSet(); + recursiveAddOperands(phi, seen, values); + if (values.size() != 1) { + throw new CompilationError("Failed to identify unique new-instance for <init>"); + } + Value newInstanceValue = values.iterator().next(); + if (newInstanceValue.definition == null || !newInstanceValue.definition.isNewInstance()) { + throw new CompilationError("Invalid defining value for call to <init>"); + } + return newInstanceValue.definition.asNewInstance(); + } + + private static void recursiveAddOperands(Phi phi, Set<Phi> seen, Set<Value> values) { + for (Value operand : phi.getOperands()) { + if (!operand.isPhi()) { + values.add(operand); + } else { + Phi phiOp = operand.asPhi(); + if (seen.add(phiOp)) { + recursiveAddOperands(phiOp, seen, values); + } + } + } + } + + // We compute the set of strongly connected phis making use of the out value and replace all + // trivial ones by the out value. + // This is a simplified variant of the removeRedundantPhis algorithm in Section 3.2 of: + // http://compilers.cs.uni-saarland.de/papers/bbhlmz13cc.pdf + private static void replaceTrivialPhis(Value outValue) { + List<Set<Value>> components = new SCC<Value>(Value::uniquePhiUsers).computeSCC(outValue); + for (int i = components.size() - 1; i >= 0; i--) { + Set<Value> component = components.get(i); + if (component.size() == 1 && component.iterator().next() == outValue) { + continue; + } + Set<Phi> trivialPhis = Sets.newIdentityHashSet(); + for (Value value : component) { + boolean isTrivial = true; + Phi p = value.asPhi(); + for (Value op : p.getOperands()) { + if (op != outValue && !component.contains(op)) { + isTrivial = false; + break; + } + } + if (isTrivial) { + trivialPhis.add(p); + } + } + for (Phi trivialPhi : trivialPhis) { + for (Value op : trivialPhi.getOperands()) { + op.removePhiUser(trivialPhi); + } + trivialPhi.replaceUsers(outValue); + trivialPhi.getBlock().removePhi(trivialPhi); + } + } + } +}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java index 26da1f1..4ad6534 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -4,7 +4,6 @@ package com.android.tools.r8.ir.optimize; -import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull; import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull; import static com.android.tools.r8.ir.analysis.type.Nullability.maybeNull; import static com.android.tools.r8.ir.code.Opcodes.CONST_CLASS; @@ -14,47 +13,29 @@ 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.algorithms.scc.SCC; -import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext; -import com.android.tools.r8.errors.CompilationError; import com.android.tools.r8.errors.Unreachable; -import com.android.tools.r8.graph.AccessControl; import com.android.tools.r8.graph.AppInfoWithClassHierarchy; import com.android.tools.r8.graph.AppView; import com.android.tools.r8.graph.DebugLocalInfo; import com.android.tools.r8.graph.DexClass; import com.android.tools.r8.graph.DexClassAndMethod; -import com.android.tools.r8.graph.DexEncodedField; import com.android.tools.r8.graph.DexEncodedMethod; import com.android.tools.r8.graph.DexItemFactory; import com.android.tools.r8.graph.DexMethod; -import com.android.tools.r8.graph.DexProgramClass; import com.android.tools.r8.graph.DexProto; import com.android.tools.r8.graph.DexString; import com.android.tools.r8.graph.DexType; -import com.android.tools.r8.graph.FieldResolutionResult.SingleProgramFieldResolutionResult; import com.android.tools.r8.graph.ProgramMethod; -import com.android.tools.r8.horizontalclassmerging.HorizontalClassMergerUtils; -import com.android.tools.r8.ir.analysis.equivalence.BasicBlockBehavioralSubsumption; -import com.android.tools.r8.ir.analysis.type.ArrayTypeElement; -import com.android.tools.r8.ir.analysis.type.DynamicTypeWithUpperBound; -import com.android.tools.r8.ir.analysis.type.Nullability; import com.android.tools.r8.ir.analysis.type.TypeAnalysis; import com.android.tools.r8.ir.analysis.type.TypeElement; -import com.android.tools.r8.ir.analysis.type.TypeUtils; import com.android.tools.r8.ir.analysis.value.AbstractValue; -import com.android.tools.r8.ir.analysis.value.ConstantOrNonConstantNumberValue; -import com.android.tools.r8.ir.analysis.value.SingleConstClassValue; -import com.android.tools.r8.ir.analysis.value.SingleFieldValue; import com.android.tools.r8.ir.code.ArrayLength; -import com.android.tools.r8.ir.code.ArrayPut; import com.android.tools.r8.ir.code.Assume; import com.android.tools.r8.ir.code.BasicBlock; import com.android.tools.r8.ir.code.BasicBlockIterator; import com.android.tools.r8.ir.code.Binop; import com.android.tools.r8.ir.code.CatchHandlers; import com.android.tools.r8.ir.code.CatchHandlers.CatchHandler; -import com.android.tools.r8.ir.code.CheckCast; import com.android.tools.r8.ir.code.ConstClass; import com.android.tools.r8.ir.code.ConstNumber; import com.android.tools.r8.ir.code.ConstString; @@ -64,85 +45,51 @@ import com.android.tools.r8.ir.code.DominatorTree; import com.android.tools.r8.ir.code.Goto; import com.android.tools.r8.ir.code.IRCode; -import com.android.tools.r8.ir.code.IRMetadata; import com.android.tools.r8.ir.code.If; import com.android.tools.r8.ir.code.IfType; import com.android.tools.r8.ir.code.InstanceFieldInstruction; import com.android.tools.r8.ir.code.InstanceGet; -import com.android.tools.r8.ir.code.InstanceOf; import com.android.tools.r8.ir.code.Instruction; import com.android.tools.r8.ir.code.Instruction.SideEffectAssumption; import com.android.tools.r8.ir.code.InstructionIterator; import com.android.tools.r8.ir.code.InstructionListIterator; import com.android.tools.r8.ir.code.InstructionOrPhi; -import com.android.tools.r8.ir.code.IntSwitch; import com.android.tools.r8.ir.code.Invoke; import com.android.tools.r8.ir.code.InvokeDirect; import com.android.tools.r8.ir.code.InvokeInterface; import com.android.tools.r8.ir.code.InvokeMethod; import com.android.tools.r8.ir.code.InvokeMethodWithReceiver; -import com.android.tools.r8.ir.code.InvokeNewArray; -import com.android.tools.r8.ir.code.InvokeStatic; import com.android.tools.r8.ir.code.InvokeVirtual; -import com.android.tools.r8.ir.code.LinearFlowInstructionListIterator; import com.android.tools.r8.ir.code.Move; -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.NumericType; import com.android.tools.r8.ir.code.Phi; import com.android.tools.r8.ir.code.Position; import com.android.tools.r8.ir.code.Position.SyntheticPosition; import com.android.tools.r8.ir.code.StaticGet; -import com.android.tools.r8.ir.code.Switch; import com.android.tools.r8.ir.code.Throw; -import com.android.tools.r8.ir.code.UnusedArgument; import com.android.tools.r8.ir.code.Value; -import com.android.tools.r8.ir.code.ValueType; -import com.android.tools.r8.ir.code.Xor; -import com.android.tools.r8.ir.conversion.MethodProcessor; -import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations.UtilityMethodForCodeOptimizations; -import com.android.tools.r8.ir.optimize.controlflow.SwitchCaseAnalyzer; import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo; import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator; -import com.android.tools.r8.shaking.AppInfoWithLiveness; -import com.android.tools.r8.utils.BooleanUtils; import com.android.tools.r8.utils.InternalOptions; -import com.android.tools.r8.utils.InternalOptions.RewriteArrayOptions; -import com.android.tools.r8.utils.InternalOutputMode; import com.android.tools.r8.utils.LazyBox; -import com.android.tools.r8.utils.LongInterval; -import com.android.tools.r8.utils.SetUtils; -import com.android.tools.r8.utils.WorkList; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; import com.google.common.collect.Streams; import it.unimi.dsi.fastutil.ints.Int2IntMap; import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; -import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap; import it.unimi.dsi.fastutil.ints.Int2ReferenceMap; import it.unimi.dsi.fastutil.ints.Int2ReferenceMap.Entry; import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; -import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap; -import it.unimi.dsi.fastutil.ints.IntArrayList; -import it.unimi.dsi.fastutil.ints.IntIterator; -import it.unimi.dsi.fastutil.ints.IntList; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; -import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; -import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Reference2IntMap; import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; import java.util.ArrayList; -import java.util.Arrays; import java.util.BitSet; -import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -150,18 +97,11 @@ import java.util.List; import java.util.ListIterator; import java.util.Map; -import java.util.PriorityQueue; import java.util.Set; import java.util.function.Predicate; public class CodeRewriter { - private enum InstanceOfResult { - UNKNOWN, - TRUE, - FALSE - } - // This constant was determined by experimentation. private static final int STOP_SHARED_CONSTANT_THRESHOLD = 50; @@ -233,28 +173,6 @@ assert Streams.stream(code.instructions()).noneMatch(Instruction::isAssume); } - private static boolean removedTrivialGotos(IRCode code) { - ListIterator<BasicBlock> iterator = code.listIterator(); - assert iterator.hasNext(); - BasicBlock block = iterator.next(); - BasicBlock nextBlock; - do { - nextBlock = iterator.hasNext() ? iterator.next() : null; - // Trivial goto block are only kept if they are self-targeting or are targeted by - // fallthroughs. - BasicBlock blk = block; // Additional local for lambda below. - assert !block.isTrivialGoto() - || block.exit().asGoto().getTarget() == block - || code.entryBlock() == block - || block.getPredecessors().stream().anyMatch((b) -> b.exit().fallthroughBlock() == blk); - // Trivial goto blocks never target the next block (in that case there should just be a - // fallthrough). - assert !block.isTrivialGoto() || block.exit().asGoto().getTarget() != nextBlock; - block = nextBlock; - } while (block != null); - return true; - } - // Rewrite 'throw new NullPointerException()' to 'throw null'. public void rewriteThrowNullPointerException(IRCode code) { boolean shouldRemoveUnreachableBlocks = false; @@ -395,815 +313,6 @@ assert code.isConsistentSSA(appView); } - public static boolean isFallthroughBlock(BasicBlock block) { - for (BasicBlock pred : block.getPredecessors()) { - if (pred.exit().fallthroughBlock() == block) { - return true; - } - } - return false; - } - - private static void collapseTrivialGoto( - IRCode code, BasicBlock block, BasicBlock nextBlock, List<BasicBlock> blocksToRemove) { - - // This is the base case for GOTO loops. - if (block.exit().asGoto().getTarget() == block) { - return; - } - - BasicBlock target = block.endOfGotoChain(); - - boolean needed = false; - - if (target == null) { - // This implies we are in a loop of GOTOs. In that case, we will iteratively remove each - // trivial GOTO one-by-one until the above base case (one block targeting itself) is left. - target = block.exit().asGoto().getTarget(); - } - - if (target != nextBlock) { - // Not targeting the fallthrough block, determine if we need this goto. We need it if - // a fallthrough can hit this block. That is the case if the block is the entry block - // or if one of the predecessors fall through to the block. - needed = code.entryBlock() == block || isFallthroughBlock(block); - } - - if (!needed) { - blocksToRemove.add(block); - unlinkTrivialGotoBlock(block, target); - } - } - - public static void unlinkTrivialGotoBlock(BasicBlock block, BasicBlock target) { - assert block.isTrivialGoto(); - for (BasicBlock pred : block.getPredecessors()) { - pred.replaceSuccessor(block, target); - } - for (BasicBlock succ : block.getSuccessors()) { - succ.getMutablePredecessors().remove(block); - } - for (BasicBlock pred : block.getPredecessors()) { - if (!target.getPredecessors().contains(pred)) { - target.getMutablePredecessors().add(pred); - } - } - } - - private static void collapseIfTrueTarget(BasicBlock block) { - If insn = block.exit().asIf(); - BasicBlock target = insn.getTrueTarget(); - BasicBlock newTarget = target.endOfGotoChain(); - BasicBlock fallthrough = insn.fallthroughBlock(); - BasicBlock newFallthrough = fallthrough.endOfGotoChain(); - if (newTarget != null && target != newTarget) { - insn.getBlock().replaceSuccessor(target, newTarget); - target.getMutablePredecessors().remove(block); - if (!newTarget.getPredecessors().contains(block)) { - newTarget.getMutablePredecessors().add(block); - } - } - if (block.exit().isIf()) { - insn = block.exit().asIf(); - if (insn.getTrueTarget() == newFallthrough) { - // Replace if with the same true and fallthrough target with a goto to the fallthrough. - block.replaceSuccessor(insn.getTrueTarget(), fallthrough); - assert block.exit().isGoto(); - assert block.exit().asGoto().getTarget() == fallthrough; - } - } - } - - private static void collapseNonFallthroughSwitchTargets(BasicBlock block) { - Switch insn = block.exit().asSwitch(); - BasicBlock fallthroughBlock = insn.fallthroughBlock(); - Set<BasicBlock> replacedBlocks = new HashSet<>(); - for (int j = 0; j < insn.targetBlockIndices().length; j++) { - BasicBlock target = insn.targetBlock(j); - if (target != fallthroughBlock) { - BasicBlock newTarget = target.endOfGotoChain(); - if (newTarget != null && target != newTarget && !replacedBlocks.contains(target)) { - insn.getBlock().replaceSuccessor(target, newTarget); - target.getMutablePredecessors().remove(block); - if (!newTarget.getPredecessors().contains(block)) { - newTarget.getMutablePredecessors().add(block); - } - replacedBlocks.add(target); - } - } - } - } - - // TODO(sgjesse); Move this somewhere else, and reuse it for some of the other switch rewritings. - public abstract static class InstructionBuilder<T> { - - protected int blockNumber; - protected final Position position; - - protected InstructionBuilder(Position position) { - this.position = position; - } - - public abstract T self(); - - public T setBlockNumber(int blockNumber) { - this.blockNumber = blockNumber; - return self(); - } - } - - public static class SwitchBuilder extends InstructionBuilder<SwitchBuilder> { - - private Value value; - private final Int2ReferenceSortedMap<BasicBlock> keyToTarget = new Int2ReferenceAVLTreeMap<>(); - private BasicBlock fallthrough; - - public SwitchBuilder(Position position) { - super(position); - } - - @Override - public SwitchBuilder self() { - return this; - } - - public SwitchBuilder setValue(Value value) { - this.value = value; - return this; - } - - public SwitchBuilder addKeyAndTarget(int key, BasicBlock target) { - keyToTarget.put(key, target); - return this; - } - - public SwitchBuilder setFallthrough(BasicBlock fallthrough) { - this.fallthrough = fallthrough; - return this; - } - - public BasicBlock build(IRMetadata metadata) { - final int NOT_FOUND = -1; - Object2IntMap<BasicBlock> targetToSuccessorIndex = new Object2IntLinkedOpenHashMap<>(); - targetToSuccessorIndex.defaultReturnValue(NOT_FOUND); - - int[] keys = new int[keyToTarget.size()]; - int[] targetBlockIndices = new int[keyToTarget.size()]; - // Sort keys descending. - int count = 0; - IntIterator iter = keyToTarget.keySet().iterator(); - while (iter.hasNext()) { - int key = iter.nextInt(); - BasicBlock target = keyToTarget.get(key); - Integer targetIndex = - targetToSuccessorIndex.computeIfAbsent(target, b -> targetToSuccessorIndex.size()); - keys[count] = key; - targetBlockIndices[count] = targetIndex; - count++; - } - Integer fallthroughIndex = - targetToSuccessorIndex.computeIfAbsent(fallthrough, b -> targetToSuccessorIndex.size()); - IntSwitch newSwitch = new IntSwitch(value, keys, targetBlockIndices, fallthroughIndex); - newSwitch.setPosition(position); - BasicBlock newSwitchBlock = BasicBlock.createSwitchBlock(blockNumber, newSwitch, metadata); - for (BasicBlock successor : targetToSuccessorIndex.keySet()) { - newSwitchBlock.link(successor); - } - return newSwitchBlock; - } - } - - public static class IfBuilder extends InstructionBuilder<IfBuilder> { - - private final IRCode code; - private Value left; - private int right; - private BasicBlock target; - private BasicBlock fallthrough; - - public IfBuilder(Position position, IRCode code) { - super(position); - this.code = code; - } - - @Override - public IfBuilder self() { - return this; - } - - public IfBuilder setLeft(Value left) { - this.left = left; - return this; - } - - public IfBuilder setRight(int right) { - this.right = right; - return this; - } - - public IfBuilder setTarget(BasicBlock target) { - this.target = target; - return this; - } - - public IfBuilder setFallthrough(BasicBlock fallthrough) { - this.fallthrough = fallthrough; - return this; - } - - public BasicBlock build() { - assert target != null; - assert fallthrough != null; - If newIf; - BasicBlock ifBlock; - if (right != 0) { - ConstNumber rightConst = code.createIntConstant(right); - rightConst.setPosition(position); - newIf = new If(IfType.EQ, ImmutableList.of(left, rightConst.dest())); - ifBlock = BasicBlock.createIfBlock(blockNumber, newIf, code.metadata(), rightConst); - } else { - newIf = new If(IfType.EQ, left); - ifBlock = BasicBlock.createIfBlock(blockNumber, newIf, code.metadata()); - } - newIf.setPosition(position); - ifBlock.link(target); - ifBlock.link(fallthrough); - return ifBlock; - } - } - - /** - * Covert the switch instruction to a sequence of if instructions checking for a specified set of - * keys, followed by a new switch with the remaining keys. - */ - // TODO(b/270398965): Replace LinkedList. - @SuppressWarnings("JdkObsolete") - void convertSwitchToSwitchAndIfs( - IRCode code, - ListIterator<BasicBlock> blocksIterator, - BasicBlock originalBlock, - InstructionListIterator iterator, - IntSwitch theSwitch, - List<IntList> switches, - IntList keysToRemove) { - - Position position = theSwitch.getPosition(); - - // Extract the information from the switch before removing it. - Int2ReferenceSortedMap<BasicBlock> keyToTarget = theSwitch.getKeyToTargetMap(); - - // Keep track of the current fallthrough, starting with the original. - BasicBlock fallthroughBlock = theSwitch.fallthroughBlock(); - - // Split the switch instruction into its own block and remove it. - iterator.previous(); - BasicBlock originalSwitchBlock = iterator.split(code, blocksIterator); - assert !originalSwitchBlock.hasCatchHandlers(); - assert originalSwitchBlock.getInstructions().size() == 1; - assert originalBlock.exit().isGoto(); - theSwitch.moveDebugValues(originalBlock.exit()); - blocksIterator.remove(); - theSwitch.getBlock().detachAllSuccessors(); - BasicBlock block = theSwitch.getBlock().unlinkSinglePredecessor(); - assert theSwitch.getBlock().getPredecessors().size() == 0; - assert theSwitch.getBlock().getSuccessors().size() == 0; - assert block == originalBlock; - - // Collect the new blocks for adding to the block list. - LinkedList<BasicBlock> newBlocks = new LinkedList<>(); - - // Build the switch-blocks backwards, to always have the fallthrough block in hand. - for (int i = switches.size() - 1; i >= 0; i--) { - SwitchBuilder switchBuilder = new SwitchBuilder(position); - switchBuilder.setValue(theSwitch.value()); - IntList keys = switches.get(i); - for (int j = 0; j < keys.size(); j++) { - int key = keys.getInt(j); - switchBuilder.addKeyAndTarget(key, keyToTarget.get(key)); - } - switchBuilder.setFallthrough(fallthroughBlock).setBlockNumber(code.getNextBlockNumber()); - BasicBlock newSwitchBlock = switchBuilder.build(code.metadata()); - newBlocks.addFirst(newSwitchBlock); - fallthroughBlock = newSwitchBlock; - } - - // Build the if-blocks backwards, to always have the fallthrough block in hand. - for (int i = keysToRemove.size() - 1; i >= 0; i--) { - int key = keysToRemove.getInt(i); - BasicBlock peeledOffTarget = keyToTarget.get(key); - IfBuilder ifBuilder = new IfBuilder(position, code); - ifBuilder - .setLeft(theSwitch.value()) - .setRight(key) - .setTarget(peeledOffTarget) - .setFallthrough(fallthroughBlock) - .setBlockNumber(code.getNextBlockNumber()); - BasicBlock ifBlock = ifBuilder.build(); - newBlocks.addFirst(ifBlock); - fallthroughBlock = ifBlock; - } - - // Finally link the block before the original switch to the new block sequence. - originalBlock.link(fallthroughBlock); - - // Finally add the blocks. - newBlocks.forEach(blocksIterator::add); - } - - private static class Interval { - - private final IntList keys = new IntArrayList(); - - public Interval(IntList... allKeys) { - assert allKeys.length > 0; - for (IntList keys : allKeys) { - assert keys.size() > 0; - this.keys.addAll(keys); - } - } - - public int getMin() { - return keys.getInt(0); - } - - public int getMax() { - return keys.getInt(keys.size() - 1); - } - - public void addInterval(Interval other) { - assert getMax() < other.getMin(); - keys.addAll(other.keys); - } - - public long packedSavings(InternalOutputMode mode) { - long packedTargets = (long) getMax() - (long) getMin() + 1; - if (!IntSwitch.canBePacked(mode, packedTargets)) { - return Long.MIN_VALUE + 1; - } - long sparseCost = - IntSwitch.baseSparseSize(mode) + IntSwitch.sparsePayloadSize(mode, keys.size()); - long packedCost = - IntSwitch.basePackedSize(mode) + IntSwitch.packedPayloadSize(mode, packedTargets); - return sparseCost - packedCost; - } - - public long estimatedSize(InternalOutputMode mode) { - return IntSwitch.estimatedSize(mode, keys.toIntArray()); - } - } - - private Interval combineOrAddInterval( - List<Interval> intervals, Interval previous, Interval current) { - // As a first iteration, we only combine intervals if their packed size is less than their - // sparse counterpart. In CF we will have to add a load and a jump which add to the - // stack map table (1 is the size of a same entry). - InternalOutputMode mode = options.getInternalOutputMode(); - int penalty = mode.isGeneratingClassFiles() ? 3 + 1 : 0; - if (previous == null) { - intervals.add(current); - return current; - } - Interval combined = new Interval(previous.keys, current.keys); - long packedSavings = combined.packedSavings(mode); - if (packedSavings <= 0 - || packedSavings < previous.estimatedSize(mode) + current.estimatedSize(mode) - penalty) { - intervals.add(current); - return current; - } else { - intervals.set(intervals.size() - 1, combined); - return combined; - } - } - - private void tryAddToBiggestSavings( - Set<Interval> biggestPackedSet, - PriorityQueue<Interval> intervals, - Interval toAdd, - int maximumNumberOfSwitches) { - assert !biggestPackedSet.contains(toAdd); - long savings = toAdd.packedSavings(options.getInternalOutputMode()); - if (savings <= 0) { - return; - } - if (intervals.size() < maximumNumberOfSwitches) { - intervals.add(toAdd); - biggestPackedSet.add(toAdd); - } else if (savings > intervals.peek().packedSavings(options.getInternalOutputMode())) { - intervals.add(toAdd); - biggestPackedSet.add(toAdd); - biggestPackedSet.remove(intervals.poll()); - } - } - - private int sizeForKeysWrittenAsIfs(ValueType type, Collection<Integer> keys) { - int ifsSize = If.estimatedSize(options.getInternalOutputMode()) * keys.size(); - // In Cf we also require a load as well (and a stack map entry) - if (options.getInternalOutputMode().isGeneratingClassFiles()) { - ifsSize += keys.size() * (3 + 1); - } - for (int k : keys) { - if (k != 0) { - ifsSize += ConstNumber.estimatedSize(options.getInternalOutputMode(), type, k); - } - } - return ifsSize; - } - - private int codeUnitMargin() { - return options.getInternalOutputMode().isGeneratingClassFiles() ? 3 : 1; - } - - private int findIfsForCandidates( - List<Interval> newSwitches, IntSwitch theSwitch, IntList outliers) { - Set<Interval> switchesToRemove = new HashSet<>(); - InternalOutputMode mode = options.getInternalOutputMode(); - int outliersAsIfSize = 0; - // The candidateForIfs is either an index to a switch that can be eliminated totally or a sparse - // where removing a key may produce a greater saving. It is only if keys are small in the packed - // switch that removing the keys makes sense (size wise). - for (Interval candidate : newSwitches) { - int maxIfBudget = 10; - long switchSize = candidate.estimatedSize(mode); - int sizeOfAllKeysAsIf = sizeForKeysWrittenAsIfs(theSwitch.value().outType(), candidate.keys); - if (candidate.keys.size() <= maxIfBudget - && sizeOfAllKeysAsIf < switchSize - codeUnitMargin()) { - outliersAsIfSize += sizeOfAllKeysAsIf; - switchesToRemove.add(candidate); - outliers.addAll(candidate.keys); - continue; - } - // One could do something clever here, but we use a simple algorithm that use the fact that - // all keys are sorted in ascending order and that the smallest absolute value will give the - // best saving. - IntList candidateKeys = candidate.keys; - int smallestPosition = -1; - long smallest = Long.MAX_VALUE; - for (int i = 0; i < candidateKeys.size(); i++) { - long current = Math.abs((long) candidateKeys.getInt(i)); - if (current < smallest) { - smallestPosition = i; - smallest = current; - } - } - // Add as many keys forward and backward as we have budget and we decrease in size. - IntList ifKeys = new IntArrayList(); - ifKeys.add(candidateKeys.getInt(smallestPosition)); - long previousSavings = 0; - long currentSavings = - switchSize - - sizeForKeysWrittenAsIfs(theSwitch.value().outType(), ifKeys) - - IntSwitch.estimatedSparseSize(mode, candidateKeys.size() - ifKeys.size()); - int minIndex = smallestPosition - 1; - int maxIndex = smallestPosition + 1; - while (ifKeys.size() < maxIfBudget && currentSavings > previousSavings) { - if (minIndex >= 0 && maxIndex < candidateKeys.size()) { - long valMin = Math.abs((long) candidateKeys.getInt(minIndex)); - int valMax = Math.abs(candidateKeys.getInt(maxIndex)); - if (valMax <= valMin) { - ifKeys.add(candidateKeys.getInt(maxIndex++)); - } else { - ifKeys.add(candidateKeys.getInt(minIndex--)); - } - } else if (minIndex >= 0) { - ifKeys.add(candidateKeys.getInt(minIndex--)); - } else if (maxIndex < candidateKeys.size()) { - ifKeys.add(candidateKeys.getInt(maxIndex++)); - } else { - // No more elements to add as if's. - break; - } - previousSavings = currentSavings; - currentSavings = - switchSize - - sizeForKeysWrittenAsIfs(theSwitch.value().outType(), ifKeys) - - IntSwitch.estimatedSparseSize(mode, candidateKeys.size() - ifKeys.size()); - } - if (previousSavings >= currentSavings) { - // Remove the last added key since it did not contribute to savings. - int lastKey = ifKeys.getInt(ifKeys.size() - 1); - ifKeys.removeInt(ifKeys.size() - 1); - if (lastKey == candidateKeys.getInt(minIndex + 1)) { - minIndex++; - } else { - maxIndex--; - } - } - // Adjust pointers into the candidate keys. - minIndex++; - maxIndex--; - if (ifKeys.size() > 0) { - int ifsSize = sizeForKeysWrittenAsIfs(theSwitch.value().outType(), ifKeys); - long newSwitchSize = - IntSwitch.estimatedSparseSize(mode, candidateKeys.size() - ifKeys.size()); - if (newSwitchSize + ifsSize + codeUnitMargin() < switchSize) { - candidateKeys.removeElements(minIndex, maxIndex); - outliers.addAll(ifKeys); - outliersAsIfSize += ifsSize; - } - } - } - newSwitches.removeAll(switchesToRemove); - return outliersAsIfSize; - } - - public boolean rewriteSwitch(IRCode code) { - return rewriteSwitch(code, SwitchCaseAnalyzer.getInstance()); - } - - private boolean rewriteSwitch(IRCode code, SwitchCaseAnalyzer switchCaseAnalyzer) { - if (!options.isSwitchRewritingEnabled()) { - return false; - } - if (!code.metadata().mayHaveSwitch()) { - return false; - } - return rewriteSwitchFull(code, switchCaseAnalyzer); - } - - private boolean rewriteSwitchFull(IRCode code, SwitchCaseAnalyzer switchCaseAnalyzer) { - boolean needToRemoveUnreachableBlocks = false; - ListIterator<BasicBlock> blocksIterator = code.listIterator(); - while (blocksIterator.hasNext()) { - BasicBlock block = blocksIterator.next(); - InstructionListIterator iterator = block.listIterator(code); - while (iterator.hasNext()) { - Instruction instruction = iterator.next(); - if (instruction.isSwitch()) { - Switch theSwitch = instruction.asSwitch(); - if (options.testing.enableDeadSwitchCaseElimination) { - SwitchCaseEliminator eliminator = - removeUnnecessarySwitchCases(code, theSwitch, iterator, switchCaseAnalyzer); - if (eliminator.mayHaveIntroducedUnreachableBlocks()) { - needToRemoveUnreachableBlocks = true; - } - - iterator.previous(); - instruction = iterator.next(); - if (instruction.isGoto()) { - continue; - } - - assert instruction.isSwitch(); - theSwitch = instruction.asSwitch(); - } - if (theSwitch.isIntSwitch()) { - rewriteIntSwitch(code, blocksIterator, block, iterator, theSwitch.asIntSwitch()); - } - } - } - } - - // Rewriting of switches introduces new branching structure. It relies on critical edges - // being split on the way in but does not maintain this property. We therefore split - // critical edges at exit. - code.splitCriticalEdges(); - - Set<Value> affectedValues = - needToRemoveUnreachableBlocks ? code.removeUnreachableBlocks() : ImmutableSet.of(); - if (!affectedValues.isEmpty()) { - new TypeAnalysis(appView).narrowing(affectedValues); - } - assert code.isConsistentSSA(appView); - return !affectedValues.isEmpty(); - } - - void rewriteSingleKeySwitchToIf( - IRCode code, BasicBlock block, InstructionListIterator iterator, IntSwitch theSwitch) { - // Rewrite the switch to an if. - int fallthroughBlockIndex = theSwitch.getFallthroughBlockIndex(); - int caseBlockIndex = theSwitch.targetBlockIndices()[0]; - if (fallthroughBlockIndex < caseBlockIndex) { - block.swapSuccessorsByIndex(fallthroughBlockIndex, caseBlockIndex); - } - If replacement; - if (theSwitch.isIntSwitch() && theSwitch.asIntSwitch().getFirstKey() == 0) { - replacement = new If(IfType.EQ, theSwitch.value()); - } else { - Instruction labelConst = theSwitch.materializeFirstKey(appView, code); - labelConst.setPosition(theSwitch.getPosition()); - iterator.previous(); - iterator.add(labelConst); - Instruction dummy = iterator.next(); - assert dummy == theSwitch; - replacement = new If(IfType.EQ, ImmutableList.of(theSwitch.value(), labelConst.outValue())); - } - iterator.replaceCurrentInstruction(replacement); - } - - private void rewriteIntSwitch( - IRCode code, - ListIterator<BasicBlock> blockIterator, - BasicBlock block, - InstructionListIterator iterator, - IntSwitch theSwitch) { - if (disableSwitchToIfRewritingForClassIdComparisons(theSwitch)) { - return; - } - - if (theSwitch.numberOfKeys() == 1) { - rewriteSingleKeySwitchToIf(code, block, iterator, theSwitch); - return; - } - - // If there are more than 1 key, we use the following algorithm to find keys to combine. - // First, scan through the keys forward and combine each packed interval with the - // previous interval if it gives a net saving. - // Secondly, go through all created intervals and combine the ones without a saving into - // a single interval and keep a max number of packed switches. - // Finally, go through all intervals and check if the switch or part of the switch - // should be transformed to ifs. - - // Phase 1: Combine packed intervals. - InternalOutputMode mode = options.getInternalOutputMode(); - int[] keys = theSwitch.getKeys(); - int maxNumberOfIfsOrSwitches = 10; - PriorityQueue<Interval> biggestPackedSavings = - new PriorityQueue<>((x, y) -> Long.compare(y.packedSavings(mode), x.packedSavings(mode))); - Set<Interval> biggestPackedSet = new HashSet<>(); - List<Interval> intervals = new ArrayList<>(); - int previousKey = keys[0]; - IntList currentKeys = new IntArrayList(); - currentKeys.add(previousKey); - Interval previousInterval = null; - for (int i = 1; i < keys.length; i++) { - int key = keys[i]; - if (((long) key - (long) previousKey) > 1) { - Interval current = new Interval(currentKeys); - Interval added = combineOrAddInterval(intervals, previousInterval, current); - if (added != current && biggestPackedSet.contains(previousInterval)) { - biggestPackedSet.remove(previousInterval); - biggestPackedSavings.remove(previousInterval); - } - tryAddToBiggestSavings( - biggestPackedSet, biggestPackedSavings, added, maxNumberOfIfsOrSwitches); - previousInterval = added; - currentKeys = new IntArrayList(); - } - currentKeys.add(key); - previousKey = key; - } - Interval current = new Interval(currentKeys); - Interval added = combineOrAddInterval(intervals, previousInterval, current); - if (added != current && biggestPackedSet.contains(previousInterval)) { - biggestPackedSet.remove(previousInterval); - biggestPackedSavings.remove(previousInterval); - } - tryAddToBiggestSavings(biggestPackedSet, biggestPackedSavings, added, maxNumberOfIfsOrSwitches); - - // Phase 2: combine sparse intervals into a single bin. - // Check if we should save a space for a sparse switch, if so, remove the switch with - // the smallest savings. - if (biggestPackedSet.size() == maxNumberOfIfsOrSwitches - && maxNumberOfIfsOrSwitches < intervals.size()) { - biggestPackedSet.remove(biggestPackedSavings.poll()); - } - Interval sparse = null; - List<Interval> newSwitches = new ArrayList<>(maxNumberOfIfsOrSwitches); - for (int i = 0; i < intervals.size(); i++) { - Interval interval = intervals.get(i); - if (biggestPackedSet.contains(interval)) { - newSwitches.add(interval); - } else if (sparse == null) { - sparse = interval; - newSwitches.add(sparse); - } else { - sparse.addInterval(interval); - } - } - - // Phase 3: at this point we are guaranteed to have the biggest saving switches - // in newIntervals, potentially with a switch combining the remaining intervals. - // Now we check to see if we can create any if's to reduce size. - IntList outliers = new IntArrayList(); - int outliersAsIfSize = - appView.options().testing.enableSwitchToIfRewriting - ? findIfsForCandidates(newSwitches, theSwitch, outliers) - : 0; - - long newSwitchesSize = 0; - List<IntList> newSwitchSequences = new ArrayList<>(newSwitches.size()); - for (Interval interval : newSwitches) { - newSwitchesSize += interval.estimatedSize(mode); - newSwitchSequences.add(interval.keys); - } - - long currentSize = IntSwitch.estimatedSize(mode, theSwitch.getKeys()); - if (newSwitchesSize + outliersAsIfSize + codeUnitMargin() < currentSize) { - convertSwitchToSwitchAndIfs( - code, blockIterator, block, iterator, theSwitch, newSwitchSequences, outliers); - } - } - - // TODO(b/181732463): We currently disable switch-to-if rewritings for switches on $r8$classId - // field values (from horizontal class merging. See bug for more details. - private boolean disableSwitchToIfRewritingForClassIdComparisons(IntSwitch theSwitch) { - Value switchValue = theSwitch.value().getAliasedValue(); - if (!switchValue.isDefinedByInstructionSatisfying(Instruction::isInstanceGet)) { - return false; - } - AppInfoWithLiveness appInfo = appView.appInfoWithLiveness(); - if (appInfo == null) { - return false; - } - InstanceGet instanceGet = switchValue.getDefinition().asInstanceGet(); - SingleProgramFieldResolutionResult resolutionResult = - appInfo.resolveField(instanceGet.getField()).asSingleProgramFieldResolutionResult(); - if (resolutionResult == null) { - return false; - } - DexEncodedField resolvedField = resolutionResult.getResolvedField(); - return HorizontalClassMergerUtils.isClassIdField(appView, resolvedField); - } - - private SwitchCaseEliminator removeUnnecessarySwitchCases( - IRCode code, - Switch theSwitch, - InstructionListIterator iterator, - SwitchCaseAnalyzer switchCaseAnalyzer) { - BasicBlock defaultTarget = theSwitch.fallthroughBlock(); - SwitchCaseEliminator eliminator = new SwitchCaseEliminator(theSwitch, iterator); - BasicBlockBehavioralSubsumption behavioralSubsumption = - new BasicBlockBehavioralSubsumption(appView, code); - - // Compute the set of switch cases that can be removed. - boolean hasSwitchCaseToDefaultRewrite = false; - AbstractValue switchAbstractValue = theSwitch.value().getAbstractValue(appView, code.context()); - for (int i = 0; i < theSwitch.numberOfKeys(); i++) { - BasicBlock targetBlock = theSwitch.targetBlock(i); - - if (switchCaseAnalyzer.switchCaseIsAlwaysHit(theSwitch, i)) { - eliminator.markSwitchCaseAsAlwaysHit(i); - break; - } - - // This switch case can be removed if the behavior of the target block is equivalent to the - // behavior of the default block, or if the switch case is unreachable. - if (switchCaseAnalyzer.switchCaseIsUnreachable(theSwitch, switchAbstractValue, i)) { - eliminator.markSwitchCaseForRemoval(i); - } else if (behavioralSubsumption.isSubsumedBy( - theSwitch.value(), targetBlock, defaultTarget)) { - eliminator.markSwitchCaseForRemoval(i); - hasSwitchCaseToDefaultRewrite = true; - } - } - - if (eliminator.isFallthroughLive() - && !hasSwitchCaseToDefaultRewrite - && switchCaseAnalyzer.switchFallthroughIsNeverHit(theSwitch, switchAbstractValue)) { - eliminator.markSwitchFallthroughAsNeverHit(); - } - - eliminator.optimize(); - return eliminator; - } - - /** - * Rewrite all branch targets to the destination of trivial goto chains when possible. Does not - * rewrite fallthrough targets as that would require block reordering and the transformation only - * makes sense after SSA destruction where there are no phis. - */ - public static void collapseTrivialGotos(AppView<?> appView, IRCode code) { - assert code.isConsistentGraph(appView); - List<BasicBlock> blocksToRemove = new ArrayList<>(); - // Rewrite all non-fallthrough targets to the end of trivial goto chains and remove - // first round of trivial goto blocks. - ListIterator<BasicBlock> iterator = code.listIterator(); - assert iterator.hasNext(); - BasicBlock block = iterator.next(); - BasicBlock nextBlock; - - do { - nextBlock = iterator.hasNext() ? iterator.next() : null; - if (block.isTrivialGoto()) { - collapseTrivialGoto(code, block, nextBlock, blocksToRemove); - } - if (block.exit().isIf()) { - collapseIfTrueTarget(block); - } - if (block.exit().isSwitch()) { - collapseNonFallthroughSwitchTargets(block); - } - block = nextBlock; - } while (nextBlock != null); - code.removeBlocks(blocksToRemove); - // Get rid of gotos to the next block. - while (!blocksToRemove.isEmpty()) { - blocksToRemove = new ArrayList<>(); - iterator = code.listIterator(); - block = iterator.next(); - do { - nextBlock = iterator.hasNext() ? iterator.next() : null; - if (block.isTrivialGoto()) { - collapseTrivialGoto(code, block, nextBlock, blocksToRemove); - } - block = nextBlock; - } while (block != null); - code.removeBlocks(blocksToRemove); - } - assert removedTrivialGotos(code); - assert code.isConsistentGraph(appView); - } - private boolean checkArgumentType(InvokeMethod invoke, int argumentIndex) { // TODO(sgjesse): Insert cast if required. TypeElement returnType = @@ -1309,333 +418,6 @@ return changed; } - enum RemoveCheckCastInstructionIfTrivialResult { - NO_REMOVALS, - REMOVED_CAST_DO_NARROW - } - - public void removeTrivialCheckCastAndInstanceOfInstructions( - IRCode code, - ProgramMethod context, - MethodProcessor methodProcessor, - MethodProcessingContext methodProcessingContext) { - if (!appView.enableWholeProgramOptimizations()) { - return; - } - - assert appView.appInfo().hasLiveness(); - AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness(); - - if (!appView.options().testing.enableCheckCastAndInstanceOfRemoval) { - return; - } - - IRMetadata metadata = code.metadata(); - if (!metadata.mayHaveCheckCast() && !metadata.mayHaveInstanceOf()) { - return; - } - - // If we can remove a CheckCast it is due to us having at least as much information about the - // type as the CheckCast gives. We then need to propagate that information to the users of - // the CheckCast to ensure further optimizations and removals of CheckCast: - // - // : 1: NewArrayEmpty v2 <- v1(1) java.lang.String[] <-- v2 = String[] - // ... - // : 2: CheckCast v5 <- v2; java.lang.Object[] <-- v5 = Object[] - // ... - // : 3: ArrayGet v7 <- v5, v6(0) <-- v7 = Object - // : 4: CheckCast v8 <- v7; java.lang.String <-- v8 = String - // ... - // - // When looking at line 2 we can conclude that the CheckCast is trivial because v2 is String[] - // and remove it. However, v7 is still only known to be Object and we cannot remove the - // CheckCast at line 4 unless we update v7 with the most precise information by narrowing the - // affected values of v5. We therefore have to run the type analysis after each CheckCast - // removal. - TypeAnalysis typeAnalysis = new TypeAnalysis(appView); - Set<Value> affectedValues = Sets.newIdentityHashSet(); - InstructionListIterator it = code.instructionListIterator(); - boolean needToRemoveTrivialPhis = false; - while (it.hasNext()) { - Instruction current = it.next(); - if (current.isCheckCast()) { - boolean hasPhiUsers = current.outValue().hasPhiUsers(); - RemoveCheckCastInstructionIfTrivialResult removeResult = - removeCheckCastInstructionIfTrivial( - appViewWithLiveness, - current.asCheckCast(), - it, - code, - context, - affectedValues, - methodProcessor, - methodProcessingContext); - if (removeResult != RemoveCheckCastInstructionIfTrivialResult.NO_REMOVALS) { - assert removeResult == RemoveCheckCastInstructionIfTrivialResult.REMOVED_CAST_DO_NARROW; - needToRemoveTrivialPhis |= hasPhiUsers; - typeAnalysis.narrowing(affectedValues); - affectedValues.clear(); - } - } else if (current.isInstanceOf()) { - boolean hasPhiUsers = current.outValue().hasPhiUsers(); - if (removeInstanceOfInstructionIfTrivial( - appViewWithLiveness, current.asInstanceOf(), it, code)) { - needToRemoveTrivialPhis |= hasPhiUsers; - } - } - } - // ... v1 - // ... - // v2 <- check-cast v1, T - // v3 <- phi(v1, v2) - // Removing check-cast may result in a trivial phi: - // v3 <- phi(v1, v1) - if (needToRemoveTrivialPhis) { - code.removeAllDeadAndTrivialPhis(affectedValues); - if (!affectedValues.isEmpty()) { - typeAnalysis.narrowing(affectedValues); - } - } - assert code.isConsistentSSA(appView); - } - - // Returns true if the given check-cast instruction was removed. - private RemoveCheckCastInstructionIfTrivialResult removeCheckCastInstructionIfTrivial( - AppView<AppInfoWithLiveness> appViewWithLiveness, - CheckCast checkCast, - InstructionListIterator it, - IRCode code, - ProgramMethod context, - Set<Value> affectedValues, - MethodProcessor methodProcessor, - MethodProcessingContext methodProcessingContext) { - Value inValue = checkCast.object(); - Value outValue = checkCast.outValue(); - DexType castType = checkCast.getType(); - DexType baseCastType = castType.toBaseType(dexItemFactory); - - // If the cast type is not accessible in the current context, we should not remove the cast - // in order to preserve runtime errors. Note that JVM and ART behave differently: see - // {@link com.android.tools.r8.ir.optimize.checkcast.IllegalAccessErrorTest}. - if (baseCastType.isClassType()) { - DexClass baseCastClass = appView.definitionFor(baseCastType); - if (baseCastClass == null - || AccessControl.isClassAccessible(baseCastClass, code.context(), appViewWithLiveness) - .isPossiblyFalse()) { - return RemoveCheckCastInstructionIfTrivialResult.NO_REMOVALS; - } - } - - if (!appView - .getOpenClosedInterfacesCollection() - .isDefinitelyInstanceOfStaticType(appViewWithLiveness, inValue)) { - return RemoveCheckCastInstructionIfTrivialResult.NO_REMOVALS; - } - - // If the in-value is `null` and the cast-type is a float-array type, then trivial check-cast - // elimination may lead to verification errors. See b/123269162. - if (options.canHaveArtCheckCastVerifierBug()) { - if (inValue.getType().isNullType() - && castType.isArrayType() - && castType.toBaseType(dexItemFactory).isFloatType()) { - return RemoveCheckCastInstructionIfTrivialResult.NO_REMOVALS; - } - } - - // If casting to an array of an interface type elimination may lead to verification errors. - // See b/132420510 and b/223424356. - if (options.canHaveIncorrectJoinForArrayOfInterfacesBug()) { - if (castType.isArrayType()) { - DexType baseType = castType.toBaseType(dexItemFactory); - if (baseType.isClassType() && baseType.isInterface(appViewWithLiveness)) { - return RemoveCheckCastInstructionIfTrivialResult.NO_REMOVALS; - } - } - } - - TypeElement inTypeLattice = inValue.getType(); - TypeElement outTypeLattice = outValue.getType(); - TypeElement castTypeLattice = castType.toTypeElement(appView, inTypeLattice.nullability()); - - assert inTypeLattice.nullability().lessThanOrEqual(outTypeLattice.nullability()); - - if (inTypeLattice.lessThanOrEqual(castTypeLattice, appView)) { - // 1) Trivial cast. - // A a = ... - // A a' = (A) a; - // 2) Up-cast: we already have finer type info. - // A < B - // A a = ... - // B b = (B) a; - assert inTypeLattice.lessThanOrEqual(outTypeLattice, appView); - // The removeOrReplaceByDebugLocalWrite will propagate the incoming value for the CheckCast - // to the users of the CheckCast's out value. - // - // v2 = CheckCast A v1 ~~> DebugLocalWrite $v0 <- v1 - // - // The DebugLocalWrite is not a user of the outvalue, we therefore have to wait and take the - // CheckCast invalue users that includes the potential DebugLocalWrite. - removeOrReplaceByDebugLocalWrite(checkCast, it, inValue, outValue); - affectedValues.addAll(inValue.affectedValues()); - return RemoveCheckCastInstructionIfTrivialResult.REMOVED_CAST_DO_NARROW; - } - - // If values of cast type are guaranteed to be null, then the out-value must be null if the cast - // succeeds. After removing all usages of the out-value, the check-cast instruction is replaced - // by a call to throwClassCastExceptionIfNotNull() to allow dead code elimination of the cast - // type. - if (castType.isClassType() - && castType.isAlwaysNull(appViewWithLiveness) - && !outValue.hasDebugUsers()) { - // Replace all usages of the out-value by null. - it.previous(); - Value nullValue = it.insertConstNullInstruction(code, options); - it.next(); - checkCast.outValue().replaceUsers(nullValue); - affectedValues.addAll(nullValue.affectedValues()); - - // Replace the check-cast instruction by throwClassCastExceptionIfNotNull(). - UtilityMethodForCodeOptimizations throwClassCastExceptionIfNotNullMethod = - UtilityMethodsForCodeOptimizations.synthesizeThrowClassCastExceptionIfNotNullMethod( - appView, methodProcessor.getEventConsumer(), methodProcessingContext); - throwClassCastExceptionIfNotNullMethod.optimize(methodProcessor); - InvokeStatic replacement = - InvokeStatic.builder() - .setMethod(throwClassCastExceptionIfNotNullMethod.getMethod()) - .setSingleArgument(checkCast.object()) - .setPosition(checkCast) - .build(); - it.replaceCurrentInstruction(replacement); - assert replacement.lookupSingleTarget(appView, context) != null; - return RemoveCheckCastInstructionIfTrivialResult.REMOVED_CAST_DO_NARROW; - } - - // If the cast is guaranteed to succeed and only there to ensure the program type checks, then - // check if the program would still type check after removing the cast. - if (checkCast.isSafeCheckCast() - || checkCast - .getFirstOperand() - .getDynamicType(appViewWithLiveness) - .getDynamicUpperBoundType() - .lessThanOrEqualUpToNullability(castTypeLattice, appView)) { - TypeElement useType = - TypeUtils.computeUseType(appViewWithLiveness, context, checkCast.outValue()); - if (inTypeLattice.lessThanOrEqualUpToNullability(useType, appView)) { - return RemoveCheckCastInstructionIfTrivialResult.REMOVED_CAST_DO_NARROW; - } - } - - // Otherwise, keep the checkcast to preserve verification errors. E.g., down-cast: - // A < B < C - // c = ... // Even though we know c is of type A, - // a' = (B) c; // (this could be removed, since chained below.) - // a'' = (A) a'; // this should remain for runtime verification. - assert !inTypeLattice.isDefinitelyNull() || (inValue.isPhi() && !inTypeLattice.isNullType()); - assert outTypeLattice.equalUpToNullability(castTypeLattice); - return RemoveCheckCastInstructionIfTrivialResult.NO_REMOVALS; - } - - // Returns true if the given instance-of instruction was removed. - private boolean removeInstanceOfInstructionIfTrivial( - AppView<AppInfoWithLiveness> appViewWithLiveness, - InstanceOf instanceOf, - InstructionListIterator it, - IRCode code) { - ProgramMethod context = code.context(); - - // If the instance-of type is not accessible in the current context, we should not remove the - // instance-of instruction in order to preserve IllegalAccessError. - DexType instanceOfBaseType = instanceOf.type().toBaseType(dexItemFactory); - if (instanceOfBaseType.isClassType()) { - DexClass instanceOfClass = appView.definitionFor(instanceOfBaseType); - if (instanceOfClass == null - || AccessControl.isClassAccessible(instanceOfClass, context, appViewWithLiveness) - .isPossiblyFalse()) { - return false; - } - } - - Value inValue = instanceOf.value(); - if (!appView - .getOpenClosedInterfacesCollection() - .isDefinitelyInstanceOfStaticType(appViewWithLiveness, inValue)) { - return false; - } - - TypeElement inType = inValue.getType(); - TypeElement instanceOfType = - TypeElement.fromDexType(instanceOf.type(), inType.nullability(), appView); - Value aliasValue = inValue.getAliasedValue(); - - InstanceOfResult result = InstanceOfResult.UNKNOWN; - if (inType.isDefinitelyNull()) { - result = InstanceOfResult.FALSE; - } else if (inType.lessThanOrEqual(instanceOfType, appView) && !inType.isNullable()) { - result = InstanceOfResult.TRUE; - } else if (!aliasValue.isPhi() - && aliasValue.definition.isCreatingInstanceOrArray() - && instanceOfType.strictlyLessThan(inType, appView)) { - result = InstanceOfResult.FALSE; - } else if (appView.appInfo().hasLiveness()) { - if (instanceOf.type().isClassType() - && isNeverInstantiatedDirectlyOrIndirectly(instanceOf.type())) { - // The type of the instance-of instruction is a program class, and is never instantiated - // directly or indirectly. Thus, the in-value must be null, meaning that the instance-of - // instruction will always evaluate to false. - result = InstanceOfResult.FALSE; - } - - if (result == InstanceOfResult.UNKNOWN) { - if (inType.isClassType() - && isNeverInstantiatedDirectlyOrIndirectly(inType.asClassType().getClassType())) { - // The type of the in-value is a program class, and is never instantiated directly or - // indirectly. This, the in-value must be null, meaning that the instance-of instruction - // will always evaluate to false. - result = InstanceOfResult.FALSE; - } - } - - if (result == InstanceOfResult.UNKNOWN) { - Value aliasedValue = - inValue.getSpecificAliasedValue( - value -> - value.isDefinedByInstructionSatisfying( - Instruction::isAssumeWithDynamicTypeAssumption)); - if (aliasedValue != null) { - DynamicTypeWithUpperBound dynamicType = - aliasedValue.getDefinition().asAssume().getDynamicTypeAssumption().getDynamicType(); - Nullability nullability = dynamicType.getNullability(); - if (nullability.isDefinitelyNull()) { - result = InstanceOfResult.FALSE; - } else if (dynamicType.getDynamicUpperBoundType().lessThanOrEqual(instanceOfType, appView) - && (!inType.isNullable() || !nullability.isNullable())) { - result = InstanceOfResult.TRUE; - } - } - } - } - if (result != InstanceOfResult.UNKNOWN) { - ConstNumber newInstruction = - new ConstNumber( - new Value( - code.valueNumberGenerator.next(), - TypeElement.getInt(), - instanceOf.outValue().getLocalInfo()), - result == InstanceOfResult.TRUE ? 1 : 0); - it.replaceCurrentInstruction(newInstruction); - return true; - } - return false; - } - - private boolean isNeverInstantiatedDirectlyOrIndirectly(DexType type) { - assert appView.appInfo().hasLiveness(); - assert type.isClassType(); - DexProgramClass clazz = asProgramClassOrNull(appView.definitionFor(type)); - return clazz != null - && !appView.appInfo().withLiveness().isInstantiatedDirectlyOrIndirectly(clazz); - } - public static void removeOrReplaceByDebugLocalWrite( Instruction currentInstruction, InstructionListIterator it, Value inValue, Value outValue) { if (outValue.hasLocalInfo() && outValue.getLocalInfo() != inValue.getLocalInfo()) { @@ -1688,6 +470,53 @@ assert code.isConsistentSSA(appView); } + public void rewriteKnownArrayLengthCalls(IRCode code) { + InstructionListIterator iterator = code.instructionListIterator(); + while (iterator.hasNext()) { + Instruction current = iterator.next(); + if (!current.isArrayLength()) { + continue; + } + + ArrayLength arrayLength = current.asArrayLength(); + if (arrayLength.hasOutValue() && arrayLength.outValue().hasLocalInfo()) { + continue; + } + + Value array = arrayLength.array().getAliasedValue(); + if (array.isPhi() || !arrayLength.array().isNeverNull() || array.hasLocalInfo()) { + continue; + } + + AbstractValue abstractValue = array.getAbstractValue(appView, code.context()); + if (!abstractValue.hasKnownArrayLength() && !array.isNeverNull()) { + continue; + } + Instruction arrayDefinition = array.getDefinition(); + assert arrayDefinition != null; + + Set<Phi> phiUsers = arrayLength.outValue().uniquePhiUsers(); + if (arrayDefinition.isNewArrayEmpty()) { + Value size = arrayDefinition.asNewArrayEmpty().size(); + arrayLength.outValue().replaceUsers(size); + iterator.removeOrReplaceByDebugLocalRead(); + } else if (arrayDefinition.isNewArrayFilledData()) { + long size = arrayDefinition.asNewArrayFilledData().size; + if (size > Integer.MAX_VALUE) { + continue; + } + iterator.replaceCurrentInstructionWithConstInt(code, (int) size); + } else if (abstractValue.hasKnownArrayLength()) { + iterator.replaceCurrentInstructionWithConstInt(code, abstractValue.getKnownArrayLength()); + } else { + continue; + } + + phiUsers.forEach(Phi::removeTrivialPhi); + } + assert code.isConsistentSSA(appView); + } + /** * If an instruction is known to be a binop/lit8 or binop//lit16 instruction, update the * instruction to use its own constant that will be defined just before the instruction. This @@ -2105,431 +934,6 @@ } } - private short[] computeArrayFilledData(Value[] values, int size, int elementSize) { - for (Value v : values) { - if (!v.isConstant()) { - return null; - } - } - if (elementSize == 1) { - short[] result = new short[(size + 1) / 2]; - for (int i = 0; i < size; i += 2) { - short value = - (short) (values[i].getConstInstruction().asConstNumber().getIntValue() & 0xFF); - if (i + 1 < size) { - value |= - (short) - ((values[i + 1].getConstInstruction().asConstNumber().getIntValue() & 0xFF) << 8); - } - result[i / 2] = value; - } - return result; - } - assert elementSize == 2 || elementSize == 4 || elementSize == 8; - int shortsPerConstant = elementSize / 2; - short[] result = new short[size * shortsPerConstant]; - for (int i = 0; i < size; i++) { - long value = values[i].getConstInstruction().asConstNumber().getRawValue(); - for (int part = 0; part < shortsPerConstant; part++) { - result[i * shortsPerConstant + part] = (short) ((value >> (16 * part)) & 0xFFFFL); - } - } - return result; - } - - private static class FilledArrayConversionInfo { - - Value[] values; - List<ArrayPut> arrayPutsToRemove; - LinearFlowInstructionListIterator lastArrayPutIterator; - - public FilledArrayConversionInfo(int size) { - values = new Value[size]; - arrayPutsToRemove = new ArrayList<>(size); - } - } - - private FilledArrayConversionInfo computeConversionInfo( - FilledArrayCandidate candidate, LinearFlowInstructionListIterator it) { - NewArrayEmpty newArrayEmpty = candidate.newArrayEmpty; - assert it.peekPrevious() == newArrayEmpty; - Value arrayValue = newArrayEmpty.outValue(); - int size = candidate.size; - - // aput-object allows any object for arrays of interfaces, but new-filled-array fails to verify - // if types require a cast. - // TODO(b/246971330): Check if adding a checked-cast would have the same observable result. E.g. - // if aput-object throws a ClassCastException if given an object that does not implement the - // desired interface, then we could add check-cast instructions for arguments we're not sure - // about. - DexType elementType = newArrayEmpty.type.toDimensionMinusOneType(dexItemFactory); - boolean needsTypeCheck = - !elementType.isPrimitiveType() && elementType != dexItemFactory.objectType; - - FilledArrayConversionInfo info = new FilledArrayConversionInfo(size); - Value[] values = info.values; - int remaining = size; - Set<Instruction> users = newArrayEmpty.outValue().uniqueUsers(); - while (it.hasNext()) { - Instruction instruction = it.next(); - BasicBlock block = instruction.getBlock(); - // If we encounter an instruction that can throw an exception we need to bail out of the - // optimization so that we do not transform half-initialized arrays into fully initialized - // arrays on exceptional edges. If the block has no handlers it is not observable so - // we perform the rewriting. - if (block.hasCatchHandlers() && instruction.instructionInstanceCanThrow()) { - return null; - } - if (!users.contains(instruction)) { - // If any instruction can transfer control between the new-array and the last array put - // then it is not safe to move the new array to the point of the last put. - if (block.hasCatchHandlers() && instruction.instructionTypeCanThrow()) { - return null; - } - continue; - } - ArrayPut arrayPut = instruction.asArrayPut(); - // If the initialization sequence is broken by another use we cannot use a fill-array-data - // instruction. - if (arrayPut == null || arrayPut.array() != arrayValue) { - return null; - } - if (!arrayPut.index().isConstNumber()) { - return null; - } - if (arrayPut.instructionInstanceCanThrow()) { - assert false; - return null; - } - int index = arrayPut.index().getConstInstruction().asConstNumber().getIntValue(); - if (index < 0 || index >= values.length) { - return null; - } - if (values[index] != null) { - return null; - } - Value value = arrayPut.value(); - if (needsTypeCheck && !value.isAlwaysNull(appView)) { - DexType valueDexType = value.getType().asReferenceType().toDexType(dexItemFactory); - if (elementType.isArrayType()) { - if (elementType != valueDexType) { - return null; - } - } else if (valueDexType.isArrayType()) { - // isSubtype asserts for this case. - return null; - } else if (valueDexType.isNullValueType()) { - // Assume instructions can cause value.isAlwaysNull() == false while the DexType is null. - // TODO(b/246971330): Figure out how to write a test in SimplifyArrayConstructionTest - // that hits this case. - } else { - // TODO(b/246971330): When in d8 mode, we might still be able to see if this is true for - // library types (which this helper does not do). - if (appView.isSubtype(valueDexType, elementType).isPossiblyFalse()) { - return null; - } - } - } - info.arrayPutsToRemove.add(arrayPut); - values[index] = value; - --remaining; - if (remaining == 0) { - info.lastArrayPutIterator = it; - return info; - } - } - return null; - } - - private static class FilledArrayCandidate { - - final NewArrayEmpty newArrayEmpty; - final int size; - final boolean encodeAsFilledNewArray; - - public FilledArrayCandidate( - NewArrayEmpty newArrayEmpty, int size, boolean encodeAsFilledNewArray) { - assert size > 0; - this.newArrayEmpty = newArrayEmpty; - this.size = size; - this.encodeAsFilledNewArray = encodeAsFilledNewArray; - } - - public boolean useFilledNewArray() { - return encodeAsFilledNewArray; - } - - public boolean useFilledArrayData() { - return !useFilledNewArray(); - } - } - - private FilledArrayCandidate computeFilledArrayCandidate( - Instruction instruction, RewriteArrayOptions options) { - NewArrayEmpty newArrayEmpty = instruction.asNewArrayEmpty(); - if (newArrayEmpty == null) { - return null; - } - if (instruction.getLocalInfo() != null) { - return null; - } - if (!newArrayEmpty.size().isConstant()) { - return null; - } - int size = newArrayEmpty.size().getConstInstruction().asConstNumber().getIntValue(); - if (!options.isPotentialSize(size)) { - return null; - } - DexType arrayType = newArrayEmpty.type; - boolean encodeAsFilledNewArray = canUseFilledNewArray(arrayType, size, options); - if (!encodeAsFilledNewArray && !canUseFilledArrayData(arrayType, size, options)) { - return null; - } - // Check that all arguments to the array is the array type or that the array is type Object[]. - if (!options.canUseSubTypesInFilledNewArray() - && arrayType != dexItemFactory.objectArrayType - && !arrayType.isPrimitiveArrayType()) { - DexType elementType = arrayType.toArrayElementType(dexItemFactory); - for (Instruction uniqueUser : newArrayEmpty.outValue().uniqueUsers()) { - if (uniqueUser.isArrayPut() - && uniqueUser.asArrayPut().array() == newArrayEmpty.outValue() - && !checkTypeOfArrayPut(uniqueUser.asArrayPut(), elementType)) { - return null; - } - } - } - return new FilledArrayCandidate(newArrayEmpty, size, encodeAsFilledNewArray); - } - - private boolean checkTypeOfArrayPut(ArrayPut arrayPut, DexType elementType) { - TypeElement valueType = arrayPut.value().getType(); - if (!valueType.isPrimitiveType() && elementType == dexItemFactory.objectType) { - return true; - } - if (valueType.isNullType() && !elementType.isPrimitiveType()) { - return true; - } - if (elementType.isArrayType()) { - if (valueType.isNullType()) { - return true; - } - ArrayTypeElement arrayTypeElement = valueType.asArrayType(); - if (arrayTypeElement == null - || arrayTypeElement.getNesting() != elementType.getNumberOfLeadingSquareBrackets()) { - return false; - } - valueType = arrayTypeElement.getBaseType(); - elementType = elementType.toBaseType(dexItemFactory); - } - assert !valueType.isArrayType(); - assert !elementType.isArrayType(); - if (valueType.isPrimitiveType() && !elementType.isPrimitiveType()) { - return false; - } - if (valueType.isPrimitiveType()) { - return true; - } - DexClass clazz = appView.definitionFor(elementType); - if (clazz == null) { - return false; - } - return clazz.isInterface() || valueType.isClassType(elementType); - } - - private boolean canUseFilledNewArray(DexType arrayType, int size, RewriteArrayOptions options) { - if (size < options.minSizeForFilledNewArray) { - return false; - } - // filled-new-array is implemented only for int[] and Object[]. - // For int[], using filled-new-array is usually smaller than filled-array-data. - // filled-new-array supports up to 5 registers before it's filled-new-array/range. - if (!arrayType.isPrimitiveArrayType()) { - if (size > options.maxSizeForFilledNewArrayOfReferences) { - return false; - } - if (arrayType == dexItemFactory.stringArrayType) { - return options.canUseFilledNewArrayOfStrings(); - } - if (!options.canUseFilledNewArrayOfNonStringObjects()) { - return false; - } - if (!options.canUseFilledNewArrayOfArrays() - && arrayType.getNumberOfLeadingSquareBrackets() > 1) { - return false; - } - return true; - } - if (arrayType == dexItemFactory.intArrayType) { - return size <= options.maxSizeForFilledNewArrayOfInts; - } - return false; - } - - private boolean canUseFilledArrayData(DexType arrayType, int size, RewriteArrayOptions options) { - // If there is only one element it is typically smaller to generate the array put - // instruction instead of fill array data. - if (size < options.minSizeForFilledArrayData || size > options.maxSizeForFilledArrayData) { - return false; - } - return arrayType.isPrimitiveArrayType(); - } - - /** - * Replace new-array followed by stores of constants to all entries with new-array and - * fill-array-data / filled-new-array. - * - * <p>The format of the new-array and its puts must be of the form: - * - * <pre> - * v0 <- new-array T vSize - * ... - * array-put v0 vValue1 vIndex1 - * ... - * array-put v0 vValueN vIndexN - * </pre> - * - * <p>The flow between the array v0 and its puts must be linear with no other uses of v0 besides - * the array-put instructions, thus any no intermediate instruction (... above) must use v0 and - * also cannot have catch handlers that would transfer out control (those could then have uses of - * v0). - * - * <p>The allocation of the new-array can itself have catch handlers, in which case, those are to - * remain active on the translated code. Translated code can have two forms. - * - * <p>The first is using the original array allocation and filling in its data if it can be - * encoded: - * - * <pre> - * v0 <- new-array T vSize - * filled-array-data v0 - * ... - * ... - * </pre> - * - * The data payload is encoded directly in the instruction so no dependencies are needed for - * filling the data array. Thus, the fill is inserted at the point of the allocation. If the - * allocation has catch handlers its block must be split and the handlers put on the fill - * instruction too. This is correct only because there are no exceptional transfers in (...) that - * could observe the early initialization of the data. - * - * <p>The second is using filled-new-array and has the form: - * - * <pre> - * ... - * ... - * v0 <- filled-new-array T vValue1 ... vValueN - * </pre> - * - * Here the correctness relies on no exceptional transfers in (...) that could observe the missing - * allocation of the array. The late allocation ensures that the values are available at - * allocation time. If the original allocation has catch handlers then the new allocation needs to - * link those too. In general that may require splitting the block twice so that the new - * allocation is the single throwing instruction in its block. - */ - public void simplifyArrayConstruction(IRCode code) { - if (options.isGeneratingClassFiles()) { - return; - } - WorkList<BasicBlock> worklist = WorkList.newIdentityWorkList(code.blocks); - while (worklist.hasNext()) { - BasicBlock block = worklist.next(); - simplifyArrayConstructionBlock(block, worklist, code, options); - } - } - - private void simplifyArrayConstructionBlock( - BasicBlock block, WorkList<BasicBlock> worklist, IRCode code, InternalOptions options) { - RewriteArrayOptions rewriteOptions = options.rewriteArrayOptions(); - InstructionListIterator it = block.listIterator(code); - while (it.hasNext()) { - FilledArrayCandidate candidate = computeFilledArrayCandidate(it.next(), rewriteOptions); - if (candidate == null) { - continue; - } - FilledArrayConversionInfo info = - computeConversionInfo( - candidate, new LinearFlowInstructionListIterator(code, block, it.nextIndex())); - if (info == null) { - continue; - } - - Instruction instructionAfterCandidate = it.peekNext(); - NewArrayEmpty newArrayEmpty = candidate.newArrayEmpty; - DexType arrayType = newArrayEmpty.type; - int size = candidate.size; - Set<Instruction> instructionsToRemove = SetUtils.newIdentityHashSet(size + 1); - if (candidate.useFilledNewArray()) { - assert newArrayEmpty.getLocalInfo() == null; - Instruction lastArrayPut = info.lastArrayPutIterator.peekPrevious(); - Value invokeValue = code.createValue(newArrayEmpty.getOutType(), null); - InvokeNewArray invoke = - new InvokeNewArray(arrayType, invokeValue, Arrays.asList(info.values)); - invoke.setPosition(lastArrayPut.getPosition()); - for (Value value : newArrayEmpty.inValues()) { - value.removeUser(newArrayEmpty); - } - newArrayEmpty.outValue().replaceUsers(invokeValue); - instructionsToRemove.add(newArrayEmpty); - - boolean originalAllocationPointHasHandlers = block.hasCatchHandlers(); - boolean insertionPointHasHandlers = lastArrayPut.getBlock().hasCatchHandlers(); - - if (!insertionPointHasHandlers && !originalAllocationPointHasHandlers) { - info.lastArrayPutIterator.add(invoke); - } else { - BasicBlock insertionBlock = info.lastArrayPutIterator.split(code); - if (originalAllocationPointHasHandlers) { - if (!insertionBlock.isTrivialGoto()) { - BasicBlock blockAfterInsertion = insertionBlock.listIterator(code).split(code); - assert insertionBlock.isTrivialGoto(); - worklist.addIfNotSeen(blockAfterInsertion); - } - insertionBlock.moveCatchHandlers(block); - } else { - worklist.addIfNotSeen(insertionBlock); - } - insertionBlock.listIterator(code).add(invoke); - } - } else { - assert candidate.useFilledArrayData(); - int elementSize = arrayType.elementSizeForPrimitiveArrayType(); - short[] contents = computeArrayFilledData(info.values, size, elementSize); - if (contents == null) { - continue; - } - // fill-array-data requires the new-array-empty instruction to remain, as it does not - // itself create an array. - NewArrayFilledData fillArray = - new NewArrayFilledData(newArrayEmpty.outValue(), elementSize, size, contents); - fillArray.setPosition(newArrayEmpty.getPosition()); - BasicBlock newBlock = - it.addThrowingInstructionToPossiblyThrowingBlock(code, null, fillArray, options); - if (newBlock != null) { - worklist.addIfNotSeen(newBlock); - } - } - - instructionsToRemove.addAll(info.arrayPutsToRemove); - Set<BasicBlock> visitedBlocks = Sets.newIdentityHashSet(); - for (Instruction instruction : instructionsToRemove) { - BasicBlock ownerBlock = instruction.getBlock(); - // If owner block is null, then the instruction has been removed already. We can't rely on - // just having the block pointer nulled, so the visited blocks guards reprocessing. - if (ownerBlock != null && visitedBlocks.add(ownerBlock)) { - InstructionListIterator removeIt = ownerBlock.listIterator(code); - while (removeIt.hasNext()) { - if (instructionsToRemove.contains(removeIt.next())) { - removeIt.removeOrReplaceByDebugLocalRead(); - } - } - } - } - - // The above has invalidated the block iterator so reset it and continue. - it = block.listIterator(code, instructionAfterCandidate); - } - } - // TODO(mikaelpeltier) Manage that from and to instruction do not belong to the same block. private static boolean hasLocalOrLineChangeBetween( Instruction from, Instruction to, DexString localVar) { @@ -2597,309 +1001,6 @@ } } - static class ControlFlowSimplificationResult { - - private boolean anyAffectedValues; - private boolean anySimplifications; - - private ControlFlowSimplificationResult(boolean anyAffectedValues, boolean anySimplifications) { - assert !anyAffectedValues || anySimplifications; - this.anyAffectedValues = anyAffectedValues; - this.anySimplifications = anySimplifications; - } - - public boolean anyAffectedValues() { - return anyAffectedValues; - } - - public boolean anySimplifications() { - return anySimplifications; - } - } - - public boolean simplifyControlFlow(IRCode code) { - boolean anyAffectedValues = rewriteSwitch(code); - anyAffectedValues |= simplifyIf(code).anyAffectedValues(); - return anyAffectedValues; - } - - public ControlFlowSimplificationResult simplifyIf(IRCode code) { - BasicBlockBehavioralSubsumption behavioralSubsumption = - new BasicBlockBehavioralSubsumption(appView, code); - boolean simplified = false; - for (BasicBlock block : code.blocks) { - // Skip removed (= unreachable) blocks. - if (block.getNumber() != 0 && block.getPredecessors().isEmpty()) { - continue; - } - if (block.exit().isIf()) { - flipIfBranchesIfNeeded(code, block); - if (rewriteIfWithConstZero(code, block)) { - simplified = true; - } - - if (simplifyKnownBooleanCondition(code, block)) { - simplified = true; - if (!block.exit().isIf()) { - continue; - } - } - - // Simplify if conditions when possible. - If theIf = block.exit().asIf(); - if (theIf.isZeroTest()) { - if (simplifyIfZeroTest(code, block, theIf)) { - simplified = true; - continue; - } - } else { - if (simplifyNonIfZeroTest(code, block, theIf)) { - simplified = true; - continue; - } - } - - // Unable to determine which branch will be taken. Check if the true target can safely be - // rewritten to the false target. - if (behavioralSubsumption.isSubsumedBy( - theIf.inValues().get(0), theIf.getTrueTarget(), theIf.fallthroughBlock())) { - simplifyIfWithKnownCondition(code, block, theIf, theIf.fallthroughBlock()); - simplified = true; - } - } - } - Set<Value> affectedValues = code.removeUnreachableBlocks(); - if (!affectedValues.isEmpty()) { - new TypeAnalysis(appView).narrowing(affectedValues); - } - assert code.isConsistentSSA(appView); - return new ControlFlowSimplificationResult(!affectedValues.isEmpty(), simplified); - } - - private boolean simplifyIfZeroTest(IRCode code, BasicBlock block, If theIf) { - Value lhs = theIf.lhs(); - Value lhsRoot = lhs.getAliasedValue(); - if (lhsRoot.isConstNumber()) { - ConstNumber cond = lhsRoot.getConstInstruction().asConstNumber(); - BasicBlock target = theIf.targetFromCondition(cond); - simplifyIfWithKnownCondition(code, block, theIf, target); - return true; - } - - if (theIf.isNullTest()) { - assert theIf.getType() == IfType.EQ || theIf.getType() == IfType.NE; - - if (lhs.isAlwaysNull(appView)) { - simplifyIfWithKnownCondition(code, block, theIf, theIf.targetFromNullObject()); - return true; - } - - if (lhs.isNeverNull()) { - simplifyIfWithKnownCondition(code, block, theIf, theIf.targetFromNonNullObject()); - return true; - } - } - - if (theIf.getType() == IfType.EQ || theIf.getType() == IfType.NE) { - AbstractValue lhsAbstractValue = lhs.getAbstractValue(appView, code.context()); - if (lhsAbstractValue.isConstantOrNonConstantNumberValue() - && !lhsAbstractValue.asConstantOrNonConstantNumberValue().containsInt(0)) { - // Value doesn't contain zero at all. - simplifyIfWithKnownCondition(code, block, theIf, theIf.targetFromCondition(1)); - return true; - } - } - - if (lhs.hasValueRange()) { - LongInterval interval = lhs.getValueRange(); - if (!interval.containsValue(0)) { - // Interval doesn't contain zero at all. - int sign = Long.signum(interval.getMin()); - simplifyIfWithKnownCondition(code, block, theIf, sign); - return true; - } - - // Interval contains zero. - switch (theIf.getType()) { - case GE: - case LT: - // [a, b] >= 0 is always true if a >= 0. - // [a, b] < 0 is always false if a >= 0. - // In both cases a zero condition takes the right branch. - if (interval.getMin() == 0) { - simplifyIfWithKnownCondition(code, block, theIf, 0); - return true; - } - break; - - case LE: - case GT: - // [a, b] <= 0 is always true if b <= 0. - // [a, b] > 0 is always false if b <= 0. - // In both cases a zero condition takes the right branch. - if (interval.getMax() == 0) { - simplifyIfWithKnownCondition(code, block, theIf, 0); - return true; - } - break; - - case EQ: - case NE: - // Only a single element interval [0, 0] can be dealt with here. - // Such intervals should have been replaced by constants. - assert !interval.isSingleValue(); - break; - } - } - return false; - } - - private boolean simplifyNonIfZeroTest(IRCode code, BasicBlock block, If theIf) { - Value lhs = theIf.lhs(); - Value lhsRoot = lhs.getAliasedValue(); - Value rhs = theIf.rhs(); - Value rhsRoot = rhs.getAliasedValue(); - if (lhsRoot == rhsRoot) { - // Comparing the same value. - simplifyIfWithKnownCondition(code, block, theIf, theIf.targetFromCondition(0)); - return true; - } - - if (lhsRoot.isDefinedByInstructionSatisfying(Instruction::isCreatingInstanceOrArray) - && rhsRoot.isDefinedByInstructionSatisfying(Instruction::isCreatingInstanceOrArray)) { - // Comparing two newly created objects. - assert theIf.getType() == IfType.EQ || theIf.getType() == IfType.NE; - simplifyIfWithKnownCondition(code, block, theIf, theIf.targetFromCondition(1)); - return true; - } - - if (lhsRoot.isConstNumber() && rhsRoot.isConstNumber()) { - // Zero test with a constant of comparison between between two constants. - ConstNumber left = lhsRoot.getConstInstruction().asConstNumber(); - ConstNumber right = rhsRoot.getConstInstruction().asConstNumber(); - BasicBlock target = theIf.targetFromCondition(left, right); - simplifyIfWithKnownCondition(code, block, theIf, target); - return true; - } - - if (theIf.getType() == IfType.EQ || theIf.getType() == IfType.NE) { - AbstractValue lhsAbstractValue = lhs.getAbstractValue(appView, code.context()); - AbstractValue rhsAbstractValue = rhs.getAbstractValue(appView, code.context()); - if (lhsAbstractValue.isConstantOrNonConstantNumberValue() - && rhsAbstractValue.isConstantOrNonConstantNumberValue()) { - ConstantOrNonConstantNumberValue lhsNumberValue = - lhsAbstractValue.asConstantOrNonConstantNumberValue(); - ConstantOrNonConstantNumberValue rhsNumberValue = - rhsAbstractValue.asConstantOrNonConstantNumberValue(); - if (!lhsNumberValue.mayOverlapWith(rhsNumberValue)) { - // No overlap. - simplifyIfWithKnownCondition(code, block, theIf, 1); - return true; - } - } - } - - if (lhs.hasValueRange() && rhs.hasValueRange()) { - // Zero test with a value range, or comparison between between two values, - // each with a value ranges. - LongInterval leftRange = lhs.getValueRange(); - LongInterval rightRange = rhs.getValueRange(); - // Two overlapping ranges. Check for single point overlap. - if (!leftRange.overlapsWith(rightRange)) { - // No overlap. - int cond = Long.signum(leftRange.getMin() - rightRange.getMin()); - simplifyIfWithKnownCondition(code, block, theIf, cond); - return true; - } - - // The two intervals overlap. We can simplify if they overlap at the end points. - switch (theIf.getType()) { - case LT: - case GE: - // [a, b] < [c, d] is always false when a == d. - // [a, b] >= [c, d] is always true when a == d. - // In both cases 0 condition will choose the right branch. - if (leftRange.getMin() == rightRange.getMax()) { - simplifyIfWithKnownCondition(code, block, theIf, 0); - return true; - } - break; - case GT: - case LE: - // [a, b] > [c, d] is always false when b == c. - // [a, b] <= [c, d] is always true when b == c. - // In both cases 0 condition will choose the right branch. - if (leftRange.getMax() == rightRange.getMin()) { - simplifyIfWithKnownCondition(code, block, theIf, 0); - return true; - } - break; - case EQ: - case NE: - // Since there is overlap EQ and NE cannot be determined. - break; - } - } - - if (theIf.getType() == IfType.EQ || theIf.getType() == IfType.NE) { - ProgramMethod context = code.context(); - AbstractValue abstractValue = lhs.getAbstractValue(appView, context); - if (abstractValue.isSingleConstClassValue()) { - AbstractValue otherAbstractValue = rhs.getAbstractValue(appView, context); - if (otherAbstractValue.isSingleConstClassValue()) { - SingleConstClassValue singleConstClassValue = abstractValue.asSingleConstClassValue(); - SingleConstClassValue otherSingleConstClassValue = - otherAbstractValue.asSingleConstClassValue(); - simplifyIfWithKnownCondition( - code, - block, - theIf, - BooleanUtils.intValue( - singleConstClassValue.getType() != otherSingleConstClassValue.getType())); - return true; - } - return false; - } - - if (abstractValue.isSingleFieldValue()) { - AbstractValue otherAbstractValue = rhs.getAbstractValue(appView, context); - if (otherAbstractValue.isSingleFieldValue()) { - SingleFieldValue singleFieldValue = abstractValue.asSingleFieldValue(); - SingleFieldValue otherSingleFieldValue = otherAbstractValue.asSingleFieldValue(); - if (singleFieldValue.getField() == otherSingleFieldValue.getField()) { - simplifyIfWithKnownCondition(code, block, theIf, 0); - return true; - } - - DexClass holder = appView.definitionForHolder(singleFieldValue.getField()); - DexEncodedField field = singleFieldValue.getField().lookupOnClass(holder); - if (field != null && field.isEnum()) { - DexClass otherHolder = appView.definitionForHolder(otherSingleFieldValue.getField()); - DexEncodedField otherField = - otherSingleFieldValue.getField().lookupOnClass(otherHolder); - if (otherField != null && otherField.isEnum()) { - simplifyIfWithKnownCondition(code, block, theIf, 1); - return true; - } - } - } - } - } - - return false; - } - - private void simplifyIfWithKnownCondition( - IRCode code, BasicBlock block, If theIf, BasicBlock target) { - BasicBlock deadTarget = - target == theIf.getTrueTarget() ? theIf.fallthroughBlock() : theIf.getTrueTarget(); - rewriteIfToGoto(code, block, theIf, target, deadTarget); - } - - private void simplifyIfWithKnownCondition(IRCode code, BasicBlock block, If theIf, int cond) { - simplifyIfWithKnownCondition(code, block, theIf, theIf.targetFromCondition(cond)); - } - /** * This optimization exploits that we can sometimes learn the constant value of an SSA value that * flows into an if-eq of if-neq instruction. @@ -3380,152 +1481,6 @@ assert code.isConsistentSSA(appView); } - /* Identify simple diamond shapes converting boolean true/false to 1/0. We consider the forms: - * - * (1) - * - * [dbg pos x] [dbg pos x] - * ifeqz booleanValue ifnez booleanValue - * / \ / \ - * [dbg pos x][dbg pos x] [dbg pos x][dbg pos x] - * [const 0] [const 1] [const 1] [const 0] - * goto goto goto goto - * \ / \ / - * phi(0, 1) phi(1, 0) - * - * which can be replaced by a fallthrough and the phi value can be replaced - * with the boolean value itself. - * - * (2) - * - * [dbg pos x] [dbg pos x] - * ifeqz booleanValue ifnez booleanValue - * / \ / \ - * [dbg pos x][dbg pos x] [dbg pos x][dbg pos x] - * [const 1] [const 0] [const 0] [const 1] - * goto goto goto goto - * \ / \ / - * phi(1, 0) phi(0, 1) - * - * which can be replaced by a fallthrough and the phi value can be replaced - * by an xor instruction which is smaller. - */ - private boolean simplifyKnownBooleanCondition(IRCode code, BasicBlock block) { - If theIf = block.exit().asIf(); - Value testValue = theIf.inValues().get(0); - if (theIf.isZeroTest() && testValue.knownToBeBoolean()) { - BasicBlock trueBlock = theIf.getTrueTarget(); - BasicBlock falseBlock = theIf.fallthroughBlock(); - if (isBlockSupportedBySimplifyKnownBooleanCondition(trueBlock) && - isBlockSupportedBySimplifyKnownBooleanCondition(falseBlock) && - trueBlock.getSuccessors().get(0) == falseBlock.getSuccessors().get(0)) { - BasicBlock targetBlock = trueBlock.getSuccessors().get(0); - if (targetBlock.getPredecessors().size() == 2) { - int trueIndex = targetBlock.getPredecessors().indexOf(trueBlock); - int falseIndex = trueIndex == 0 ? 1 : 0; - int deadPhis = 0; - // Locate the phis that have the same value as the boolean and replace them - // by the boolean in all users. - for (Phi phi : targetBlock.getPhis()) { - Value trueValue = phi.getOperand(trueIndex); - Value falseValue = phi.getOperand(falseIndex); - if (trueValue.isConstNumber() && falseValue.isConstNumber()) { - ConstNumber trueNumber = trueValue.getConstInstruction().asConstNumber(); - ConstNumber falseNumber = falseValue.getConstInstruction().asConstNumber(); - if ((theIf.getType() == IfType.EQ - && trueNumber.isIntegerZero() - && falseNumber.isIntegerOne()) - || (theIf.getType() == IfType.NE - && trueNumber.isIntegerOne() - && falseNumber.isIntegerZero())) { - phi.replaceUsers(testValue); - deadPhis++; - } else if ((theIf.getType() == IfType.NE - && trueNumber.isIntegerZero() - && falseNumber.isIntegerOne()) - || (theIf.getType() == IfType.EQ - && trueNumber.isIntegerOne() - && falseNumber.isIntegerZero())) { - Value newOutValue = code.createValue(phi.getType(), phi.getLocalInfo()); - ConstNumber cstToUse = trueNumber.isIntegerOne() ? trueNumber : falseNumber; - BasicBlock phiBlock = phi.getBlock(); - Position phiPosition = phiBlock.getPosition(); - int insertIndex = 0; - if (cstToUse.getBlock() == trueBlock || cstToUse.getBlock() == falseBlock) { - // The constant belongs to the block to remove, create a new one. - cstToUse = ConstNumber.copyOf(code, cstToUse); - cstToUse.setBlock(phiBlock); - cstToUse.setPosition(phiPosition); - phiBlock.getInstructions().add(insertIndex++, cstToUse); - } - phi.replaceUsers(newOutValue); - Instruction newInstruction = - Xor.create(NumericType.INT, newOutValue, testValue, cstToUse.outValue()); - newInstruction.setBlock(phiBlock); - // The xor is replacing a phi so it does not have an actual position. - newInstruction.setPosition(phiPosition); - phiBlock.listIterator(code, insertIndex).add(newInstruction); - deadPhis++; - } - } - } - // If all phis were removed, there is no need for the diamond shape anymore - // and it can be rewritten to a goto to one of the branches. - if (deadPhis == targetBlock.getPhis().size()) { - rewriteIfToGoto(code, block, theIf, trueBlock, falseBlock); - return true; - } - return deadPhis > 0; - } - } - } - return false; - } - - private boolean isBlockSupportedBySimplifyKnownBooleanCondition(BasicBlock b) { - if (b.isTrivialGoto()) { - return true; - } - - int instructionSize = b.getInstructions().size(); - if (b.exit().isGoto() && (instructionSize == 2 || instructionSize == 3)) { - Instruction constInstruction = b.getInstructions().get(instructionSize - 2); - if (constInstruction.isConstNumber()) { - if (!constInstruction.asConstNumber().isIntegerOne() && - !constInstruction.asConstNumber().isIntegerZero()) { - return false; - } - if (instructionSize == 2) { - return true; - } - Instruction firstInstruction = b.getInstructions().getFirst(); - if (firstInstruction.isDebugPosition()) { - assert b.getPredecessors().size() == 1; - BasicBlock predecessorBlock = b.getPredecessors().get(0); - InstructionIterator it = predecessorBlock.iterator(predecessorBlock.exit()); - Instruction previousPosition = null; - while (it.hasPrevious() && !(previousPosition = it.previous()).isDebugPosition()) { - // Intentionally empty. - } - if (previousPosition != null) { - return previousPosition.getPosition() == firstInstruction.getPosition(); - } - } - } - } - - return false; - } - - private void rewriteIfToGoto( - IRCode code, BasicBlock block, If theIf, BasicBlock target, BasicBlock deadTarget) { - deadTarget.unlinkSinglePredecessorSiblingsAllowed(); - assert theIf == block.exit(); - block.replaceLastInstruction(new Goto(), code); - assert block.exit().isGoto(); - assert block.exit().asGoto().getTarget() == target; - } - private void insertNotNullCheck( BasicBlock block, InstructionListIterator iterator, @@ -3547,101 +1502,6 @@ assert block.exit().asGoto().getTarget() == target; } - private boolean rewriteIfWithConstZero(IRCode code, BasicBlock block) { - If theIf = block.exit().asIf(); - if (theIf.isZeroTest()) { - return false; - } - - Value leftValue = theIf.lhs(); - Value rightValue = theIf.rhs(); - if (leftValue.isConstNumber() || rightValue.isConstNumber()) { - if (leftValue.isConstNumber()) { - if (leftValue.getConstInstruction().asConstNumber().isZero()) { - If ifz = new If(theIf.getType().forSwappedOperands(), rightValue); - block.replaceLastInstruction(ifz, code); - assert block.exit() == ifz; - return true; - } - } else if (rightValue.getConstInstruction().asConstNumber().isZero()) { - If ifz = new If(theIf.getType(), leftValue); - block.replaceLastInstruction(ifz, code); - assert block.exit() == ifz; - return true; - } - } - - return false; - } - - private boolean flipIfBranchesIfNeeded(IRCode code, BasicBlock block) { - If theIf = block.exit().asIf(); - BasicBlock trueTarget = theIf.getTrueTarget(); - BasicBlock fallthrough = theIf.fallthroughBlock(); - assert trueTarget != fallthrough; - - if (!fallthrough.isSimpleAlwaysThrowingPath() || trueTarget.isSimpleAlwaysThrowingPath()) { - return false; - } - - // In case fall-through block always throws there is a good chance that it - // is created for error checks and 'trueTarget' represents most more common - // non-error case. Flipping the if in this case may result in faster code - // on older Android versions. - List<Value> inValues = theIf.inValues(); - If newIf = new If(theIf.getType().inverted(), inValues); - block.replaceLastInstruction(newIf, code); - block.swapSuccessors(trueTarget, fallthrough); - return true; - } - - public void rewriteKnownArrayLengthCalls(IRCode code) { - InstructionListIterator iterator = code.instructionListIterator(); - while (iterator.hasNext()) { - Instruction current = iterator.next(); - if (!current.isArrayLength()) { - continue; - } - - ArrayLength arrayLength = current.asArrayLength(); - if (arrayLength.hasOutValue() && arrayLength.outValue().hasLocalInfo()) { - continue; - } - - Value array = arrayLength.array().getAliasedValue(); - if (array.isPhi() || !arrayLength.array().isNeverNull() || array.hasLocalInfo()) { - continue; - } - - AbstractValue abstractValue = array.getAbstractValue(appView, code.context()); - if (!abstractValue.hasKnownArrayLength() && !array.isNeverNull()) { - continue; - } - Instruction arrayDefinition = array.getDefinition(); - assert arrayDefinition != null; - - Set<Phi> phiUsers = arrayLength.outValue().uniquePhiUsers(); - if (arrayDefinition.isNewArrayEmpty()) { - Value size = arrayDefinition.asNewArrayEmpty().size(); - arrayLength.outValue().replaceUsers(size); - iterator.removeOrReplaceByDebugLocalRead(); - } else if (arrayDefinition.isNewArrayFilledData()) { - long size = arrayDefinition.asNewArrayFilledData().size; - if (size > Integer.MAX_VALUE) { - continue; - } - iterator.replaceCurrentInstructionWithConstInt(code, (int) size); - } else if (abstractValue.hasKnownArrayLength()) { - iterator.replaceCurrentInstructionWithConstInt(code, abstractValue.getKnownArrayLength()); - } else { - continue; - } - - phiUsers.forEach(Phi::removeTrivialPhi); - } - assert code.isConsistentSSA(appView); - } - /** * Remove moves that are not actually used by instructions in exiting paths. These moves can arise * due to debug local info needing a particular value and the live-interval for it then moves it @@ -3891,34 +1751,6 @@ iterator.add(new InvokeVirtual(printLn, null, ImmutableList.of(out, empty))); } - public static void replaceUnusedArgumentTrivialPhis(UnusedArgument unusedArgument) { - replaceTrivialPhis(unusedArgument.outValue()); - for (Phi phiUser : unusedArgument.outValue().uniquePhiUsers()) { - phiUser.removeTrivialPhi(); - } - assert !unusedArgument.outValue().hasPhiUsers(); - } - - public static void ensureDirectStringNewToInit(IRCode code, DexItemFactory dexItemFactory) { - for (Instruction instruction : code.instructions()) { - if (instruction.isInvokeDirect()) { - InvokeDirect invoke = instruction.asInvokeDirect(); - DexMethod method = invoke.getInvokedMethod(); - if (dexItemFactory.isConstructor(method) - && method.holder == dexItemFactory.stringType - && invoke.getReceiver().isPhi()) { - NewInstance newInstance = findNewInstance(invoke.getReceiver().asPhi()); - replaceTrivialPhis(newInstance.outValue()); - if (invoke.getReceiver().isPhi()) { - throw new CompilationError( - "Failed to remove trivial phis between new-instance and <init>"); - } - newInstance.markNoSpilling(); - } - } - } - } - // The javac fix for JDK-8272564 has to be rewritten back to invoke-virtual on Object if the // method with an Object signature is not defined on the interface. See // https://bugs.openjdk.java.net/browse/JDK-8272564 @@ -3943,66 +1775,4 @@ } } } - - private static NewInstance findNewInstance(Phi phi) { - Set<Phi> seen = Sets.newIdentityHashSet(); - Set<Value> values = Sets.newIdentityHashSet(); - recursiveAddOperands(phi, seen, values); - if (values.size() != 1) { - throw new CompilationError("Failed to identify unique new-instance for <init>"); - } - Value newInstanceValue = values.iterator().next(); - if (newInstanceValue.definition == null || !newInstanceValue.definition.isNewInstance()) { - throw new CompilationError("Invalid defining value for call to <init>"); - } - return newInstanceValue.definition.asNewInstance(); - } - - private static void recursiveAddOperands(Phi phi, Set<Phi> seen, Set<Value> values) { - for (Value operand : phi.getOperands()) { - if (!operand.isPhi()) { - values.add(operand); - } else { - Phi phiOp = operand.asPhi(); - if (seen.add(phiOp)) { - recursiveAddOperands(phiOp, seen, values); - } - } - } - } - - // We compute the set of strongly connected phis making use of the out value and replace all - // trivial ones by the out value. - // This is a simplified variant of the removeRedundantPhis algorithm in Section 3.2 of: - // http://compilers.cs.uni-saarland.de/papers/bbhlmz13cc.pdf - private static void replaceTrivialPhis(Value outValue) { - List<Set<Value>> components = new SCC<Value>(Value::uniquePhiUsers).computeSCC(outValue); - for (int i = components.size() - 1; i >= 0; i--) { - Set<Value> component = components.get(i); - if (component.size() == 1 && component.iterator().next() == outValue) { - continue; - } - Set<Phi> trivialPhis = Sets.newIdentityHashSet(); - for (Value value : component) { - boolean isTrivial = true; - Phi p = value.asPhi(); - for (Value op : p.getOperands()) { - if (op != outValue && !component.contains(op)) { - isTrivial = false; - break; - } - } - if (isTrivial) { - trivialPhis.add(p); - } - } - for (Phi trivialPhi : trivialPhis) { - for (Value op : trivialPhi.getOperands()) { - op.removePhiUser(trivialPhi); - } - trivialPhi.replaceUsers(outValue); - trivialPhi.getBlock().removePhi(trivialPhi); - } - } - } }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java index 677da9c..b4fa5a6 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
@@ -41,6 +41,7 @@ import com.android.tools.r8.ir.code.Position; import com.android.tools.r8.ir.code.StaticGet; import com.android.tools.r8.ir.code.Value; +import com.android.tools.r8.ir.conversion.passes.BranchSimplifier; import com.android.tools.r8.utils.OptionalBool; import com.android.tools.r8.utils.WorkList; import com.google.common.collect.Sets; @@ -65,17 +66,16 @@ private static final int MAX_CANONICALIZED_CONSTANT = 22; private final AppView<?> appView; - private final CodeRewriter codeRewriter; + private final BranchSimplifier branchSimplifier; private final ProgramMethod context; private final IRCode code; private OptionalBool isAccessingVolatileField = OptionalBool.unknown(); private Set<InstanceGet> ineligibleInstanceGetInstructions; - public ConstantCanonicalizer( - AppView<?> appView, CodeRewriter codeRewriter, ProgramMethod context, IRCode code) { + public ConstantCanonicalizer(AppView<?> appView, ProgramMethod context, IRCode code) { this.appView = appView; - this.codeRewriter = codeRewriter; + this.branchSimplifier = new BranchSimplifier(appView); this.context = context; this.code = code; } @@ -288,7 +288,7 @@ shouldSimplifyIfs |= code.removeAllDeadAndTrivialPhis(); if (shouldSimplifyIfs) { - codeRewriter.simplifyIf(code); + branchSimplifier.simplifyIf(code); } assert code.isConsistentSSA(appView);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java b/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java index b7d7b10..fcefb79 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
@@ -20,6 +20,7 @@ import com.android.tools.r8.ir.code.Phi; import com.android.tools.r8.ir.code.Value; import com.android.tools.r8.ir.code.ValueIsDeadAnalysis; +import com.android.tools.r8.ir.conversion.passes.BranchSimplifier; import com.android.tools.r8.shaking.AppInfoWithLiveness; import com.android.tools.r8.utils.Box; import com.android.tools.r8.utils.IterableUtils; @@ -50,6 +51,8 @@ codeRewriter.rewriteMoveResult(code); + BranchSimplifier branchSimplifier = new BranchSimplifier(appView); + // We may encounter unneeded catch handlers after each iteration, e.g., if a dead instruction // is the only throwing instruction in a block. Removing unneeded catch handlers can lead to // more dead instructions. @@ -62,7 +65,7 @@ removeDeadInstructions(worklist, code, block, valueIsDeadAnalysis); removeDeadPhis(worklist, block, valueIsDeadAnalysis); } - } while (codeRewriter.simplifyIf(code).anySimplifications() + } while (branchSimplifier.simplifyIf(code).anySimplifications() || removeUnneededCatchHandlers(code)); assert code.isConsistentSSA(appView);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MultiCallerInliner.java b/src/main/java/com/android/tools/r8/ir/optimize/MultiCallerInliner.java index 54c8f61..4b5f0c9 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/MultiCallerInliner.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/MultiCallerInliner.java
@@ -147,12 +147,11 @@ // We track up to n call sites, where n is the size of multiCallerInliningInstructionLimits. if (callers.size() > multiCallerInliningInstructionLimits.length) { stopTrackingCallSitesForMethodIfDefinitelyIneligibleForMultiCallerInlining( - method, singleTarget, methodProcessor, callers); + singleTarget, methodProcessor, callers); } } private void stopTrackingCallSitesForMethodIfDefinitelyIneligibleForMultiCallerInlining( - ProgramMethod method, ProgramMethod singleTarget, MethodProcessor methodProcessor, ProgramMethodMultiset callers) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RuntimeWorkaroundCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/RuntimeWorkaroundCodeRewriter.java index afed4c2..370a5d7 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/RuntimeWorkaroundCodeRewriter.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/RuntimeWorkaroundCodeRewriter.java
@@ -22,6 +22,7 @@ import com.android.tools.r8.ir.code.InvokeStatic; import com.android.tools.r8.ir.code.NumericType; import com.android.tools.r8.ir.code.Value; +import com.android.tools.r8.ir.conversion.passes.BranchSimplifier; import com.android.tools.r8.utils.InternalOptions; import com.android.tools.r8.utils.LazyBox; import com.google.common.collect.ImmutableList; @@ -68,6 +69,58 @@ } } + public static void workaroundSwitchMaxIntBug(IRCode code, AppView<?> appView) { + if (appView.options().canHaveSwitchMaxIntBug() && code.metadata().mayHaveSwitch()) { + // Always rewrite for workaround switch bug. + rewriteSwitchForMaxIntOnly(code, appView); + } + } + + private static void rewriteSwitchForMaxIntOnly(IRCode code, AppView<?> appView) { + boolean needToSplitCriticalEdges = false; + BranchSimplifier branchSimplifier = new BranchSimplifier(appView); + ListIterator<BasicBlock> blocksIterator = code.listIterator(); + while (blocksIterator.hasNext()) { + BasicBlock block = blocksIterator.next(); + InstructionListIterator iterator = block.listIterator(code); + while (iterator.hasNext()) { + Instruction instruction = iterator.next(); + assert !instruction.isStringSwitch(); + if (instruction.isIntSwitch()) { + IntSwitch intSwitch = instruction.asIntSwitch(); + if (intSwitch.getKey(intSwitch.numberOfKeys() - 1) == Integer.MAX_VALUE) { + if (intSwitch.numberOfKeys() == 1) { + branchSimplifier.rewriteSingleKeySwitchToIf(code, block, iterator, intSwitch); + } else { + IntList newSwitchSequences = new IntArrayList(intSwitch.numberOfKeys() - 1); + for (int i = 0; i < intSwitch.numberOfKeys() - 1; i++) { + newSwitchSequences.add(intSwitch.getKey(i)); + } + IntList outliers = new IntArrayList(1); + outliers.add(Integer.MAX_VALUE); + branchSimplifier.convertSwitchToSwitchAndIfs( + code, + blocksIterator, + block, + iterator, + intSwitch, + ImmutableList.of(newSwitchSequences), + outliers); + } + needToSplitCriticalEdges = true; + } + } + } + } + + // Rewriting of switches introduces new branching structure. It relies on critical edges + // being split on the way in but does not maintain this property. We therefore split + // critical edges at exit. + if (needToSplitCriticalEdges) { + code.splitCriticalEdges(); + } + } + /** * For each block, we look to see if the header matches: * @@ -192,58 +245,6 @@ } } - public static void workaroundSwitchMaxIntBug( - IRCode code, CodeRewriter codeRewriter, InternalOptions options) { - if (options.canHaveSwitchMaxIntBug() && code.metadata().mayHaveSwitch()) { - // Always rewrite for workaround switch bug. - rewriteSwitchForMaxIntOnly(code, codeRewriter); - } - } - - private static void rewriteSwitchForMaxIntOnly(IRCode code, CodeRewriter codeRewriter) { - boolean needToSplitCriticalEdges = false; - ListIterator<BasicBlock> blocksIterator = code.listIterator(); - while (blocksIterator.hasNext()) { - BasicBlock block = blocksIterator.next(); - InstructionListIterator iterator = block.listIterator(code); - while (iterator.hasNext()) { - Instruction instruction = iterator.next(); - assert !instruction.isStringSwitch(); - if (instruction.isIntSwitch()) { - IntSwitch intSwitch = instruction.asIntSwitch(); - if (intSwitch.getKey(intSwitch.numberOfKeys() - 1) == Integer.MAX_VALUE) { - if (intSwitch.numberOfKeys() == 1) { - codeRewriter.rewriteSingleKeySwitchToIf(code, block, iterator, intSwitch); - } else { - IntList newSwitchSequences = new IntArrayList(intSwitch.numberOfKeys() - 1); - for (int i = 0; i < intSwitch.numberOfKeys() - 1; i++) { - newSwitchSequences.add(intSwitch.getKey(i)); - } - IntList outliers = new IntArrayList(1); - outliers.add(Integer.MAX_VALUE); - codeRewriter.convertSwitchToSwitchAndIfs( - code, - blocksIterator, - block, - iterator, - intSwitch, - ImmutableList.of(newSwitchSequences), - outliers); - } - needToSplitCriticalEdges = true; - } - } - } - } - - // Rewriting of switches introduces new branching structure. It relies on critical edges - // being split on the way in but does not maintain this property. We therefore split - // critical edges at exit. - if (needToSplitCriticalEdges) { - code.splitCriticalEdges(); - } - } - // See comment for InternalOptions.canHaveNumberConversionRegisterAllocationBug(). public static void workaroundNumberConversionRegisterAllocationBug( IRCode code, InternalOptions options) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java index 7607596..889a434 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
@@ -19,8 +19,9 @@ import com.android.tools.r8.ir.code.InstructionOrPhi; import com.android.tools.r8.ir.code.Value; import com.android.tools.r8.ir.conversion.MethodProcessor; +import com.android.tools.r8.ir.conversion.passes.BranchSimplifier; +import com.android.tools.r8.ir.conversion.passes.TrivialCheckCastAndInstanceOfRemover; import com.android.tools.r8.ir.optimize.AssumeRemover; -import com.android.tools.r8.ir.optimize.CodeRewriter; import com.android.tools.r8.ir.optimize.Inliner; import com.android.tools.r8.ir.optimize.InliningOracle; import com.android.tools.r8.ir.optimize.classinliner.InlineCandidateProcessor.IllegalClassInlinerStateException; @@ -126,7 +127,6 @@ // public final void processMethodCode( AppView<AppInfoWithLiveness> appView, - CodeRewriter codeRewriter, StringOptimizer stringOptimizer, EnumValueOptimizer enumValueOptimizer, ProgramMethod method, @@ -247,10 +247,10 @@ // If a method was inlined we may be able to remove check-cast instructions because we may // have more information about the types of the arguments at the call site. This is // particularly important for bridge methods. - codeRewriter.removeTrivialCheckCastAndInstanceOfInstructions( - code, method, methodProcessor, methodProcessingContext); + new TrivialCheckCastAndInstanceOfRemover(appView) + .run(code, method, methodProcessor, methodProcessingContext); // If a method was inlined we may be able to prune additional branches. - codeRewriter.simplifyControlFlow(code); + new BranchSimplifier(appView).simplifyBranches(code); // If a method was inlined we may see more trivial computation/conversion of String. boolean isDebugMode = appView.options().debug || method.getOrComputeReachabilitySensitive(appView);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/DefaultInliningReasonStrategy.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/DefaultInliningReasonStrategy.java index 537150e..02d13dd 100644 --- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/DefaultInliningReasonStrategy.java +++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/DefaultInliningReasonStrategy.java
@@ -53,7 +53,7 @@ // program. return Reason.SIMPLE; } - if (isSingleCallerInliningTarget(target)) { + if (isSingleCallerInliningTarget(target, context)) { return Reason.SINGLE_CALLER; } if (isMultiCallerInlineCandidate(invoke, target, oracle, methodProcessor)) { @@ -64,8 +64,8 @@ return Reason.SIMPLE; } - private boolean isSingleCallerInliningTarget(ProgramMethod method) { - if (!callSiteInformation.hasSingleCallSite(method)) { + private boolean isSingleCallerInliningTarget(ProgramMethod method, ProgramMethod context) { + if (!callSiteInformation.hasSingleCallSite(method, context)) { return false; } if (appView.appInfo().isNeverInlineDueToSingleCallerMethod(method)) {
diff --git a/src/main/java/com/android/tools/r8/naming/ComposingBuilder.java b/src/main/java/com/android/tools/r8/naming/ComposingBuilder.java index d87e036..502f120 100644 --- a/src/main/java/com/android/tools/r8/naming/ComposingBuilder.java +++ b/src/main/java/com/android/tools/r8/naming/ComposingBuilder.java
@@ -1306,7 +1306,7 @@ } newMappingInformation.forEach( info -> { - if (!nonCompasableNewInfos.contains(info)) { + if (!nonCompasableNewInfos.contains(info) && !info.isFileNameInformation()) { consumer.accept(info); } });
diff --git a/src/main/java/com/android/tools/r8/retrace/internal/ProguardMapPartitionerOnClassNameToText.java b/src/main/java/com/android/tools/r8/retrace/internal/ProguardMapPartitionerOnClassNameToText.java index 3af7dff..4fdc94a 100644 --- a/src/main/java/com/android/tools/r8/retrace/internal/ProguardMapPartitionerOnClassNameToText.java +++ b/src/main/java/com/android/tools/r8/retrace/internal/ProguardMapPartitionerOnClassNameToText.java
@@ -273,7 +273,7 @@ extends ProguardMapPartitionerBuilderImpl { private MappingPartitionKeyStrategy mappingPartitionKeyStrategy = - MappingPartitionKeyStrategy.OBFUSCATED_TYPE_NAME_AS_KEY_WITH_PARTITIONS; + MappingPartitionKeyStrategy.getDefaultStrategy(); public ProguardMapPartitionerBuilderImplInternal(DiagnosticsHandler diagnosticsHandler) { super(diagnosticsHandler);
diff --git a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java index 2c6f535..19b4d38 100644 --- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java +++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -456,8 +456,8 @@ * @return java package name i.e. "java.lang" */ public static String getPackageNameFromTypeName(String typeName) { - return getPackageNameFromBinaryName( - getClassBinaryNameFromDescriptor(javaTypeToDescriptor(typeName))); + int packageEndIndex = typeName.lastIndexOf(JAVA_PACKAGE_SEPARATOR); + return (packageEndIndex < 0) ? "" : typeName.substring(0, packageEndIndex); } /**
diff --git a/src/main/java/com/android/tools/r8/utils/Timing.java b/src/main/java/com/android/tools/r8/utils/Timing.java index 1e61b88..143bc2f 100644 --- a/src/main/java/com/android/tools/r8/utils/Timing.java +++ b/src/main/java/com/android/tools/r8/utils/Timing.java
@@ -19,6 +19,7 @@ import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; +import java.util.concurrent.ExecutorService; public class Timing { @@ -373,6 +374,10 @@ } } + public final TimingMerger beginMerger(String title, ExecutorService executorService) { + return beginMerger(title, ThreadUtils.getNumberOfThreads(executorService)); + } + public TimingMerger beginMerger(String title, int numberOfThreads) { return new TimingMerger(title, numberOfThreads, this); }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11StreamAbstractTests.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11StreamAbstractTests.java index 5822d2f..9ff2346 100644 --- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11StreamAbstractTests.java +++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/jdktests/Jdk11StreamAbstractTests.java
@@ -115,12 +115,17 @@ public static final String[] SUCCESSFUL_RUNNABLE_TESTS_ON_JDK11_AND_V7 = new String[] { // Require the virtual method isDefault() in class java/lang/reflect/Method. - "org/openjdk/tests/java/util/stream/WhileOpTest.java", "org/openjdk/tests/java/util/stream/WhileOpStatefulTest.java", // Require a Random method not present before Android 7 and not desugared. "org/openjdk/tests/java/util/stream/IntPrimitiveOpsTests.java" }; + public static final String[] LONG_RUNNING_SUCCESSFUL_RUNNABLE_TESTS_ON_JDK11_AND_V7 = + new String[] { + // Require the virtual method isDefault() in class java/lang/reflect/Method. + "org/openjdk/tests/java/util/stream/WhileOpTest.java", + }; + // Disabled because time to run > 1 min for each test. // Can be used for experimentation/testing purposes. private static String[] LONG_RUNNING_TESTS =
diff --git a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/InvokeSuperInDefaultInterfaceMethodToNonImmediateInterfaceTest.java b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/InvokeSuperInDefaultInterfaceMethodToNonImmediateInterfaceTest.java index f4fec42..d743010 100644 --- a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/InvokeSuperInDefaultInterfaceMethodToNonImmediateInterfaceTest.java +++ b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/InvokeSuperInDefaultInterfaceMethodToNonImmediateInterfaceTest.java
@@ -7,117 +7,132 @@ import static org.junit.Assert.assertEquals; import com.android.tools.r8.TestBase; -import com.android.tools.r8.ToolHelper; -import com.android.tools.r8.ToolHelper.DexVm.Version; -import com.android.tools.r8.origin.Origin; -import com.android.tools.r8.smali.SmaliBuilder; +import com.android.tools.r8.TestParameters; import com.android.tools.r8.utils.AndroidApiLevel; -import com.android.tools.r8.utils.AndroidApp; import com.android.tools.r8.utils.BooleanUtils; import com.android.tools.r8.utils.StringUtils; import com.google.common.collect.ImmutableList; -import org.junit.Ignore; +import java.io.IOException; +import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import org.objectweb.asm.Opcodes; @RunWith(Parameterized.class) public class InvokeSuperInDefaultInterfaceMethodToNonImmediateInterfaceTest extends TestBase { + private final TestParameters parameters; private final boolean includeInterfaceMethodOnJ; - @Parameterized.Parameters(name = "Include interface method on J: {0}") - public static Boolean[] data() { - return BooleanUtils.values(); + // Note that the expected output is independent of the presence of J.m(). + private static final String EXPECTED = StringUtils.lines("I.m()", "JImpl.m()"); + + @Parameterized.Parameters(name = "{0}, J.m(): {1}") + public static List<Object[]> data() { + return buildParameters( + TestParameters.builder() + .withCfRuntimes() + .enableApiLevelsForCf() + .withDexRuntimes() + .withApiLevel(AndroidApiLevel.B) + .build(), + BooleanUtils.values()); } public InvokeSuperInDefaultInterfaceMethodToNonImmediateInterfaceTest( - boolean includeInterfaceMethodOnJ) { + TestParameters parameters, boolean includeInterfaceMethodOnJ) { + this.parameters = parameters; this.includeInterfaceMethodOnJ = includeInterfaceMethodOnJ; } @Test - @Ignore("b/197494749 and b/120130831") - // TODO(b/197494749): Update this test now that desugaring is moved up. In particular this must - // be rewritten with CF based transformers as R8 does not support interface desugaring on DEX. - // TODO(b/120130831): With the move of desugaring this issue should be resolved. The bug indicates - // that a workaround for issues related to the IR implementation can now be reverted. - public void test() throws Exception { - // Note that the expected output is independent of the presence of J.m(). - String expectedOutput = StringUtils.lines("I.m()", "JImpl.m()"); - - byte[] dex = buildProgramDexFileData(); - if (ToolHelper.getDexVm().getVersion().isNewerThan(Version.V6_0_1)) { - AndroidApp app = - AndroidApp.builder() - .addDexProgramData(buildProgramDexFileData(), Origin.unknown()) - .build(); - assertEquals(expectedOutput, runOnArt(app, "TestClass")); - } - - testForR8(Backend.DEX) - .addProgramDexFileData(dex) - .addKeepMainRule("TestClass") - .setMinApi(AndroidApiLevel.M) - .run("TestClass") - .assertSuccessWithOutput(expectedOutput); + public void testJvm() throws Exception { + // The rewritten input is invalid on JVM. + parameters.assumeJvmTestParameters(); + testForJvm(parameters) + .addProgramClasses(getClasses()) + .addProgramClassFileData(getTransformedClasses()) + .run(parameters.getRuntime(), TestClass.class) + .assertFailureWithErrorThatThrows(VerifyError.class); } - // Using Smali instead of Jasmin because interfaces are broken in Jasmin. - private byte[] buildProgramDexFileData() throws Exception { - SmaliBuilder smaliBuilder = new SmaliBuilder(); - smaliBuilder.setMinApi(AndroidApiLevel.N); + @Test + public void testD8() throws Exception { + // Notice that desugaring will map out of the invalid invoke. + testForD8(parameters.getBackend()) + .addProgramClasses(getClasses()) + .addProgramClassFileData(getTransformedClasses()) + .setMinApi(parameters) + .run(parameters.getRuntime(), TestClass.class) + .assertSuccessWithOutput(EXPECTED); + } - smaliBuilder.addClass("TestClass"); + @Test + public void testR8() throws Exception { + // Notice that desugaring will map out of the invalid invoke. + testForR8(parameters.getBackend()) + .addProgramClasses(getClasses()) + .addProgramClassFileData(getTransformedClasses()) + .addKeepMainRule(TestClass.class) + .setMinApi(parameters) + .run(parameters.getRuntime(), TestClass.class) + .assertSuccessWithOutput(EXPECTED); + } - // public void main(String[] args) { new JImpl().m(); } - smaliBuilder.addMainMethod( - 1, - "new-instance v0, LJImpl;", - "invoke-direct {v0}, LJImpl;-><init>()V", - "invoke-virtual {v0}, LJImpl;->m()V", - "return-void"); + private List<Class<?>> getClasses() { + return ImmutableList.of(TestClass.class, I.class); + } - smaliBuilder.addInterface("I"); + private List<byte[]> getTransformedClasses() throws IOException { + return ImmutableList.of( + transformer(JImpl.class) + .transformMethodInsnInMethod( + "m", + (opcode, owner, name, descriptor, isInterface, visitor) -> { + if (opcode == Opcodes.INVOKESPECIAL) { + assertEquals(owner, binaryName(J.class)); + owner = binaryName(I.class); + } + visitor.visitMethodInsn(opcode, owner, name, descriptor, isInterface); + }) + .transform(), + transformer(J.class) + .removeMethods( + (access, name, descriptor, signature, exceptions) -> + !includeInterfaceMethodOnJ && name.equals("m")) + .transform()); + } - // default void m() { System.out.println("In I.m()"); } - smaliBuilder.addInstanceMethod( - "void", - "m", - 2, - "sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;", - "const-string v1, \"I.m()\"", - "invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V", - "return-void"); + static class TestClass { - smaliBuilder.addInterface("J", "java.lang.Object", ImmutableList.of("I")); - if (includeInterfaceMethodOnJ) { - smaliBuilder.addInstanceMethod( - "void", - "m", - 2, - "invoke-super {p0}, LI;->m()V", - "sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;", - "const-string v1, \"J.m()\"", - "invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V", - "return-void"); + public static void main(String[] args) { + new JImpl().m(); } + } - smaliBuilder.addClass("JImpl", "java.lang.Object", ImmutableList.of("J")); - smaliBuilder.addDefaultConstructor(); + interface I { - // default void m() { I.super.m(); System.out.println("In JImpl.m()"); } - smaliBuilder.addInstanceMethod( - "void", - "m", - 2, - // Note: invoke-super instruction to the non-immediate interface I. - "invoke-super {p0}, LI;->m()V", - "sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;", - "const-string v1, \"JImpl.m()\"", - "invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V", - "return-void"); + default void m() { + System.out.println("I.m()"); + } + } - return smaliBuilder.compile(); + interface J extends I { + + @Override + default void m() { + I.super.m(); + System.out.println("J.m()"); + } + } + + static class JImpl implements J { + + @Override + public void m() { + J.super.m(); // Will be rewritten to non-immediate interface: I.super.m(); + System.out.println("JImpl.m()"); + } } }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/TrivialGotoEliminationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/TrivialGotoEliminationTest.java index 019431d..8a8ab51 100644 --- a/src/test/java/com/android/tools/r8/ir/optimize/TrivialGotoEliminationTest.java +++ b/src/test/java/com/android/tools/r8/ir/optimize/TrivialGotoEliminationTest.java
@@ -29,6 +29,7 @@ import com.android.tools.r8.ir.code.Throw; import com.android.tools.r8.ir.code.Value; import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions; +import com.android.tools.r8.ir.conversion.passes.TrivialGotosCollapser; import com.android.tools.r8.origin.Origin; import com.android.tools.r8.utils.AndroidApp; import com.android.tools.r8.utils.InternalOptions; @@ -107,7 +108,7 @@ IRMetadata.unknown(), Origin.unknown(), new MutableMethodConversionOptions(options)); - CodeRewriter.collapseTrivialGotos(appView, code); + new TrivialGotosCollapser(appView).run(code.context(), code); assertTrue(code.entryBlock().isTrivialGoto()); assertTrue(blocks.contains(block0)); assertTrue(blocks.contains(block1)); @@ -196,7 +197,7 @@ IRMetadata.unknown(), Origin.unknown(), new MutableMethodConversionOptions(options)); - CodeRewriter.collapseTrivialGotos(appView, code); + new TrivialGotosCollapser(appView).run(code.context(), code); assertTrue(block0.getInstructions().get(1).isIf()); assertEquals(block1, block0.getInstructions().get(1).asIf().fallthroughBlock()); assertTrue(blocks.containsAll(ImmutableList.of(block0, block1, block2, block3)));
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java index f5b5a47..706f8f0 100644 --- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java +++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java
@@ -75,13 +75,15 @@ ClassSubject aClassSubject = inspector.clazz(A.class); assertThat(aClassSubject, isPresent()); - // B.foo() should be absent if the max inlining depth is 1, because indirection() has been - // inlined into main(), which makes B.foo() eligible for inlining into main(). + // B.foo() could potentially be absent if the max inlining depth is 1, because indirection() has + // been inlined into main(), which makes B.foo() eligible for inlining into main() since we can + // compute a single target. However, because indirection() can be inlined into multiple callers + // we cannot trust the single caller predicate. If indirection() also has a singler caller we + // could propagate the single call site property to B.foo(). ClassSubject bClassSubject = inspector.clazz(B.class); assertThat(bClassSubject, isPresent()); - assertThat( - bClassSubject.uniqueMethodWithOriginalName("foo"), - notIf(isPresent(), maxInliningDepth == 1)); + // TODO(b/286058449): We could inline this. + assertThat(bClassSubject.uniqueMethodWithOriginalName("foo"), isPresent()); // B.bar() should always be inlined because it is marked as @AlwaysInline. assertThat(
diff --git a/src/test/java/com/android/tools/r8/mappingcompose/ComposeSourceFileTest.java b/src/test/java/com/android/tools/r8/mappingcompose/ComposeSourceFileTest.java index b07c55c..434c494 100644 --- a/src/test/java/com/android/tools/r8/mappingcompose/ComposeSourceFileTest.java +++ b/src/test/java/com/android/tools/r8/mappingcompose/ComposeSourceFileTest.java
@@ -31,22 +31,32 @@ private static final String mappingFoo = StringUtils.unixLines( - "# {'id':'com.android.tools.r8.mapping','version':'experimental'}", - "com.foo -> a:", - " # {'id':'sourceFile','fileName':'Foo.kt'}"); + "# {'id':'com.android.tools.r8.mapping','version':'2.2'}", + "com.foo -> A:", + " # {'id':'sourceFile','fileName':'Foo.kt'}", + "com.bar -> B:", + " # {'id':'sourceFile','fileName':'Bar.kt'}", + "com.baz -> C:"); private static final String mappingBar = StringUtils.unixLines( - "# {'id':'com.android.tools.r8.mapping','version':'experimental'}", - "com.bar -> c:", - " # {'id':'sourceFile','fileName':'Bar.kt'}", - "a -> b:"); + "# {'id':'com.android.tools.r8.mapping','version':'2.2'}", + "A -> a:", + " # {'id':'sourceFile','fileName':'some-hash-inserted-into-source-file'}", + "B -> b:", + "C -> c:", + " # {'id':'sourceFile','fileName':'some-other-hash-inserted-into-source-file'}", + "com.qux -> d:", + " # {'id':'sourceFile','fileName':'Qux.kt'}"); private static final String mappingResult = StringUtils.unixLines( - "# {'id':'com.android.tools.r8.mapping','version':'experimental'}", - "com.bar -> c:", + "# {'id':'com.android.tools.r8.mapping','version':'2.2'}", + "com.bar -> b:", "# {'id':'sourceFile','fileName':'Bar.kt'}", - "com.foo -> b:", - "# {'id':'sourceFile','fileName':'Foo.kt'}"); + "com.baz -> c:", + "com.foo -> a:", + "# {'id':'sourceFile','fileName':'Foo.kt'}", + "com.qux -> d:", + "# {'id':'sourceFile','fileName':'Qux.kt'}"); @Test public void testCompose() throws Exception {
diff --git a/src/test/java/com/android/tools/r8/retrace/partition/RetracePartitionWithPrimitiveNameTest.java b/src/test/java/com/android/tools/r8/retrace/partition/RetracePartitionWithPrimitiveNameTest.java new file mode 100644 index 0000000..e848753 --- /dev/null +++ b/src/test/java/com/android/tools/r8/retrace/partition/RetracePartitionWithPrimitiveNameTest.java
@@ -0,0 +1,77 @@ +// Copyright (c) 2023, 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.retrace.partition; + +import static org.junit.Assert.assertEquals; + +import com.android.tools.r8.TestBase; +import com.android.tools.r8.TestDiagnosticMessagesImpl; +import com.android.tools.r8.TestParameters; +import com.android.tools.r8.TestParametersCollection; +import com.android.tools.r8.retrace.MappingPartitionMetadata; +import com.android.tools.r8.retrace.PartitionMappingSupplier; +import com.android.tools.r8.retrace.ProguardMapProducer; +import com.android.tools.r8.retrace.RetraceOptions; +import com.android.tools.r8.retrace.RetraceStackFrameAmbiguousResultWithContext; +import com.android.tools.r8.retrace.RetraceStackTraceContext; +import com.android.tools.r8.retrace.StringRetrace; +import com.android.tools.r8.retrace.internal.MappingPartitionKeyStrategy; +import com.android.tools.r8.retrace.internal.ProguardMapPartitionerOnClassNameToText.ProguardMapPartitionerBuilderImplInternal; +import com.android.tools.r8.utils.StringUtils; +import java.util.HashMap; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +/** Regression test for b/286001537. */ +@RunWith(Parameterized.class) +public class RetracePartitionWithPrimitiveNameTest extends TestBase { + + @Parameters(name = "{0}") + public static TestParametersCollection data() { + return getTestParameters().withNoneRuntime().build(); + } + + public RetracePartitionWithPrimitiveNameTest(TestParameters parameters) { + parameters.assertNoneRuntime(); + } + + public final String mapping = + StringUtils.unixLines( + "# { id: 'com.android.tools.r8.mapping', version: '2.0' }", + "some.class1 -> int:", + " void method() -> a"); + + @Test + public void testPartitionAndRetrace() throws Exception { + ProguardMapProducer proguardMapProducer = ProguardMapProducer.fromString(mapping); + TestDiagnosticMessagesImpl diagnosticsHandler = new TestDiagnosticMessagesImpl(); + Map<String, byte[]> partitions = new HashMap<>(); + MappingPartitionMetadata metadata = + new ProguardMapPartitionerBuilderImplInternal(diagnosticsHandler) + .setMappingPartitionKeyStrategy(MappingPartitionKeyStrategy.getDefaultStrategy()) + .setProguardMapProducer(proguardMapProducer) + .setPartitionConsumer( + partition -> partitions.put(partition.getKey(), partition.getPayload())) + .build() + .run(); + + PartitionMappingSupplier mappingSupplier = + PartitionMappingSupplier.builder() + .setMetadata(metadata.getBytes()) + .setMappingPartitionFromKeySupplier(partitions::get) + .build(); + + StringRetrace retracer = + StringRetrace.create(RetraceOptions.builder().setMappingSupplier(mappingSupplier).build()); + RetraceStackFrameAmbiguousResultWithContext<String> result = + retracer.retraceFrame(" at int.a()", RetraceStackTraceContext.empty()); + StringBuilder sb = new StringBuilder(); + result.forEach(frames -> frames.forEach(sb::append)); + assertEquals(" at some.class1.method(class1.java)", sb.toString()); + } +}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayWithDataLengthRewriteTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayWithDataLengthRewriteTest.java index bcacbe7..a09de37 100644 --- a/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayWithDataLengthRewriteTest.java +++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ArrayWithDataLengthRewriteTest.java
@@ -13,7 +13,7 @@ import com.android.tools.r8.graph.AppView; import com.android.tools.r8.ir.code.IRCode; import com.android.tools.r8.ir.code.Instruction; -import com.android.tools.r8.ir.optimize.CodeRewriter; +import com.android.tools.r8.ir.conversion.passes.ArrayConstructionSimplifier; import com.android.tools.r8.utils.codeinspector.ClassSubject; import com.android.tools.r8.utils.codeinspector.CodeInspector; import com.android.tools.r8.utils.codeinspector.InstructionSubject; @@ -64,7 +64,7 @@ } private void transformArray(IRCode irCode, AppView<?> appView) { - new CodeRewriter(appView).simplifyArrayConstruction(irCode); + new ArrayConstructionSimplifier(appView).run(irCode.context(), irCode); String name = irCode.context().getReference().getName().toString(); if (name.contains("filledArrayData")) { assertTrue(irCode.streamInstructions().anyMatch(Instruction::isNewArrayFilledData));
diff --git a/src/test/java/com/android/tools/r8/startup/SingleCallerBridgeStartupTest.java b/src/test/java/com/android/tools/r8/startup/SingleCallerBridgeStartupTest.java index 4a27e0f..1cb68de 100644 --- a/src/test/java/com/android/tools/r8/startup/SingleCallerBridgeStartupTest.java +++ b/src/test/java/com/android/tools/r8/startup/SingleCallerBridgeStartupTest.java
@@ -4,6 +4,9 @@ package com.android.tools.r8.startup; +import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent; +import static org.hamcrest.MatcherAssert.assertThat; + import com.android.tools.r8.TestBase; import com.android.tools.r8.TestParameters; import com.android.tools.r8.TestParametersCollection; @@ -12,6 +15,7 @@ import com.android.tools.r8.startup.profile.ExternalStartupItem; import com.android.tools.r8.startup.profile.ExternalStartupMethod; import com.android.tools.r8.startup.utils.StartupTestingUtils; +import com.android.tools.r8.utils.codeinspector.ClassSubject; import com.google.common.collect.ImmutableList; import java.util.Collection; import org.junit.Test; @@ -55,9 +59,16 @@ (appView, inlinee, inliningDepth) -> inlinee.getMethodReference().equals(barMethod)) .setMinApi(parameters) + .compile() + .inspect( + inspector -> { + // Assert that foo is not inlined. + ClassSubject A = inspector.clazz(A.class); + assertThat(A, isPresent()); + assertThat(A.uniqueMethodWithOriginalName("foo"), isPresent()); + }) .run(parameters.getRuntime(), Main.class) - // TODO(b/285021603): We should not fail here. - .assertFailureWithErrorThatThrows(NoSuchMethodError.class); + .assertSuccessWithOutputLines("A::foo", "A::foo"); } static class Main {
diff --git a/tools/create_maven_release.py b/tools/create_maven_release.py index ee68e1e..1ca239e 100755 --- a/tools/create_maven_release.py +++ b/tools/create_maven_release.py
@@ -124,6 +124,12 @@ help='Build desugar library configuration (original JDK-8)') group.add_argument('--desugar-configuration-jdk11-legacy', action='store_true', help='Build desugar library configuration (JDK-11 legacy)') + group.add_argument('--desugar-configuration-jdk11-minimal', action='store_true', + help='Build desugar library configuration (JDK-11 minimal)') + group.add_argument('--desugar-configuration-jdk11', action='store_true', + help='Build desugar library configuration (JDK-11)') + group.add_argument('--desugar-configuration-jdk11-nio', action='store_true', + help='Build desugar library configuration (JDK-11 nio)') return result.parse_args(argv) def determine_version(): @@ -369,7 +375,7 @@ with zipfile.ZipFile(conversions, 'r') as conversions_zip: conversions_zip.extractall(tmp_dir) - # Add configuration + # Add configuration. configuration_dir = join(tmp_dir, 'META-INF', 'desugar', 'd8') makedirs(configuration_dir) copyfile(configuration, join(configuration_dir, 'desugar.json')) @@ -388,6 +394,9 @@ utils.PrintCmd(cmd) subprocess.check_call(cmd) + # Add LICENSE file. + copyfile(join(utils.REPO_ROOT, 'LICENSE'), join(tmp_dir, 'LICENSE')) + make_archive(destination, 'zip', tmp_dir) move(destination + '.zip', destination) @@ -435,11 +444,34 @@ 'Need to supply output zip with --out.') if options.desugar_configuration or options.desugar_configuration_jdk8: generate_desugar_configuration_maven_zip( - options.out, utils.DESUGAR_CONFIGURATION, utils.DESUGAR_IMPLEMENTATION) + options.out, + utils.DESUGAR_CONFIGURATION, + utils.DESUGAR_IMPLEMENTATION, + utils.LIBRARY_DESUGAR_CONVERSIONS_LEGACY_ZIP) elif options.desugar_configuration_jdk11_legacy: generate_desugar_configuration_maven_zip( - options.out, utils.DESUGAR_CONFIGURATION_JDK11_LEGACY, - utils.DESUGAR_IMPLEMENTATION_JDK11) + options.out, + utils.DESUGAR_CONFIGURATION_JDK11_LEGACY, + utils.DESUGAR_IMPLEMENTATION_JDK11, + utils.LIBRARY_DESUGAR_CONVERSIONS_LEGACY_ZIP) + elif options.desugar_configuration_jdk11_minimal: + generate_desugar_configuration_maven_zip( + options.out, + utils.DESUGAR_CONFIGURATION_JDK11_MINIMAL, + utils.DESUGAR_IMPLEMENTATION_JDK11, + utils.LIBRARY_DESUGAR_CONVERSIONS_ZIP) + elif options.desugar_configuration_jdk11: + generate_desugar_configuration_maven_zip( + options.out, + utils.DESUGAR_CONFIGURATION_JDK11, + utils.DESUGAR_IMPLEMENTATION_JDK11, + utils.LIBRARY_DESUGAR_CONVERSIONS_ZIP) + elif options.desugar_configuration_jdk11_nio: + generate_desugar_configuration_maven_zip( + options.out, + utils.DESUGAR_CONFIGURATION_JDK11_NIO, + utils.DESUGAR_IMPLEMENTATION_JDK11, + utils.LIBRARY_DESUGAR_CONVERSIONS_ZIP) else: generate_r8_maven_zip(options.out, options.r8lib)