blob: 2a93788edd4e34bcc8e96bf400265854c9b022d8 [file] [log] [blame]
// 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.DexType;
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.LinearFlowInstructionListIterator;
import com.android.tools.r8.ir.code.NewArrayEmpty;
import com.android.tools.r8.ir.code.NewArrayFilled;
import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
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> {
public ArrayConstructionSimplifier(AppView<?> appView) {
super(appView);
}
@Override
protected String getRewriterId() {
return "ArrayConstructionSimplifier";
}
@Override
protected CodeRewriterResult rewriteCode(IRCode code) {
boolean hasChanged = false;
WorkList<BasicBlock> worklist = WorkList.newIdentityWorkList(code.blocks);
while (worklist.hasNext()) {
BasicBlock block = worklist.next();
hasChanged |= simplifyArrayConstructionBlock(block, worklist, code, appView.options());
}
if (hasChanged) {
code.removeRedundantBlocks();
}
return CodeRewriterResult.hasChanged(hasChanged);
}
@Override
protected boolean shouldRewriteCode(IRCode code) {
return true;
}
private boolean simplifyArrayConstructionBlock(
BasicBlock block, WorkList<BasicBlock> worklist, IRCode code, InternalOptions options) {
boolean hasChanged = false;
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(
code, 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);
assert newArrayEmpty.getLocalInfo() == null;
Instruction lastArrayPut = info.lastArrayPutIterator.peekPrevious();
Value invokeValue = code.createValue(newArrayEmpty.getOutType(), null);
NewArrayFilled invoke =
new NewArrayFilled(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);
}
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);
hasChanged = true;
}
if (hasChanged) {
code.removeRedundantBlocks();
}
return hasChanged;
}
private static class FilledArrayConversionInfo {
Value[] values;
List<ArrayPut> arrayPutsToRemove;
LinearFlowInstructionListIterator lastArrayPutIterator;
public FilledArrayConversionInfo(int size) {
values = new Value[size];
arrayPutsToRemove = new ArrayList<>(size);
}
}
@SuppressWarnings("ReferenceEquality")
private FilledArrayConversionInfo computeConversionInfo(
IRCode code, 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.toArrayElementType(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(appView, code.context())) {
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;
}
int index = arrayPut.indexIfConstAndInBounds(values.length);
if (index < 0 || values[index] != null) {
return null;
}
if (arrayPut.instructionInstanceCanThrow(appView, code.context())) {
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;
public FilledArrayCandidate(NewArrayEmpty newArrayEmpty, int size) {
assert size > 0;
this.newArrayEmpty = newArrayEmpty;
this.size = size;
}
}
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;
}
return new FilledArrayCandidate(newArrayEmpty, size);
}
}