Reuse constants in NewArrayEmpty rewriting
When NewArrayEmpty is rewritten to a series of aput instructions where
the materialization of the constant values is moved, reuse these constants if same value is put multiple times.
Limit the sharing to less than 100 constants simultaniously.
Only implemented for string constants.
Bug: b/297500960
Bug: b/297497196
Change-Id: Ib1e5561e4659a3e045fe9b1f886896ae7e62e9c3
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
index 9764e66..4297b03 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
@@ -4,9 +4,12 @@
 
 package com.android.tools.r8.ir.conversion.passes;
 
+import com.android.tools.r8.dex.Constants;
 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.DexItem;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.type.ArrayTypeElement;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
@@ -31,6 +34,8 @@
 import com.android.tools.r8.utils.SetUtils;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
+import java.util.IdentityHashMap;
+import java.util.Map;
 import java.util.Set;
 
 public class FilledNewArrayRewriter extends CodeRewriterPass<AppInfo> {
@@ -294,19 +299,157 @@
     return result;
   }
 
+  private static class ConstantMaterializingInstructionCache {
+
+    // All DEX aput instructions takes an 8-bit wide register value for the source.
+    private final int MAX_MATERIALIZING_CONSTANTS = Constants.U8BIT_MAX - 16;
+    // Track constants as DexItems, DexString for string constants and DexType for class constants.
+    private final Map<DexItem, Integer> constantOccurrences = new IdentityHashMap<>();
+    private final Map<DexItem, Value> constantValue = new IdentityHashMap<>();
+
+    private ConstantMaterializingInstructionCache(NewArrayFilled newArrayFilled) {
+      for (Value elementValue : newArrayFilled.inValues()) {
+        if (elementValue.hasAnyUsers()) {
+          continue;
+        }
+        if (elementValue.isConstString()) {
+          addOccurrence(elementValue.getDefinition().asConstString().getValue());
+        } else if (elementValue.isConstClass()) {
+          addOccurrence(elementValue.getDefinition().asConstClass().getValue());
+        }
+        // Don't canonicalize numbers, as on DEX FilledNewArray is supported for primitives
+        // on all versions.
+      }
+    }
+
+    private Value getValue(Value elementValue) {
+      if (elementValue.isConstString()) {
+        DexString string = elementValue.getDefinition().asConstString().getValue();
+        Value value = constantValue.get(string);
+        if (value != null) {
+          seenOcourence(string);
+          return value;
+        }
+      } else if (elementValue.isConstClass()) {
+        DexType type = elementValue.getDefinition().asConstClass().getValue();
+        Value value = constantValue.get(type);
+        if (value != null) {
+          seenOcourence(type);
+          return value;
+        }
+      }
+      return null;
+    }
+
+    private DexItem smallestConstant(DexItem c1, DexItem c2) {
+      if (c1 instanceof DexString) {
+        if (c2 instanceof DexString) {
+          return ((DexString) c1).compareTo((DexString) c2) < 0 ? c1 : c2;
+        } else {
+          assert c2 instanceof DexType;
+          return c2; // String larger than class.
+        }
+      } else {
+        assert c1 instanceof DexType;
+        if (c2 instanceof DexType) {
+          return ((DexType) c1).compareTo((DexType) c2) < 0 ? c1 : c2;
+        } else {
+          assert c2 instanceof DexString;
+          return c1; // String larger than class.
+        }
+      }
+    }
+
+    private DexItem getConstant(Value value) {
+      Instruction instruction = value.getDefinition();
+      if (instruction.isConstString()) {
+        return instruction.asConstString().getValue();
+      } else {
+        assert instruction.isConstClass();
+        return instruction.asConstClass().getValue();
+      }
+    }
+
+    private void putNewValue(Value value) {
+      DexItem constant = getConstant(value);
+      assert constantOccurrences.containsKey(constant);
+      assert !constantValue.containsKey(constant);
+      if (constantValue.size() < MAX_MATERIALIZING_CONSTANTS) {
+        constantValue.put(constant, value);
+      } else {
+        assert constantValue.size() == MAX_MATERIALIZING_CONSTANTS;
+        // Find the least valuable active constant.
+        int leastOccurrences = Integer.MAX_VALUE;
+        DexItem valueWithLeastOccurrences = null;
+        for (DexItem key : constantValue.keySet()) {
+          int remainingOccurrences = constantOccurrences.get(key);
+          if (remainingOccurrences < leastOccurrences) {
+            leastOccurrences = remainingOccurrences;
+            valueWithLeastOccurrences = key;
+          } else if (remainingOccurrences == leastOccurrences) {
+            assert valueWithLeastOccurrences
+                != null; // Will always be set before the else branch is ever hit.
+            valueWithLeastOccurrences = smallestConstant(valueWithLeastOccurrences, key);
+          }
+        }
+        // Replace the new constant with the current least valuable one if more valuable.
+        int newConstantOccurrences = constantOccurrences.get(constant);
+        if (newConstantOccurrences > leastOccurrences
+            || (newConstantOccurrences == leastOccurrences
+                && smallestConstant(valueWithLeastOccurrences, constant)
+                    == valueWithLeastOccurrences)) {
+          constantValue.remove(valueWithLeastOccurrences);
+          constantValue.put(constant, value);
+        }
+        assert constantValue.size() == MAX_MATERIALIZING_CONSTANTS;
+      }
+      seenOcourence(constant);
+    }
+
+    private void addOccurrence(DexItem constant) {
+      constantOccurrences.compute(constant, (k, v) -> (v == null) ? 1 : ++v);
+    }
+
+    private void seenOcourence(DexItem constant) {
+      int remainingOccourences =
+          constantOccurrences.compute(constant, (k, v) -> (v == null) ? Integer.MAX_VALUE : --v);
+      // Remove from sets after last occurrence.
+      if (remainingOccourences == 0) {
+        constantOccurrences.remove(constant);
+        constantValue.remove(constant);
+      }
+    }
+
+    private boolean checkAllOccurrenceProcessed() {
+      assert constantOccurrences.size() == 0;
+      assert constantValue.size() == 0;
+      return true;
+    }
+  }
+
   private void rewriteToArrayPuts(
       IRCode code,
       BasicBlockIterator blockIterator,
       BasicBlockInstructionListIterator instructionIterator,
       NewArrayFilled newArrayFilled) {
     NewArrayEmpty newArrayEmpty = rewriteToNewArrayEmpty(code, instructionIterator, newArrayFilled);
+
+    ConstantMaterializingInstructionCache constantMaterializingInstructionCache =
+        new ConstantMaterializingInstructionCache(newArrayFilled);
+
     int index = 0;
     for (Value elementValue : newArrayFilled.inValues()) {
       if (instructionIterator.getBlock().hasCatchHandlers()) {
         BasicBlock splitBlock =
             instructionIterator.splitCopyCatchHandlers(code, blockIterator, options);
         instructionIterator = splitBlock.listIterator(code);
-        Value putValue = getPutValue(code, instructionIterator, newArrayEmpty, elementValue);
+        Value putValue =
+            getPutValue(
+                code,
+                instructionIterator,
+                newArrayEmpty,
+                elementValue,
+                constantMaterializingInstructionCache);
         blockIterator.positionAfterPreviousBlock(splitBlock);
         splitBlock = instructionIterator.splitCopyCatchHandlers(code, blockIterator, options);
         instructionIterator = splitBlock.listIterator(code);
@@ -314,18 +457,27 @@
         blockIterator.positionAfterPreviousBlock(splitBlock);
         mayHaveRedundantBlocks = true;
       } else {
-        Value putValue = getPutValue(code, instructionIterator, newArrayEmpty, elementValue);
+        Value putValue =
+            getPutValue(
+                code,
+                instructionIterator,
+                newArrayEmpty,
+                elementValue,
+                constantMaterializingInstructionCache);
         addArrayPut(code, instructionIterator, newArrayEmpty, index, putValue);
       }
       index++;
     }
+
+    assert constantMaterializingInstructionCache.checkAllOccurrenceProcessed();
   }
 
   private Value getPutValue(
       IRCode code,
       BasicBlockInstructionListIterator instructionIterator,
       NewArrayEmpty newArrayEmpty,
-      Value elementValue) {
+      Value elementValue,
+      ConstantMaterializingInstructionCache constantMaterializingInstructionCache) {
     // If the value was only used by the NewArrayFilled instruction it now has no normal users.
     if (elementValue.hasAnyUsers()
         || !(elementValue.isConstString()
@@ -333,13 +485,22 @@
             || elementValue.isConstClass())) {
       return elementValue;
     }
+
+    Value existingValue = constantMaterializingInstructionCache.getValue(elementValue);
+    if (existingValue != null) {
+      addToRemove(elementValue.definition);
+      return existingValue;
+    }
+
     Instruction copy;
     if (elementValue.isConstNumber()) {
       copy = ConstNumber.copyOf(code, elementValue.definition.asConstNumber());
     } else if (elementValue.isConstString()) {
       copy = ConstString.copyOf(code, elementValue.definition.asConstString());
+      constantMaterializingInstructionCache.putNewValue(copy.asConstString().outValue());
     } else if (elementValue.isConstClass()) {
       copy = ConstClass.copyOf(code, elementValue.definition.asConstClass());
+      constantMaterializingInstructionCache.putNewValue(copy.asConstClass().outValue());
     } else {
       assert false;
       return elementValue;
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithNonUniqueValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithNonUniqueValuesTest.java
new file mode 100644
index 0000000..cc339a8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithNonUniqueValuesTest.java
@@ -0,0 +1,199 @@
+// 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.rewrite.arrays;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+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 java.util.Arrays;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ConstClassArrayWithNonUniqueValuesTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public CompilationMode compilationMode;
+
+  @Parameters(name = "{0}, mode = {1}")
+  public static Iterable<?> data() {
+    return buildParameters(
+        getTestParameters().withDefaultCfRuntime().withDexRuntimesAndAllApiLevels().build(),
+        CompilationMode.values());
+  }
+
+  private static final String EXPECTED_OUTPUT = StringUtils.lines("100", "104");
+
+  public boolean canUseFilledNewArrayOfClass(TestParameters parameters) {
+    return parameters.isDexRuntime()
+        && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N);
+  }
+
+  private void inspect(
+      MethodSubject method, int constClasses, int puts, boolean insideCatchHandler) {
+    boolean expectingFilledNewArray =
+        canUseFilledNewArrayOfClass(parameters) && !insideCatchHandler;
+    assertEquals(
+        expectingFilledNewArray ? 0 : puts,
+        method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
+    assertEquals(
+        expectingFilledNewArray ? 1 : 0,
+        method.streamInstructions().filter(InstructionSubject::isFilledNewArray).count());
+    assertEquals(
+        expectingFilledNewArray || parameters.isCfRuntime() ? puts : constClasses,
+        method.streamInstructions().filter(InstructionSubject::isConstClass).count());
+  }
+
+  private void inspect(CodeInspector inspector) {
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), 1, 100, false);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"), 26, 104, false);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .inspect(this::inspect)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .enableInliningAnnotations()
+        .addDontObfuscate()
+        .run(parameters.getRuntime(), TestClass.class)
+        .inspect(this::inspect)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  public static final class TestClass {
+
+    @NeverInline
+    public static void m1() {
+      Class<?>[] array =
+          new Class<?>[] {
+            A.class, A.class, A.class, A.class, A.class, A.class, A.class, A.class, A.class,
+                A.class,
+            A.class, A.class, A.class, A.class, A.class, A.class, A.class, A.class, A.class,
+                A.class,
+            A.class, A.class, A.class, A.class, A.class, A.class, A.class, A.class, A.class,
+                A.class,
+            A.class, A.class, A.class, A.class, A.class, A.class, A.class, A.class, A.class,
+                A.class,
+            A.class, A.class, A.class, A.class, A.class, A.class, A.class, A.class, A.class,
+                A.class,
+            A.class, A.class, A.class, A.class, A.class, A.class, A.class, A.class, A.class,
+                A.class,
+            A.class, A.class, A.class, A.class, A.class, A.class, A.class, A.class, A.class,
+                A.class,
+            A.class, A.class, A.class, A.class, A.class, A.class, A.class, A.class, A.class,
+                A.class,
+            A.class, A.class, A.class, A.class, A.class, A.class, A.class, A.class, A.class,
+                A.class,
+            A.class, A.class, A.class, A.class, A.class, A.class, A.class, A.class, A.class,
+                A.class,
+          };
+      System.out.println(Arrays.asList(array).size());
+    }
+
+    @NeverInline
+    public static void m2() {
+      Class<?>[] array =
+          new Class<?>[] {
+            A.class, B.class, C.class, D.class, E.class, F.class, G.class, H.class, I.class,
+            J.class, K.class, L.class, M.class, N.class, O.class, P.class, Q.class, R.class,
+            S.class, T.class, U.class, V.class, W.class, X.class, Y.class, Z.class, A.class,
+            B.class, C.class, D.class, E.class, F.class, G.class, H.class, I.class, J.class,
+            K.class, L.class, M.class, N.class, O.class, P.class, Q.class, R.class, S.class,
+            T.class, U.class, V.class, W.class, X.class, Y.class, Z.class, A.class, B.class,
+            C.class, D.class, E.class, F.class, G.class, H.class, I.class, J.class, K.class,
+            L.class, M.class, N.class, O.class, P.class, Q.class, R.class, S.class, T.class,
+            U.class, V.class, W.class, X.class, Y.class, Z.class, A.class, B.class, C.class,
+            D.class, E.class, F.class, G.class, H.class, I.class, J.class, K.class, L.class,
+            M.class, N.class, O.class, P.class, Q.class, R.class, S.class, T.class, U.class,
+            V.class, W.class, X.class, Y.class, Z.class,
+          };
+      System.out.println(Arrays.asList(array).size());
+    }
+
+    public static void main(String[] args) {
+      m1();
+      m2();
+    }
+  }
+
+  static class A {}
+
+  static class B {}
+
+  static class C {}
+
+  static class D {}
+
+  static class E {}
+
+  static class F {}
+
+  static class G {}
+
+  static class H {}
+
+  static class I {}
+
+  static class J {}
+
+  static class K {}
+
+  static class L {}
+
+  static class M {}
+
+  static class N {}
+
+  static class O {}
+
+  static class P {}
+
+  static class Q {}
+
+  static class R {}
+
+  static class S {}
+
+  static class T {}
+
+  static class U {}
+
+  static class V {}
+
+  static class W {}
+
+  static class X {}
+
+  static class Y {}
+
+  static class Z {}
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithUniqueValuesTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithUniqueValuesTest.java
new file mode 100644
index 0000000..4dca11b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/ConstClassArrayWithUniqueValuesTest.java
@@ -0,0 +1,379 @@
+// 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.rewrite.arrays;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+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 java.util.Arrays;
+import java.util.Iterator;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ConstClassArrayWithUniqueValuesTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameter(1)
+  public CompilationMode compilationMode;
+
+  @Parameters(name = "{0}, mode = {1}")
+  public static Iterable<?> data() {
+    return buildParameters(
+        getTestParameters().withDefaultCfRuntime().withDexRuntimesAndAllApiLevels().build(),
+        CompilationMode.values());
+  }
+
+  private static final String EXPECTED_OUTPUT =
+      StringUtils.lines("[A00, A01, A02, A03, A04]", "[A00, A01, A02, A03, A04]", "100");
+
+  public boolean canUseFilledNewArrayOfClass(TestParameters parameters) {
+    return parameters.isDexRuntime()
+        && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.N);
+  }
+
+  private enum State {
+    EXPECTING_CONSTCLASS,
+    EXPECTING_APUTOBJECT
+  }
+
+  private void inspect(MethodSubject method, int puts, boolean insideCatchHandler) {
+    boolean expectingFilledNewArray =
+        canUseFilledNewArrayOfClass(parameters) && !insideCatchHandler;
+    assertEquals(
+        expectingFilledNewArray ? 0 : puts,
+        method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
+    assertEquals(
+        expectingFilledNewArray ? 1 : 0,
+        method.streamInstructions().filter(InstructionSubject::isFilledNewArray).count());
+    assertEquals(
+        puts, method.streamInstructions().filter(InstructionSubject::isConstClass).count());
+    if (!expectingFilledNewArray) {
+      // Test that const-string and aput instructions are interleaved by the lowering of
+      // filled-new-array.
+      int aputCount = 0;
+      State state = State.EXPECTING_CONSTCLASS;
+      Iterator<InstructionSubject> iterator = method.iterateInstructions();
+      while (iterator.hasNext()) {
+        InstructionSubject instruction = iterator.next();
+        if (instruction.isConstClass()) {
+          assertEquals(State.EXPECTING_CONSTCLASS, state);
+          state = State.EXPECTING_APUTOBJECT;
+        } else if (instruction.isArrayPut()) {
+          assertEquals(State.EXPECTING_APUTOBJECT, state);
+          state = State.EXPECTING_CONSTCLASS;
+          aputCount++;
+        }
+      }
+      assertEquals(State.EXPECTING_CONSTCLASS, state);
+      assertEquals(puts, aputCount);
+    }
+  }
+
+  private void inspect(CodeInspector inspector) {
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), 5, false);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"), 5, true);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m3"), 100, false);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .inspect(this::inspect)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters)
+        .enableInliningAnnotations()
+        .addDontObfuscate()
+        .run(parameters.getRuntime(), TestClass.class)
+        .inspect(this::inspect)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  public static final class TestClass {
+
+    @NeverInline
+    public static void m1() {
+      Class<?>[] array = {A00.class, A01.class, A02.class, A03.class, A04.class};
+      printArray(array);
+    }
+
+    @NeverInline
+    public static void m2() {
+      try {
+        Class<?>[] array = {A00.class, A01.class, A02.class, A03.class, A04.class};
+        printArray(array);
+      } catch (Exception e) {
+        throw new RuntimeException();
+      }
+    }
+
+    @NeverInline
+    public static void m3() {
+      Class<?>[] array =
+          new Class<?>[] {
+            A00.class, A01.class, A02.class, A03.class, A04.class, A05.class, A06.class, A07.class,
+            A08.class, A09.class, A10.class, A11.class, A12.class, A13.class, A14.class, A15.class,
+            A16.class, A17.class, A18.class, A19.class, A20.class, A21.class, A22.class, A23.class,
+            A24.class, A25.class, A26.class, A27.class, A28.class, A29.class, A30.class, A31.class,
+            A32.class, A33.class, A34.class, A35.class, A36.class, A37.class, A38.class, A39.class,
+            A40.class, A41.class, A42.class, A43.class, A44.class, A45.class, A46.class, A47.class,
+            A48.class, A49.class, A50.class, A51.class, A52.class, A53.class, A54.class, A55.class,
+            A56.class, A57.class, A58.class, A59.class, A60.class, A61.class, A62.class, A63.class,
+            A64.class, A65.class, A66.class, A67.class, A68.class, A69.class, A70.class, A71.class,
+            A72.class, A73.class, A74.class, A75.class, A76.class, A77.class, A78.class, A79.class,
+            A80.class, A81.class, A82.class, A83.class, A84.class, A85.class, A86.class, A87.class,
+            A88.class, A89.class, A90.class, A91.class, A92.class, A93.class, A94.class, A95.class,
+            A96.class, A97.class, A98.class, A99.class,
+          };
+      System.out.println(Arrays.asList(array).size());
+    }
+
+    @NeverInline
+    public static void printArray(Class<?>[] classArray) {
+      System.out.print("[");
+      for (Class<?> clazz : classArray) {
+        if (clazz != classArray[0]) {
+          System.out.print(", ");
+        }
+        String simpleName = clazz.getName();
+        if (simpleName.lastIndexOf("$") > 0) {
+          simpleName = simpleName.substring(simpleName.lastIndexOf("$") + 1);
+        }
+        System.out.print(simpleName);
+      }
+      System.out.println("]");
+    }
+
+    public static void main(String[] args) {
+      m1();
+      m2();
+      m3();
+    }
+  }
+
+  static class A00 {}
+
+  static class A01 {}
+
+  static class A02 {}
+
+  static class A03 {}
+
+  static class A04 {}
+
+  static class A05 {}
+
+  static class A06 {}
+
+  static class A07 {}
+
+  static class A08 {}
+
+  static class A09 {}
+
+  static class A10 {}
+
+  static class A11 {}
+
+  static class A12 {}
+
+  static class A13 {}
+
+  static class A14 {}
+
+  static class A15 {}
+
+  static class A16 {}
+
+  static class A17 {}
+
+  static class A18 {}
+
+  static class A19 {}
+
+  static class A20 {}
+
+  static class A21 {}
+
+  static class A22 {}
+
+  static class A23 {}
+
+  static class A24 {}
+
+  static class A25 {}
+
+  static class A26 {}
+
+  static class A27 {}
+
+  static class A28 {}
+
+  static class A29 {}
+
+  static class A30 {}
+
+  static class A31 {}
+
+  static class A32 {}
+
+  static class A33 {}
+
+  static class A34 {}
+
+  static class A35 {}
+
+  static class A36 {}
+
+  static class A37 {}
+
+  static class A38 {}
+
+  static class A39 {}
+
+  static class A40 {}
+
+  static class A41 {}
+
+  static class A42 {}
+
+  static class A43 {}
+
+  static class A44 {}
+
+  static class A45 {}
+
+  static class A46 {}
+
+  static class A47 {}
+
+  static class A48 {}
+
+  static class A49 {}
+
+  static class A50 {}
+
+  static class A51 {}
+
+  static class A52 {}
+
+  static class A53 {}
+
+  static class A54 {}
+
+  static class A55 {}
+
+  static class A56 {}
+
+  static class A57 {}
+
+  static class A58 {}
+
+  static class A59 {}
+
+  static class A60 {}
+
+  static class A61 {}
+
+  static class A62 {}
+
+  static class A63 {}
+
+  static class A64 {}
+
+  static class A65 {}
+
+  static class A66 {}
+
+  static class A67 {}
+
+  static class A68 {}
+
+  static class A69 {}
+
+  static class A70 {}
+
+  static class A71 {}
+
+  static class A72 {}
+
+  static class A73 {}
+
+  static class A74 {}
+
+  static class A75 {}
+
+  static class A76 {}
+
+  static class A77 {}
+
+  static class A78 {}
+
+  static class A79 {}
+
+  static class A80 {}
+
+  static class A81 {}
+
+  static class A82 {}
+
+  static class A83 {}
+
+  static class A84 {}
+
+  static class A85 {}
+
+  static class A86 {}
+
+  static class A87 {}
+
+  static class A88 {}
+
+  static class A89 {}
+
+  static class A90 {}
+
+  static class A91 {}
+
+  static class A92 {}
+
+  static class A93 {}
+
+  static class A94 {}
+
+  static class A95 {}
+
+  static class A96 {}
+
+  static class A97 {}
+
+  static class A98 {}
+
+  static class A99 {}
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithNonUniqueValuesTest.java
similarity index 60%
copy from src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayTest.java
copy to src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithNonUniqueValuesTest.java
index b2e70a7..220e5dc 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithNonUniqueValuesTest.java
@@ -16,7 +16,6 @@
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.util.Arrays;
-import java.util.Iterator;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -24,7 +23,7 @@
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
-public class StringArrayTest extends TestBase {
+public class StringArrayWithNonUniqueValuesTest extends TestBase {
 
   @Parameter(0)
   public TestParameters parameters;
@@ -39,53 +38,31 @@
         CompilationMode.values());
   }
 
