Add disabled logic for using new-filled-array for more things.
Flag: InternalOptions.experimentalNewFilledArraySupport
This change adds a new test the enables the option and tests various
types of array construction.
The pass cannot be enabled until other optimizations that inspect arrays
of constants are updated to also consider InvokerNewArray instructions.
Does not yet handle phis (e.g. new int[] { foo ? 0 : 1 }).
Bug: 246971330
Change-Id: I4437e4594e59b075527bbcdd37c37d32efb96277
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 6099074..cade4ce 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
@@ -16,7 +16,6 @@
import com.android.tools.r8.algorithms.scc.SCC;
import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
-import com.android.tools.r8.dex.Constants;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.AccessControl;
@@ -55,7 +54,6 @@
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.ConstInstruction;
import com.android.tools.r8.ir.code.ConstNumber;
import com.android.tools.r8.ir.code.ConstString;
import com.android.tools.r8.ir.code.DebugLocalWrite;
@@ -84,6 +82,7 @@
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;
@@ -138,6 +137,7 @@
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;
@@ -162,7 +162,6 @@
FALSE
}
- private static final int MAX_FILL_ARRAY_SIZE = 8 * Constants.KILOBYTE;
// This constant was determined by experimentation.
private static final int STOP_SHARED_CONSTANT_THRESHOLD = 50;
@@ -2100,16 +2099,21 @@
}
}
- private short[] computeArrayFilledData(ConstInstruction[] values, int size, int elementSize) {
- if (values == null) {
- return null;
+ 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].asConstNumber().getIntValue() & 0xFF);
+ short value =
+ (short) (values[i].getConstInstruction().asConstNumber().getIntValue() & 0xFF);
if (i + 1 < size) {
- value |= (short) ((values[i + 1].asConstNumber().getIntValue() & 0xFF) << 8);
+ value |=
+ (short)
+ ((values[i + 1].getConstInstruction().asConstNumber().getIntValue() & 0xFF) << 8);
}
result[i / 2] = value;
}
@@ -2119,7 +2123,7 @@
int shortsPerConstant = elementSize / 2;
short[] result = new short[size * shortsPerConstant];
for (int i = 0; i < size; i++) {
- long value = values[i].asConstNumber().getRawValue();
+ long value = values[i].getConstInstruction().asConstNumber().getRawValue();
for (int part = 0; part < shortsPerConstant; part++) {
result[i * shortsPerConstant + part] = (short) ((value >> (16 * part)) & 0xFFFFL);
}
@@ -2127,40 +2131,44 @@
return result;
}
- private ConstInstruction[] computeConstantArrayValues(
- NewArrayEmpty newArray, BasicBlock block, int size) {
- if (size > MAX_FILL_ARRAY_SIZE) {
- return null;
- }
- ConstInstruction[] values = new ConstInstruction[size];
+ private Value[] computeArrayValues(LinearFlowInstructionListIterator it, int size) {
+ NewArrayEmpty newArrayEmpty = it.next().asNewArrayEmpty();
+ assert newArrayEmpty != null;
+
+ // 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;
+
+ Value[] values = new Value[size];
int remaining = size;
- Set<Instruction> users = newArray.outValue().uniqueUsers();
- Set<BasicBlock> visitedBlocks = Sets.newIdentityHashSet();
- // We allow the array instantiations to cross block boundaries as long as it hasn't encountered
- // an instruction instance that can throw an exception.
- InstructionIterator it = block.iterator();
- it.nextUntil(i -> i == newArray);
- do {
- visitedBlocks.add(block);
+ Set<Instruction> users = newArrayEmpty.outValue().uniqueUsers();
while (it.hasNext()) {
Instruction instruction = it.next();
- // 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()) {
+ // 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.
+ // TODO(b/246971330): Allow simplification when all users of the array are in the same
+ // try/catch.
+ if (instruction.getBlock().hasCatchHandlers() && instruction.instructionInstanceCanThrow()) {
return null;
}
if (!users.contains(instruction)) {
continue;
}
- // If the initialization sequence is broken by another use we cannot use a
- // fill-array-data instruction.
- if (!instruction.isArrayPut()) {
+ ArrayPut arrayPut = instruction.asArrayPut();
+ // If the initialization sequence is broken by another use we cannot use a fill-array-data
+ // instruction.
+ if (arrayPut == null) {
return null;
}
- ArrayPut arrayPut = instruction.asArrayPut();
- if (!(arrayPut.value().isConstant() && arrayPut.index().isConstNumber())) {
+ if (!arrayPut.index().isConstNumber()) {
return null;
}
int index = arrayPut.index().getConstInstruction().asConstNumber().getIntValue();
@@ -2170,40 +2178,30 @@
if (values[index] != null) {
return null;
}
- ConstInstruction value = arrayPut.value().getConstInstruction();
+ Value value = arrayPut.value();
+ if (needsTypeCheck && !value.isAlwaysNull(appView)) {
+ DexType valueDexType = value.getType().asReferenceType().toDexType(dexItemFactory);
+ if (elementType.isArrayType()) {
+ if (!elementType.equals(valueDexType)) {
+ return null;
+ }
+ } 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;
+ }
+ }
+ }
values[index] = value;
--remaining;
if (remaining == 0) {
return values;
}
}
- BasicBlock nextBlock = block.exit().isGoto() ? block.exit().asGoto().getTarget() : null;
- block = nextBlock != null && !visitedBlocks.contains(nextBlock) ? nextBlock : null;
- it = block != null ? block.iterator() : null;
- } while (it != null);
return null;
}
- private boolean allowNewFilledArrayConstruction(Instruction instruction) {
- if (!(instruction instanceof NewArrayEmpty)) {
- return false;
- }
- NewArrayEmpty newArray = instruction.asNewArrayEmpty();
- if (!newArray.size().isConstant()) {
- return false;
- }
- assert newArray.size().isConstNumber();
- int size = newArray.size().getConstInstruction().asConstNumber().getIntValue();
- if (size < 1) {
- return false;
- }
- if (newArray.type.isPrimitiveArrayType()) {
- return true;
- }
- return newArray.type == dexItemFactory.stringArrayType
- && options.canUseFilledNewArrayOfObjects();
- }
-
/**
* Replace new-array followed by stores of constants to all entries with new-array
* and fill-array-data / filled-new-array.
@@ -2212,6 +2210,10 @@
if (options.isGeneratingClassFiles()) {
return;
}
+ InternalOptions.RewriteArrayOptions rewriteOptions = options.rewriteArrayOptions();
+ boolean canUseForObjects = rewriteOptions.canUseFilledNewArrayOfObjects();
+ boolean canUseForArrays = rewriteOptions.canUseFilledNewArrayOfArrays();
+
for (BasicBlock block : code.blocks) {
// Map from the array value to the number of array put instruction to remove for that value.
Map<Value, Instruction> instructionToInsertForArray = new HashMap<>();
@@ -2220,32 +2222,58 @@
InstructionListIterator it = block.listIterator(code);
while (it.hasNext()) {
Instruction instruction = it.next();
- if (instruction.getLocalInfo() != null
- || !allowNewFilledArrayConstruction(instruction)) {
+ NewArrayEmpty newArrayEmpty = instruction.asNewArrayEmpty();
+ if (newArrayEmpty == null || !newArrayEmpty.size().isConstant()) {
continue;
}
- NewArrayEmpty newArray = instruction.asNewArrayEmpty();
- int size = newArray.size().getConstInstruction().asConstNumber().getIntValue();
- ConstInstruction[] values = computeConstantArrayValues(newArray, block, size);
+ if (instruction.getLocalInfo() != null) {
+ continue;
+ }
+ int size = newArrayEmpty.size().getConstInstruction().asConstNumber().getIntValue();
+ if (size < 1 || size > rewriteOptions.maxFillArrayDataInputs) {
+ continue;
+ }
+ if (!newArrayEmpty.type.isPrimitiveArrayType() && !canUseForObjects) {
+ continue;
+ }
+ if (newArrayEmpty.type.getNumberOfLeadingSquareBrackets() > 1 && !canUseForArrays) {
+ continue;
+ }
+
+ Value[] values =
+ computeArrayValues(
+ new LinearFlowInstructionListIterator(code, block, it.previousIndex()), size);
if (values == null) {
continue;
}
- if (newArray.type == dexItemFactory.stringArrayType) {
+ if (!rewriteOptions.experimentalNewFilledArraySupport
+ && !newArrayEmpty.type.isPrimitiveArrayType()
+ && newArrayEmpty.type != dexItemFactory.stringArrayType) {
+ continue;
+ }
+ // 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 (!newArrayEmpty.type.isPrimitiveArrayType()
+ || (rewriteOptions.experimentalNewFilledArraySupport
+ && newArrayEmpty.type == dexItemFactory.intArrayType
+ && size <= 5)) {
// Don't replace with filled-new-array if it requires more than 200 consecutive registers.
- if (size > 200) {
+ if (size > rewriteOptions.maxRangeInputs) {
continue;
}
- List<Value> stringValues = new ArrayList<>(size);
- for (ConstInstruction value : values) {
- stringValues.add(value.outValue());
- }
- Value invokeValue = code.createValue(newArray.getOutType(), newArray.getLocalInfo());
+ // block.hasCatchHandlers() is fine here since new-array-filled replaces new-array-empty
+ // and computeArrayValues already checks that no throwing instructions exist between the
+ // original new-array-empty and the final aput-object (where the new-array-filled will be
+ // positioned).
+ Value invokeValue =
+ code.createValue(newArrayEmpty.getOutType(), newArrayEmpty.getLocalInfo());
InvokeNewArray invoke =
- new InvokeNewArray(dexItemFactory.stringArrayType, invokeValue, stringValues);
- for (Value value : newArray.inValues()) {
- value.removeUser(newArray);
+ new InvokeNewArray(newArrayEmpty.type, invokeValue, Arrays.asList(values));
+ for (Value value : newArrayEmpty.inValues()) {
+ value.removeUser(newArrayEmpty);
}
- newArray.outValue().replaceUsers(invokeValue);
+ newArrayEmpty.outValue().replaceUsers(invokeValue);
it.removeOrReplaceByDebugLocalRead();
instructionToInsertForArray.put(invokeValue, invoke);
storesToRemoveForArray.put(invokeValue, size);
@@ -2255,20 +2283,26 @@
if (size == 1) {
continue;
}
- int elementSize = newArray.type.elementSizeForPrimitiveArrayType();
+ // TODO(b/246971330): Allow simplification when all users of the array are in the same
+ // try/catch.
+ if (block.hasCatchHandlers()) {
+ // NewArrayFilledData can throw, so creating one as done below would add a second
+ // throwing instruction to the same block (the first one being NewArrayEmpty).
+ continue;
+ }
+ int elementSize = newArrayEmpty.type.elementSizeForPrimitiveArrayType();
short[] contents = computeArrayFilledData(values, size, elementSize);
if (contents == null) {
continue;
}
- if (block.hasCatchHandlers()) {
- continue;
- }
- int arraySize = newArray.size().getConstInstruction().asConstNumber().getIntValue();
+ int arraySize = newArrayEmpty.size().getConstInstruction().asConstNumber().getIntValue();
+ // fill-array-data requires the new-array-empty instruction to remain, as it does not
+ // itself create an array.
NewArrayFilledData fillArray =
- new NewArrayFilledData(newArray.outValue(), elementSize, arraySize, contents);
- fillArray.setPosition(newArray.getPosition());
+ new NewArrayFilledData(newArrayEmpty.outValue(), elementSize, arraySize, contents);
+ fillArray.setPosition(newArrayEmpty.getPosition());
it.add(fillArray);
- storesToRemoveForArray.put(newArray.outValue(), size);
+ storesToRemoveForArray.put(newArrayEmpty.outValue(), size);
}
}
// Second pass: remove all the array put instructions for the array for which we have
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index e653a7a..acf5ac1 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -816,6 +816,7 @@
public boolean debug = false;
+ private final RewriteArrayOptions rewriteArrayOptions = new RewriteArrayOptions();
private final CallSiteOptimizationOptions callSiteOptimizationOptions =
new CallSiteOptimizationOptions();
private final CfCodeAnalysisOptions cfCodeAnalysisOptions = new CfCodeAnalysisOptions();
@@ -850,6 +851,10 @@
public LineNumberOptimization lineNumberOptimization = LineNumberOptimization.ON;
+ public RewriteArrayOptions rewriteArrayOptions() {
+ return rewriteArrayOptions;
+ }
+
public CallSiteOptimizationOptions callSiteOptimizationOptions() {
return callSiteOptimizationOptions;
}
@@ -1383,6 +1388,35 @@
System.getProperty("com.android.tools.r8.lambdaClassFieldsNotFinal") == null;
}
+ public class RewriteArrayOptions {
+ // Arbitrary limit of number of inputs to new-filled-array/range.
+ public int maxRangeInputs = 200;
+ // Arbitrary limit of number of inputs to fill-array-data.
+ public int maxFillArrayDataInputs = 8 * 1024;
+
+ // TODO(b/246971330): Remove when feature is complete.
+ public boolean experimentalNewFilledArraySupport = false;
+ // Dalvik x86-atom backend had a bug that made it crash on filled-new-array instructions for
+ // arrays of objects. This is unfortunate, since this never hits arm devices, but we have
+ // to disallow filled-new-array of objects for dalvik until kitkat. The buggy code was
+ // removed during the jelly-bean release cycle and is not there from kitkat.
+ //
+ // Buggy code that accidentally call code that only works on primitives arrays.
+ //
+ // https://android.googlesource.com/platform/dalvik/+/ics-mr0/vm/mterp/out/InterpAsm-x86-atom.S#25106
+ public boolean canUseFilledNewArrayOfObjects() {
+ assert isGeneratingDex();
+ return hasFeaturePresentFrom(AndroidApiLevel.K);
+ }
+
+ // Dalvik doesn't handle new-filled-array with arrays as values. It fails with:
+ // W(629880) VFY: [Ljava/lang/Integer; is not instance of Ljava/lang/Integer; (dalvikvm)
+ public boolean canUseFilledNewArrayOfArrays() {
+ assert isGeneratingDex();
+ return hasFeaturePresentFrom(AndroidApiLevel.L);
+ }
+ }
+
public class CallSiteOptimizationOptions {
private boolean enabled = true;
@@ -2387,19 +2421,6 @@
return hasFeaturePresentFrom(AndroidApiLevel.J);
}
- // Dalvik x86-atom backend had a bug that made it crash on filled-new-array instructions for
- // arrays of objects. This is unfortunate, since this never hits arm devices, but we have
- // to disallow filled-new-array of objects for dalvik until kitkat. The buggy code was
- // removed during the jelly-bean release cycle and is not there from kitkat.
- //
- // Buggy code that accidentally call code that only works on primitives arrays.
- //
- // https://android.googlesource.com/platform/dalvik/+/ics-mr0/vm/mterp/out/InterpAsm-x86-atom.S#25106
- public boolean canUseFilledNewArrayOfObjects() {
- assert isGeneratingDex();
- return hasFeaturePresentFrom(AndroidApiLevel.K);
- }
-
// Art had a bug (b/68761724) for Android N and O in the arm32 interpreter
// where an aget-wide instruction using the same register for the array
// and the first register of the result could lead to the wrong exception
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classmerger/vertical/ArrayPutToInterfaceWithObjectMergingTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classmerger/vertical/ArrayPutToInterfaceWithObjectMergingTest.java
index 1e43777..7141ad08 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classmerger/vertical/ArrayPutToInterfaceWithObjectMergingTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classmerger/vertical/ArrayPutToInterfaceWithObjectMergingTest.java
@@ -53,8 +53,7 @@
.addProgramClasses(I.class, A.class)
.addProgramClassFileData(getTransformedMain())
.addKeepMainRule(Main.class)
- // Keep get() to prevent that we optimize it into having static return type A.
- .addKeepRules("-keepclassmembers class " + Main.class.getTypeName() + " { *** get(...); }")
+ .addKeepMethodRules(Reference.methodFromMethod(Main.class.getDeclaredMethod("get")))
.addOptionsModification(
options ->
options
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/SimplifyArrayConstructionTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/SimplifyArrayConstructionTest.java
new file mode 100644
index 0000000..4fe27d8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/SimplifyArrayConstructionTest.java
@@ -0,0 +1,738 @@
+// Copyright (c) 2022, 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.rewrite.arrays;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeFalse;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.dex.code.DexFillArrayData;
+import com.android.tools.r8.dex.code.DexFilledNewArray;
+import com.android.tools.r8.dex.code.DexFilledNewArrayRange;
+import com.android.tools.r8.dex.code.DexNewArray;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.transformers.ClassFileTransformer;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.beust.jcommander.internal.Lists;
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Predicate;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class SimplifyArrayConstructionTest extends TestBase {
+ @Parameters(name = "{0}, mode = {1}")
+ public static Iterable<?> data() {
+ return buildParameters(
+ getTestParameters().withDefaultCfRuntime().withDexRuntimesAndAllApiLevels().build(),
+ CompilationMode.values());
+ }
+
+ private final TestParameters parameters;
+ private final CompilationMode compilationMode;
+
+ public SimplifyArrayConstructionTest(TestParameters parameters, CompilationMode compilationMode) {
+ this.parameters = parameters;
+ this.compilationMode = compilationMode;
+ }
+
+ private static final Class<?>[] DEX_ARRAY_INSTRUCTIONS = {
+ DexNewArray.class, DexFilledNewArray.class, DexFilledNewArrayRange.class, DexFillArrayData.class
+ };
+
+ private static final String[] EXPECTED_OUTPUT = {
+ "[a]",
+ "[a, 1, null]",
+ "[1, null]",
+ "[1, null, 2]",
+ "[1, null, 2]",
+ "[1]",
+ "[a, b]",
+ "[1, 2, 3, 4, 5]",
+ "[1]",
+ "[a, 1, null, d, e, f]",
+ "[a, b, c, d, e, f]",
+ "[1, null, 3, null, null, 6]",
+ "[1, 2, 3, 4, 5, 6]",
+ "[true, false]",
+ "[1, 2]",
+ "[1, 2]",
+ "[1, 2]",
+ "[1.0, 2.0]",
+ "[1.0, 2.0]",
+ "[]",
+ "[]",
+ "[true]",
+ "[1]",
+ "[1]",
+ "[1]",
+ "[1.0]",
+ "[1.0]",
+ "[0, 1]",
+ "[1, null]",
+ "[a]",
+ "[0, 1]",
+ "200",
+ "[0, 1, 2, 3, 4]",
+ "[4, 0, 0, 0, 0]",
+ "[4, 1, 2, 3, 4]",
+ "[0, 1, 2]",
+ "[0]",
+ "[0, 1, 2]",
+ "[1, 2, 3]",
+ "[1, 2, 3, 4, 5, 6]",
+ };
+
+ private static final byte[] TRANSFORMED_MAIN = transformedMain();
+
+ @Test
+ public void testRuntime() throws Exception {
+ assumeFalse(compilationMode == CompilationMode.DEBUG);
+ testForRuntime(
+ parameters.getRuntime(),
+ d8TestBuilder ->
+ d8TestBuilder
+ .setMinApi(parameters.getApiLevel())
+ .addOptionsModification(
+ options ->
+ options.rewriteArrayOptions().experimentalNewFilledArraySupport = true)
+ .setMode(compilationMode))
+ .addProgramClassFileData(TRANSFORMED_MAIN)
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines(EXPECTED_OUTPUT)
+ .inspect(this::inspect);
+ }
+
+ @Test
+ public void testR8() throws Exception {
+ testForR8(parameters.getBackend())
+ .setMinApi(parameters.getApiLevel())
+ .addOptionsModification(
+ options -> {
+ options.rewriteArrayOptions().experimentalNewFilledArraySupport = true;
+ options
+ .getOpenClosedInterfacesOptions()
+ .suppressSingleOpenInterface(Reference.classFromClass(Serializable.class));
+ })
+ .setMode(compilationMode)
+ .addProgramClassFileData(TRANSFORMED_MAIN)
+ .addKeepMainRule(Main.class)
+ .enableInliningAnnotations()
+ .addKeepAnnotation()
+ .addKeepRules("-keepclassmembers class * { @com.android.tools.r8.Keep *; }")
+ .addDontObfuscate()
+ .run(parameters.getRuntime(), Main.class)
+ .assertSuccessWithOutputLines(EXPECTED_OUTPUT)
+ .inspect(this::inspect);
+ }
+
+ private static byte[] transformedMain() {
+ try {
+ return transformer(Main.class)
+ .transformMethodInsnInMethod(
+ "interfaceArrayWithRawObject",
+ (opcode, owner, name, descriptor, isInterface, visitor) -> {
+ if (name.equals("getObjectThatImplementsSerializable")) {
+ visitor.visitMethodInsn(opcode, owner, name, "()Ljava/lang/Object;", isInterface);
+ } else {
+ visitor.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
+ }
+ })
+ .setReturnType(
+ ClassFileTransformer.MethodPredicate.onName("getObjectThatImplementsSerializable"),
+ Object.class.getTypeName())
+ .transform();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void inspect(CodeInspector inspector) {
+ if (parameters.isCfRuntime()) {
+ return;
+ }
+ ClassSubject mainClass = inspector.clazz(Main.class);
+ assertTrue(mainClass.isPresent());
+
+ MethodSubject referenceArraysNoCasts =
+ mainClass.uniqueMethodWithOriginalName("referenceArraysNoCasts");
+ MethodSubject referenceArraysWithSubclasses =
+ mainClass.uniqueMethodWithOriginalName("referenceArraysWithSubclasses");
+ MethodSubject interfaceArrayWithRawObject =
+ mainClass.uniqueMethodWithOriginalName("interfaceArrayWithRawObject");
+
+ MethodSubject phiFilledNewArray = mainClass.uniqueMethodWithOriginalName("phiFilledNewArray");
+ MethodSubject intsThatUseFilledNewArray =
+ mainClass.uniqueMethodWithOriginalName("intsThatUseFilledNewArray");
+ MethodSubject twoDimensionalArrays =
+ mainClass.uniqueMethodWithOriginalName("twoDimensionalArrays");
+ MethodSubject objectArraysFilledNewArrayRange =
+ mainClass.uniqueMethodWithOriginalName("objectArraysFilledNewArrayRange");
+ MethodSubject arraysThatUseFilledData =
+ mainClass.uniqueMethodWithOriginalName("arraysThatUseFilledData");
+ MethodSubject arraysThatUseNewArrayEmpty =
+ mainClass.uniqueMethodWithOriginalName("arraysThatUseNewArrayEmpty");
+ MethodSubject reversedArray = mainClass.uniqueMethodWithOriginalName("reversedArray");
+ MethodSubject arrayWithCorrectCountButIncompleteCoverage =
+ mainClass.uniqueMethodWithOriginalName("arrayWithCorrectCountButIncompleteCoverage");
+ MethodSubject arrayWithExtraInitialPuts =
+ mainClass.uniqueMethodWithOriginalName("arrayWithExtraInitialPuts");
+ MethodSubject catchHandlerThrowing =
+ mainClass.uniqueMethodWithOriginalName("catchHandlerThrowing");
+ MethodSubject catchHandlerNonThrowingFilledNewArray =
+ mainClass.uniqueMethodWithOriginalName("catchHandlerNonThrowingFilledNewArray");
+ MethodSubject catchHandlerNonThrowingFillArrayData =
+ mainClass.uniqueMethodWithOriginalName("catchHandlerNonThrowingFillArrayData");
+
+ assertArrayTypes(arraysThatUseNewArrayEmpty, DexNewArray.class);
+ assertArrayTypes(intsThatUseFilledNewArray, DexFilledNewArray.class);
+ assertFilledArrayData(arraysThatUseFilledData);
+
+ if (compilationMode == CompilationMode.DEBUG) {
+ // The explicit assignments can't be collapsed without breaking the debugger's ability to
+ // visit each line.
+ assertArrayTypes(reversedArray, DexNewArray.class);
+ } else {
+ assertArrayTypes(reversedArray, DexFilledNewArray.class);
+ }
+
+ if (parameters.getApiLevel().isLessThan(AndroidApiLevel.K)) {
+ assertArrayTypes(referenceArraysNoCasts, DexNewArray.class);
+ assertArrayTypes(referenceArraysWithSubclasses, DexNewArray.class);
+ assertArrayTypes(phiFilledNewArray, DexNewArray.class);
+ assertArrayTypes(objectArraysFilledNewArrayRange, DexNewArray.class);
+ assertArrayTypes(twoDimensionalArrays, DexNewArray.class);
+ } else {
+ assertArrayTypes(referenceArraysNoCasts, DexFilledNewArray.class);
+ // TODO(b/246971330): Add support for arrays with subtypes.
+ // assertArrayTypes(referenceArraysWithSubclasses, DexFilledNewArray.class);
+
+ // TODO(b/246971330): Add support for arrays whose values have conditionals.
+ // assertArrayTypes(phiFilledNewArray, DexFilledNewArray.class);
+
+ assertArrayTypes(objectArraysFilledNewArrayRange, DexFilledNewArrayRange.class);
+
+ if (parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.L)) {
+ assertArrayTypes(twoDimensionalArrays, DexFilledNewArray.class);
+ } else {
+ // No need to assert this case. If it's wrong, Dalvik verify errors cause test failures.
+ }
+ }
+ // filled-new-array fails verification when types of parameters are not subclasses (aput-object
+ // does not).
+ assertArrayTypes(interfaceArrayWithRawObject, DexNewArray.class);
+
+ // These could be optimized to use InvokeNewArray, but they seem like rare code patterns so we
+ // haven't bothered.
+ assertArrayTypes(arrayWithExtraInitialPuts, DexNewArray.class);
+ assertArrayTypes(arrayWithCorrectCountButIncompleteCoverage, DexNewArray.class);
+
+ assertArrayTypes(catchHandlerThrowing, DexNewArray.class);
+ assertArrayTypes(catchHandlerNonThrowingFillArrayData, DexNewArray.class);
+ assertArrayTypes(catchHandlerNonThrowingFilledNewArray, DexFilledNewArray.class);
+ }
+
+ private static Predicate<InstructionSubject> isInstruction(List<Class<?>> allowlist) {
+ return (ins) -> allowlist.contains(ins.asDexInstruction().getInstruction().getClass());
+ }
+
+ private static Predicate<InstructionSubject> isInstruction(Class<?> clazz) {
+ return isInstruction(Arrays.asList(clazz));
+ }
+
+ private static void assertArrayTypes(MethodSubject method, Class<?> allowedArrayInst) {
+ assertTrue(method.isPresent());
+ List<Class<?>> disallowedClasses = Lists.newArrayList(DEX_ARRAY_INSTRUCTIONS);
+ disallowedClasses.remove(allowedArrayInst);
+
+ assertTrue(method.streamInstructions().anyMatch(isInstruction(allowedArrayInst)));
+ assertTrue(method.streamInstructions().noneMatch(isInstruction(disallowedClasses)));
+ }
+
+ private static void assertFilledArrayData(MethodSubject method) {
+ assertTrue(method.isPresent());
+ List<Class<?>> disallowedClasses = Lists.newArrayList(DEX_ARRAY_INSTRUCTIONS);
+ disallowedClasses.remove(DexFillArrayData.class);
+ disallowedClasses.remove(DexNewArray.class);
+
+ assertTrue(method.streamInstructions().noneMatch(isInstruction(disallowedClasses)));
+ assertTrue(method.streamInstructions().noneMatch(InstructionSubject::isArrayPut));
+ long numNewArray = method.streamInstructions().filter(InstructionSubject::isNewArray).count();
+ long numFillArray =
+ method.streamInstructions().filter(isInstruction(DexFillArrayData.class)).count();
+ assertEquals(numNewArray, numFillArray);
+ }
+
+ public static final class Main {
+ public static void main(String[] args) {
+ referenceArraysNoCasts();
+ referenceArraysWithSubclasses();
+ interfaceArrayWithRawObject();
+ phiFilledNewArray();
+ intsThatUseFilledNewArray();
+ twoDimensionalArrays();
+ objectArraysFilledNewArrayRange();
+ arraysThatUseFilledData();
+ arraysThatUseNewArrayEmpty();
+ reversedArray();
+ arrayWithCorrectCountButIncompleteCoverage();
+ arrayWithExtraInitialPuts();
+ catchHandlerThrowing();
+ catchHandlerNonThrowingFilledNewArray();
+ catchHandlerNonThrowingFillArrayData();
+ }
+
+ @NeverInline
+ private static void referenceArraysNoCasts() {
+ // Test exact class, no null.
+ String[] stringArr = {"a"};
+ System.out.println(Arrays.toString(stringArr));
+ // Tests that no type info is needed when array type is Object[].
+ Object[] objectArr = {"a", 1, null};
+ System.out.println(Arrays.toString(objectArr));
+ // Test that interface arrays work when we have the exact interface already.
+ Serializable[] interfaceArr = {getSerializable(1), null};
+ System.out.println(Arrays.toString(interfaceArr));
+ }
+
+ @Keep
+ private static Serializable getSerializable(Integer value) {
+ return value;
+ }
+
+ @NeverInline
+ private static void referenceArraysWithSubclasses() {
+ Serializable[] interfaceArr = {1, null, 2};
+ System.out.println(Arrays.toString(interfaceArr));
+ Number[] objArray = {1, null, 2};
+ System.out.println(Arrays.toString(objArray));
+ }
+
+ @NeverInline
+ private static void interfaceArrayWithRawObject() {
+ // Interfaces can use filled-new-array only when we know types implement the interface.
+ Serializable[] arr = new Serializable[1];
+ // Transformed from `I get()` to `Object get()`.
+ arr[0] = getObjectThatImplementsSerializable();
+ System.out.println(Arrays.toString(arr));
+ }
+
+ @Keep
+ private static /*Object*/ Serializable getObjectThatImplementsSerializable() {
+ return 1;
+ }
+
+ @NeverInline
+ private static void reversedArray() {
+ int[] arr = new int[5];
+ arr[4] = 4;
+ arr[3] = 3;
+ arr[2] = 2;
+ arr[1] = 1;
+ arr[0] = 0;
+ System.out.println(Arrays.toString(arr));
+ }
+
+ @NeverInline
+ private static void arrayWithCorrectCountButIncompleteCoverage() {
+ int[] arr = new int[5];
+ arr[0] = 0;
+ arr[0] = 1;
+ arr[0] = 2;
+ arr[0] = 3;
+ arr[0] = 4;
+ System.out.println(Arrays.toString(arr));
+ }
+
+ @NeverInline
+ private static void arrayWithExtraInitialPuts() {
+ int[] arr = new int[5];
+ arr[0] = 0;
+ arr[0] = 1;
+ arr[0] = 2;
+ arr[0] = 3;
+ arr[0] = 4;
+ arr[1] = 1;
+ arr[2] = 2;
+ arr[3] = 3;
+ arr[4] = 4;
+ System.out.println(Arrays.toString(arr));
+ }
+
+ @NeverInline
+ private static void catchHandlerNonThrowingFilledNewArray() {
+ try {
+ int[] arr1 = {1, 2, 3};
+ System.currentTimeMillis();
+ System.out.println(Arrays.toString(arr1));
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ }
+
+ @NeverInline
+ private static void catchHandlerNonThrowingFillArrayData() {
+ try {
+ int[] arr = {1, 2, 3, 4, 5, 6};
+ System.currentTimeMillis();
+ System.out.println(Arrays.toString(arr));
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ }
+
+ @NeverInline
+ private static void catchHandlerThrowing() {
+ int[] arr1 = new int[3];
+ arr1[0] = 0;
+ arr1[1] = 1;
+ // Since the array is used in only one spot, and that spot is not within the try/catch, it
+ // should be safe to use filled-new-array, but we don't.
+ try {
+ System.currentTimeMillis();
+ arr1[2] = 2;
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ System.out.println(Arrays.toString(arr1));
+
+ try {
+ // Test filled-new-array with a throwing instruction before the last array-put.
+ int[] arr2 = new int[1];
+ System.currentTimeMillis();
+ arr2[0] = 0;
+ System.out.println(Arrays.toString(arr2));
+
+ // Test filled-array-data with a throwing instruction before the last array-put.
+ short[] arr3 = new short[3];
+ arr3[0] = 0;
+ arr3[1] = 1;
+ System.currentTimeMillis();
+ arr3[2] = 2;
+ System.out.println(Arrays.toString(arr3));
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ }
+
+ @NeverInline
+ private static void phiFilledNewArray() {
+ // The presence of ? should not affect use of filled-new-array.
+ String[] phiArray = {"a", System.nanoTime() > 0 ? "b" : "c"};
+ System.out.println(Arrays.toString(phiArray));
+ }
+
+ @NeverInline
+ private static void intsThatUseFilledNewArray() {
+ // Up to 5 ints uses filled-new-array rather than filled-array-data.
+ int[] intArr = {1, 2, 3, 4, 5};
+ System.out.println(Arrays.toString(intArr));
+ }
+
+ @NeverInline
+ private static void twoDimensionalArrays() {
+ Integer[][] twoDimensions = {new Integer[] {1}, null};
+ System.out.println(Arrays.toString(Arrays.asList(twoDimensions).get(0)));
+ }
+
+ @NeverInline
+ private static void objectArraysFilledNewArrayRange() {
+ // 6 or more elements use /range variant.
+ Object[] objectArr = {"a", 1, null, "d", "e", "f"};
+ System.out.println(Arrays.toString(objectArr));
+ String[] stringArr = {"a", "b", "c", "d", "e", "f"};
+ System.out.println(Arrays.toString(stringArr));
+ Serializable[] interfaceArr = {
+ getSerializable(1), null, getSerializable(3), null, null, getSerializable(6)
+ };
+ System.out.println(Arrays.toString(interfaceArr));
+ }
+
+ @NeverInline
+ private static void arraysThatUseFilledData() {
+ // For int[], <= 5 elements should use NewArrayFilledData (otherwise NewFilledArray is used).
+ int[] intArr = {1, 2, 3, 4, 5, 6};
+ // For other primitives, > 1 element leads to fill-array-data.
+ System.out.println(Arrays.toString(intArr));
+ boolean[] boolArr = {true, false};
+ System.out.println(Arrays.toString(boolArr));
+ byte[] byteArr = {1, 2};
+ System.out.println(Arrays.toString(byteArr));
+ char[] charArr = {'1', '2'};
+ System.out.println(Arrays.toString(charArr));
+ long[] longArr = {1, 2};
+ System.out.println(Arrays.toString(longArr));
+ float[] floatArr = {1, 2};
+ System.out.println(Arrays.toString(floatArr));
+ double[] doubleArr = {1, 2};
+ System.out.println(Arrays.toString(doubleArr));
+ }
+
+ @NeverInline
+ private static void arraysThatUseNewArrayEmpty() {
+ // int/object of size zero should not use filled-new-array.
+ int[] intArr = {};
+ System.out.println(Arrays.toString(intArr));
+ String[] strArr = {};
+ System.out.println(Arrays.toString(strArr));
+
+ // Other primitives with size <= 1 should not use filled-array-data.
+ boolean[] boolArr = {true};
+ System.out.println(Arrays.toString(boolArr));
+ byte[] byteArr = {1};
+ System.out.println(Arrays.toString(byteArr));
+ char[] charArr = {'1'};
+ System.out.println(Arrays.toString(charArr));
+ long[] longArr = {1};
+ System.out.println(Arrays.toString(longArr));
+ float[] floatArr = {1};
+ System.out.println(Arrays.toString(floatArr));
+ double[] doubleArr = {1};
+ System.out.println(Arrays.toString(doubleArr));
+
+ // Array used before all members are set.
+ int[] readArray = new int[2];
+ readArray[0] = (int) (System.currentTimeMillis() / System.nanoTime());
+ readArray[1] = readArray[0] + 1;
+ System.out.println(Arrays.toString(readArray));
+
+ // Array does not have all elements set (we could make this work, but likely this is rare).
+ Integer[] partialArray = new Integer[2];
+ partialArray[0] = 1;
+ System.out.println(Arrays.toString(partialArray));
+
+ // Non-constant array size.
+ int trickyZero = (int) (System.currentTimeMillis() / System.nanoTime());
+ Object[] nonConstSize = new Object[trickyZero + 1];
+ nonConstSize[0] = "a";
+ System.out.println(Arrays.toString(nonConstSize));
+
+ // Non-constant index.
+ Object[] nonConstIndex = new Object[2];
+ nonConstIndex[trickyZero] = 0;
+ nonConstIndex[trickyZero + 1] = 1;
+ System.out.println(Arrays.toString(nonConstIndex));
+
+ // Exceeds our (arbitrary) size limit for /range.
+ String[] bigArr = new String[201];
+ bigArr[0] = "0";
+ bigArr[1] = "1";
+ bigArr[2] = "2";
+ bigArr[3] = "3";
+ bigArr[4] = "4";
+ bigArr[5] = "5";
+ bigArr[6] = "6";
+ bigArr[7] = "7";
+ bigArr[8] = "8";
+ bigArr[9] = "9";
+ bigArr[10] = "10";
+ bigArr[11] = "11";
+ bigArr[12] = "12";
+ bigArr[13] = "13";
+ bigArr[14] = "14";
+ bigArr[15] = "15";
+ bigArr[16] = "16";
+ bigArr[17] = "17";
+ bigArr[18] = "18";
+ bigArr[19] = "19";
+ bigArr[20] = "20";
+ bigArr[21] = "21";
+ bigArr[22] = "22";
+ bigArr[23] = "23";
+ bigArr[24] = "24";
+ bigArr[25] = "25";
+ bigArr[26] = "26";
+ bigArr[27] = "27";
+ bigArr[28] = "28";
+ bigArr[29] = "29";
+ bigArr[30] = "30";
+ bigArr[31] = "31";
+ bigArr[32] = "32";
+ bigArr[33] = "33";
+ bigArr[34] = "34";
+ bigArr[35] = "35";
+ bigArr[36] = "36";
+ bigArr[37] = "37";
+ bigArr[38] = "38";
+ bigArr[39] = "39";
+ bigArr[40] = "40";
+ bigArr[41] = "41";
+ bigArr[42] = "42";
+ bigArr[43] = "43";
+ bigArr[44] = "44";
+ bigArr[45] = "45";
+ bigArr[46] = "46";
+ bigArr[47] = "47";
+ bigArr[48] = "48";
+ bigArr[49] = "49";
+ bigArr[50] = "50";
+ bigArr[51] = "51";
+ bigArr[52] = "52";
+ bigArr[53] = "53";
+ bigArr[54] = "54";
+ bigArr[55] = "55";
+ bigArr[56] = "56";
+ bigArr[57] = "57";
+ bigArr[58] = "58";
+ bigArr[59] = "59";
+ bigArr[60] = "60";
+ bigArr[61] = "61";
+ bigArr[62] = "62";
+ bigArr[63] = "63";
+ bigArr[64] = "64";
+ bigArr[65] = "65";
+ bigArr[66] = "66";
+ bigArr[67] = "67";
+ bigArr[68] = "68";
+ bigArr[69] = "69";
+ bigArr[70] = "70";
+ bigArr[71] = "71";
+ bigArr[72] = "72";
+ bigArr[73] = "73";
+ bigArr[74] = "74";
+ bigArr[75] = "75";
+ bigArr[76] = "76";
+ bigArr[77] = "77";
+ bigArr[78] = "78";
+ bigArr[79] = "79";
+ bigArr[80] = "80";
+ bigArr[81] = "81";
+ bigArr[82] = "82";
+ bigArr[83] = "83";
+ bigArr[84] = "84";
+ bigArr[85] = "85";
+ bigArr[86] = "86";
+ bigArr[87] = "87";
+ bigArr[88] = "88";
+ bigArr[89] = "89";
+ bigArr[90] = "90";
+ bigArr[91] = "91";
+ bigArr[92] = "92";
+ bigArr[93] = "93";
+ bigArr[94] = "94";
+ bigArr[95] = "95";
+ bigArr[96] = "96";
+ bigArr[97] = "97";
+ bigArr[98] = "98";
+ bigArr[99] = "99";
+ bigArr[100] = "100";
+ bigArr[101] = "101";
+ bigArr[102] = "102";
+ bigArr[103] = "103";
+ bigArr[104] = "104";
+ bigArr[105] = "105";
+ bigArr[106] = "106";
+ bigArr[107] = "107";
+ bigArr[108] = "108";
+ bigArr[109] = "109";
+ bigArr[110] = "110";
+ bigArr[111] = "111";
+ bigArr[112] = "112";
+ bigArr[113] = "113";
+ bigArr[114] = "114";
+ bigArr[115] = "115";
+ bigArr[116] = "116";
+ bigArr[117] = "117";
+ bigArr[118] = "118";
+ bigArr[119] = "119";
+ bigArr[120] = "120";
+ bigArr[121] = "121";
+ bigArr[122] = "122";
+ bigArr[123] = "123";
+ bigArr[124] = "124";
+ bigArr[125] = "125";
+ bigArr[126] = "126";
+ bigArr[127] = "127";
+ bigArr[128] = "128";
+ bigArr[129] = "129";
+ bigArr[130] = "130";
+ bigArr[131] = "131";
+ bigArr[132] = "132";
+ bigArr[133] = "133";
+ bigArr[134] = "134";
+ bigArr[135] = "135";
+ bigArr[136] = "136";
+ bigArr[137] = "137";
+ bigArr[138] = "138";
+ bigArr[139] = "139";
+ bigArr[140] = "140";
+ bigArr[141] = "141";
+ bigArr[142] = "142";
+ bigArr[143] = "143";
+ bigArr[144] = "144";
+ bigArr[145] = "145";
+ bigArr[146] = "146";
+ bigArr[147] = "147";
+ bigArr[148] = "148";
+ bigArr[149] = "149";
+ bigArr[150] = "150";
+ bigArr[151] = "151";
+ bigArr[152] = "152";
+ bigArr[153] = "153";
+ bigArr[154] = "154";
+ bigArr[155] = "155";
+ bigArr[156] = "156";
+ bigArr[157] = "157";
+ bigArr[158] = "158";
+ bigArr[159] = "159";
+ bigArr[160] = "160";
+ bigArr[161] = "161";
+ bigArr[162] = "162";
+ bigArr[163] = "163";
+ bigArr[164] = "164";
+ bigArr[165] = "165";
+ bigArr[166] = "166";
+ bigArr[167] = "167";
+ bigArr[168] = "168";
+ bigArr[169] = "169";
+ bigArr[170] = "170";
+ bigArr[171] = "171";
+ bigArr[172] = "172";
+ bigArr[173] = "173";
+ bigArr[174] = "174";
+ bigArr[175] = "175";
+ bigArr[176] = "176";
+ bigArr[177] = "177";
+ bigArr[178] = "178";
+ bigArr[179] = "179";
+ bigArr[180] = "180";
+ bigArr[181] = "181";
+ bigArr[182] = "182";
+ bigArr[183] = "183";
+ bigArr[184] = "184";
+ bigArr[185] = "185";
+ bigArr[186] = "186";
+ bigArr[187] = "187";
+ bigArr[188] = "188";
+ bigArr[189] = "189";
+ bigArr[190] = "190";
+ bigArr[191] = "191";
+ bigArr[192] = "192";
+ bigArr[193] = "193";
+ bigArr[194] = "194";
+ bigArr[195] = "195";
+ bigArr[196] = "196";
+ bigArr[197] = "197";
+ bigArr[198] = "198";
+ bigArr[199] = "199";
+ bigArr[200] = "200";
+ System.out.println(Arrays.asList(bigArr).get(200));
+ }
+ }
+}