-  private static final String EXPECTED_OUTPUT =
-      StringUtils.lines("[A, B, C, D, E]", "[F, G, H, I, J]");
+  private static final String EXPECTED_OUTPUT = StringUtils.lines("100", "104");
 
   public boolean canUseFilledNewArrayOfStrings(TestParameters parameters) {
     return parameters.isDexRuntime()
         && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.K);
   }
 
-  private enum State {
-    EXPECTING_CONSTSTRING,
-    EXPECTING_APUTOBJECT
-  }
-
-  private void inspect(MethodSubject method, boolean insideCatchHandler) {
+  private void inspect(
+      MethodSubject method, int constStrings, int puts, boolean insideCatchHandler) {
     boolean expectingFilledNewArray =
         canUseFilledNewArrayOfStrings(parameters) && !insideCatchHandler;
     assertEquals(
-        expectingFilledNewArray ? 0 : 5,
+        expectingFilledNewArray ? 0 : puts,
         method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
     assertEquals(
         expectingFilledNewArray ? 1 : 0,
         method.streamInstructions().filter(InstructionSubject::isFilledNewArray).count());
-    if (!expectingFilledNewArray) {
-      // Test that const-string and aput instructions are interleaved by the lowering of
-      // filled-new-array.
-      int aputCount = 0;
-      State state = State.EXPECTING_CONSTSTRING;
-      Iterator<InstructionSubject> iterator = method.iterateInstructions();
-      while (iterator.hasNext()) {
-        InstructionSubject instruction = iterator.next();
-        if (instruction.isConstString()) {
-          assertEquals(State.EXPECTING_CONSTSTRING, state);
-          state = State.EXPECTING_APUTOBJECT;
-        } else if (instruction.isArrayPut()) {
-          assertEquals(State.EXPECTING_APUTOBJECT, state);
-          state = State.EXPECTING_CONSTSTRING;
-          aputCount++;
-        }
-      }
-      assertEquals(State.EXPECTING_CONSTSTRING, state);
-      assertEquals(5, aputCount);
-    }
+    assertEquals(
+        expectingFilledNewArray || parameters.isCfRuntime() ? puts : constStrings,
+        method.streamInstructions().filter(InstructionSubject::isConstString).count());
   }
 
   private void inspect(CodeInspector inspector) {
-    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), false);
-    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"), true);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), 1, 100, false);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"), 26, 104, false);
   }
 
   @Test
@@ -116,18 +93,36 @@
 
     @NeverInline
     public static void m1() {
-      String[] stringArray = {"A", "B", "C", "D", "E"};
-      System.out.println(Arrays.asList(stringArray));
+      String[] array =
+          new String[] {
+            "A", "A", "A", "A", "A", "A", "A", "A", "A", "A",
+            "A", "A", "A", "A", "A", "A", "A", "A", "A", "A",
+            "A", "A", "A", "A", "A", "A", "A", "A", "A", "A",
+            "A", "A", "A", "A", "A", "A", "A", "A", "A", "A",
+            "A", "A", "A", "A", "A", "A", "A", "A", "A", "A",
+            "A", "A", "A", "A", "A", "A", "A", "A", "A", "A",
+            "A", "A", "A", "A", "A", "A", "A", "A", "A", "A",
+            "A", "A", "A", "A", "A", "A", "A", "A", "A", "A",
+            "A", "A", "A", "A", "A", "A", "A", "A", "A", "A",
+            "A", "A", "A", "A", "A", "A", "A", "A", "A", "A",
+          };
+      System.out.println(Arrays.asList(array).size());
     }
 
     @NeverInline
     public static void m2() {
-      try {
-        String[] stringArray = {"F", "G", "H", "I", "J"};
-        System.out.println(Arrays.asList(stringArray));
-      } catch (Exception e) {
-        throw new RuntimeException();
-      }
+      String[] array =
+          new String[] {
+            "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q",
+                "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
+            "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q",
+                "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
+            "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q",
+                "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
+            "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q",
+                "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
+          };
+      System.out.println(Arrays.asList(array).size());
     }
 
     public static void main(String[] args) {
diff --git a/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayTest.java b/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithUniqueValuesTest.java
similarity index 74%
rename from src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayTest.java
rename to src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithUniqueValuesTest.java
index b2e70a7..c3a70be 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/StringArrayWithUniqueValuesTest.java
@@ -24,7 +24,7 @@
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
-public class StringArrayTest extends TestBase {
+public class StringArrayWithUniqueValuesTest extends TestBase {
 
   @Parameter(0)
   public TestParameters parameters;
@@ -40,7 +40,7 @@
   }
 
   private static final String EXPECTED_OUTPUT =
-      StringUtils.lines("[A, B, C, D, E]", "[F, G, H, I, J]");
+      StringUtils.lines("[A, B, C, D, E]", "[F, G, H, I, J]", "100");
 
   public boolean canUseFilledNewArrayOfStrings(TestParameters parameters) {
     return parameters.isDexRuntime()
@@ -52,15 +52,17 @@
     EXPECTING_APUTOBJECT
   }
 
-  private void inspect(MethodSubject method, boolean insideCatchHandler) {
+  private void inspect(MethodSubject method, int puts, boolean insideCatchHandler) {
     boolean expectingFilledNewArray =
         canUseFilledNewArrayOfStrings(parameters) && !insideCatchHandler;
     assertEquals(
-        expectingFilledNewArray ? 0 : 5,
+        expectingFilledNewArray ? 0 : puts,
         method.streamInstructions().filter(InstructionSubject::isArrayPut).count());
     assertEquals(
         expectingFilledNewArray ? 1 : 0,
         method.streamInstructions().filter(InstructionSubject::isFilledNewArray).count());
+    assertEquals(
+        puts, method.streamInstructions().filter(InstructionSubject::isConstString).count());
     if (!expectingFilledNewArray) {
       // Test that const-string and aput instructions are interleaved by the lowering of
       // filled-new-array.
@@ -79,13 +81,14 @@
         }
       }
       assertEquals(State.EXPECTING_CONSTSTRING, state);
-      assertEquals(5, aputCount);
+      assertEquals(puts, aputCount);
     }
   }
 
   private void inspect(CodeInspector inspector) {
-    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), false);
-    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"), true);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m1"), 5, false);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m2"), 5, true);
+    inspect(inspector.clazz(TestClass.class).uniqueMethodWithOriginalName("m3"), 100, false);
   }
 
   @Test
@@ -130,9 +133,28 @@
       }
     }
 
+    @NeverInline
+    public static void m3() {
+      String[] array =
+          new String[] {
+            "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
+            "10", "11", "12", "13", "14", "15", "16", "17", "18", "19",
+            "20", "21", "22", "23", "24", "25", "26", "27", "28", "29",
+            "30", "31", "32", "33", "34", "35", "36", "37", "38", "39",
+            "40", "41", "42", "43", "44", "45", "46", "47", "48", "49",
+            "50", "51", "52", "53", "54", "55", "56", "57", "58", "59",
+            "60", "61", "62", "63", "64", "65", "66", "67", "68", "69",
+            "70", "71", "72", "73", "74", "75", "76", "77", "78", "79",
+            "80", "81", "82", "83", "84", "85", "86", "87", "88", "89",
+            "90", "91", "92", "93", "94", "95", "96", "97", "98", "99",
+          };
+      System.out.println(Arrays.asList(array).size());
+    }
+
     public static void main(String[] args) {
       m1();
       m2();
+      m3();
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
index e7e5bc3..0b6a2b6 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
@@ -50,6 +50,7 @@
 import com.android.tools.r8.dex.code.DexDivLong;
 import com.android.tools.r8.dex.code.DexDivLong2Addr;
 import com.android.tools.r8.dex.code.DexFilledNewArray;
+import com.android.tools.r8.dex.code.DexFilledNewArrayRange;
 import com.android.tools.r8.dex.code.DexGoto;
 import com.android.tools.r8.dex.code.DexIfEq;
 import com.android.tools.r8.dex.code.DexIfEqz;
@@ -641,7 +642,8 @@
 
   @Override
   public boolean isFilledNewArray() {
-    return instruction instanceof DexFilledNewArray;
+    return instruction instanceof DexFilledNewArray
+        || instruction instanceof DexFilledNewArrayRange;
   }
 
   @Override