Merge commit '8aba099c7ddc36e0284ed4d9d701691923dfc5ab' into dev-release

Change-Id: I431fd3cc4946a764fedf2a1dbf2606491382611b
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs.json b/src/library_desugar/jdk11/desugar_jdk_libs.json
index 5265c49..221f44c 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs.json
@@ -1,5 +1,5 @@
 {
-  "identifier": "com.tools.android:desugar_jdk_libs_configuration:2.1.2",
+  "identifier": "com.tools.android:desugar_jdk_libs_configuration:2.1.3",
   "configuration_format_version": 101,
   "required_compilation_api_level": 30,
   "synthesized_library_classes_package_prefix": "j$.",
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs_minimal.json b/src/library_desugar/jdk11/desugar_jdk_libs_minimal.json
index a3cd551..f4febc3 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs_minimal.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs_minimal.json
@@ -1,5 +1,5 @@
 {
-  "identifier": "com.tools.android:desugar_jdk_libs_configuration_minimal:2.1.2",
+  "identifier": "com.tools.android:desugar_jdk_libs_configuration_minimal:2.1.3",
   "configuration_format_version": 101,
   "required_compilation_api_level": 24,
   "synthesized_library_classes_package_prefix": "j$.",
diff --git a/src/library_desugar/jdk11/desugar_jdk_libs_nio.json b/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
index 05c530d..2c8bcb4 100644
--- a/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
+++ b/src/library_desugar/jdk11/desugar_jdk_libs_nio.json
@@ -1,5 +1,5 @@
 {
-  "identifier": "com.tools.android:desugar_jdk_libs_configuration_nio:2.1.2",
+  "identifier": "com.tools.android:desugar_jdk_libs_configuration_nio:2.1.3",
   "configuration_format_version": 101,
   "required_compilation_api_level": 30,
   "synthesized_library_classes_package_prefix": "j$.",
diff --git a/src/main/java/com/android/tools/r8/cf/CfRegisterAllocator.java b/src/main/java/com/android/tools/r8/cf/CfRegisterAllocator.java
index ad077ea..a23ee09 100644
--- a/src/main/java/com/android/tools/r8/cf/CfRegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/cf/CfRegisterAllocator.java
@@ -319,7 +319,7 @@
 
   private void updateHints(LiveIntervals intervals) {
     for (Phi phi : intervals.getValue().uniquePhiUsers()) {
-      if (!phi.isValueOnStack() && phi.getLiveIntervals().getHint() == null) {
+      if (!phi.isValueOnStack() && !phi.getLiveIntervals().hasHint()) {
         phi.getLiveIntervals().setHint(intervals, unhandled);
         for (Value value : phi.getOperands()) {
           value.getLiveIntervals().setHint(intervals, unhandled);
@@ -329,7 +329,7 @@
   }
 
   private boolean tryHint(LiveIntervals unhandled) {
-    if (unhandled.getHint() == null) {
+    if (!unhandled.hasHint()) {
       return false;
     }
     boolean isWide = unhandled.getType().isWide();
@@ -521,10 +521,11 @@
 
   private void applyInstructionsBackwardsToRegisterLiveness(
       BasicBlock block, IntSet liveRegisters, int suffixSize) {
-    InstructionIterator iterator = block.iterator(block.getInstructions().size());
-    int instructionsLeft = suffixSize;
-    while (--instructionsLeft >= 0 && iterator.hasPrevious()) {
-      Instruction current = iterator.previous();
+    int i = 0;
+    for (var current = block.getLastInstruction(); current != null; current = current.getPrev()) {
+      if (++i > suffixSize) {
+        break;
+      }
       Value outValue = current.outValue();
       if (outValue != null && outValue.needsRegister()) {
         int register = getRegisterForValue(outValue);
diff --git a/src/main/java/com/android/tools/r8/cf/LoadStoreHelper.java b/src/main/java/com/android/tools/r8/cf/LoadStoreHelper.java
index 9139e16..a92f931 100644
--- a/src/main/java/com/android/tools/r8/cf/LoadStoreHelper.java
+++ b/src/main/java/com/android/tools/r8/cf/LoadStoreHelper.java
@@ -213,7 +213,7 @@
       storeBlock = it.split(this.code, this.blockIterator);
       it = storeBlock.listIterator(code);
     }
-    add(store, storeBlock, instruction.getPosition(), it);
+    add(store, instruction.getPosition(), it);
     if (hasCatchHandlers && !instruction.instructionTypeCanThrow()) {
       splitAfterStoredOutValue(it);
     }
@@ -242,7 +242,7 @@
       it = insertBlock.listIterator(code);
     }
     instruction.swapOutValue(newOutValue);
-    add(new Pop(newOutValue), insertBlock, instruction.getPosition(), it);
+    add(new Pop(newOutValue), instruction.getPosition(), it);
   }
 
   private static class PhiMove {
@@ -261,7 +261,7 @@
     List<StackValue> temps = new ArrayList<>(moves.size());
     for (PhiMove move : moves) {
       StackValue tmp = createStackValue(move.phi, topOfStack++);
-      add(load(tmp, move.operand), move.phi.getBlock(), position, it);
+      add(load(tmp, move.operand), position, it);
       temps.add(tmp);
       move.operand.removePhiUser(move.phi);
     }
@@ -269,7 +269,7 @@
       PhiMove move = moves.get(i);
       StackValue tmp = temps.get(i);
       FixedLocalValue out = new FixedLocalValue(move.phi);
-      add(new Store(out, tmp), move.phi.getBlock(), position, it);
+      add(new Store(out, tmp), position, it);
       move.phi.replaceUsers(out);
     }
   }
@@ -296,12 +296,11 @@
 
   private static void add(
       Instruction newInstruction, Instruction existingInstruction, InstructionListIterator it) {
-    add(newInstruction, existingInstruction.getBlock(), existingInstruction.getPosition(), it);
+    add(newInstruction, existingInstruction.getPosition(), it);
   }
 
   private static void add(
-      Instruction newInstruction, BasicBlock block, Position position, InstructionListIterator it) {
-    newInstruction.setBlock(block);
+      Instruction newInstruction, Position position, InstructionListIterator it) {
     newInstruction.setPosition(position);
     it.add(newInstruction);
   }
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfFrame.java b/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
index 62a8f65..0dedc1d 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.cf.code.frame.FrameType;
 import com.android.tools.r8.cf.code.frame.PreciseFrameType;
 import com.android.tools.r8.cf.code.frame.UninitializedFrameType;
+import com.android.tools.r8.cf.code.frame.UninitializedNew;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexClassAndMethod;
@@ -281,7 +282,10 @@
       registry.registerTypeReference(
           frameType.asInitializedNonNullReferenceTypeWithoutInterfaces().getInitializedType());
     } else if (frameType.isUninitializedNew()) {
-      registry.registerTypeReference(frameType.asUninitializedNew().getUninitializedNewType());
+      UninitializedNew uninitializedNew = frameType.asUninitializedNew();
+      if (uninitializedNew.getUninitializedNewType() != null) {
+        registry.registerTypeReference(uninitializedNew.getUninitializedNewType());
+      }
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 3ac2bd6..586b968 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -625,6 +625,8 @@
   public final DexType javaUtilLoggingLoggerType =
       createStaticallyKnownType("Ljava/util/logging/Logger;");
   public final DexType javaUtilSetType = createStaticallyKnownType("Ljava/util/Set;");
+  public final DexType javaUtilEnumMapType = createStaticallyKnownType("Ljava/util/EnumMap;");
+  public final DexType javaUtilEnumSetType = createStaticallyKnownType("Ljava/util/EnumSet;");
 
   public final DexType androidAppActivity = createStaticallyKnownType("Landroid/app/Activity;");
   public final DexType androidAppFragment = createStaticallyKnownType("Landroid/app/Fragment;");
@@ -742,6 +744,8 @@
   public final JavaUtilLocaleMembers javaUtilLocaleMembers = new JavaUtilLocaleMembers();
   public final JavaUtilLoggingLevelMembers javaUtilLoggingLevelMembers =
       new JavaUtilLoggingLevelMembers();
+  public final JavaUtilEnumMapMembers javaUtilEnumMapMembers = new JavaUtilEnumMapMembers();
+  public final JavaUtilEnumSetMembers javaUtilEnumSetMembers = new JavaUtilEnumSetMembers();
 
   public final List<LibraryMembers> libraryMembersCollection =
       ImmutableList.of(
@@ -1547,6 +1551,29 @@
     }
   }
 
+  public class JavaUtilEnumMapMembers {
+    public final DexMethod constructor =
+        createMethod(javaUtilEnumMapType, createProto(voidType, classType), constructorMethodName);
+  }
+
+  public class JavaUtilEnumSetMembers {
+    private final DexString allOfString = createString("allOf");
+    private final DexString noneOfString = createString("noneOf");
+    private final DexString ofString = createString("of");
+    private final DexString rangeString = createString("range");
+
+    public boolean isFactoryMethod(DexMethod invokedMethod) {
+      if (!invokedMethod.getHolderType().equals(javaUtilEnumSetType)) {
+        return false;
+      }
+      DexString name = invokedMethod.getName();
+      return name.isIdenticalTo(allOfString)
+          || name.isIdenticalTo(noneOfString)
+          || name.isIdenticalTo(ofString)
+          || name.isIdenticalTo(rangeString);
+    }
+  }
+
   public class LongMembers extends BoxedPrimitiveMembers {
 
     public final DexField TYPE = createField(boxedLongType, classType, "TYPE");
@@ -2153,7 +2180,6 @@
           && accessFlags.isFinal();
     }
   }
-
   public class NullPointerExceptionMethods {
 
     public final DexMethod init =
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java
index 5e52e33..660504d 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java
@@ -65,7 +65,11 @@
                   && definition.getStaticValue().isDexValueResourceNumber()) {
                 appView
                     .getResourceShrinkerState()
-                    .trace(definition.getStaticValue().asDexValueResourceNumber().getValue());
+                    .trace(
+                        definition.getStaticValue().asDexValueResourceNumber().getValue(),
+                        // TODO(b/378625969): Consider wrapping this in a reachability structure
+                        // to avoid decoding.
+                        field.toString());
               }
             }
           });
@@ -110,7 +114,7 @@
       // these.
       if (integers != null) {
         for (Integer integer : integers) {
-          resourceShrinkerState.trace(integer);
+          resourceShrinkerState.trace(integer, field.toString());
         }
       }
     }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java
index adf8f89..ccffd96 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java
@@ -149,7 +149,7 @@
       NumberGenerator blockNumberGenerator = new NumberGenerator();
       NumberGenerator valueNumberGenerator = new NumberGenerator();
 
-      BasicBlock block = new BasicBlock();
+      BasicBlock block = new BasicBlock(metadata);
       block.setNumber(blockNumberGenerator.next());
 
       // Add "invoke-static <clinit>" for each of the class initializers to the exit block.
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysis.java
index fc21b02..99f161e 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysis.java
@@ -131,9 +131,8 @@
       if (user.getBlock() == block) {
         // When the value of interest has the definition
         if (!root.isPhi()) {
-          List<Instruction> instructions = block.getInstructions();
           // Make sure we're not considering instructions prior to the value of interest.
-          if (instructions.indexOf(user) < instructions.indexOf(root.definition)) {
+          if (user.comesBefore(root.definition)) {
             continue;
           }
         }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/InstanceFieldValueAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/InstanceFieldValueAnalysis.java
index c60e298..ac5a053 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/InstanceFieldValueAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/InstanceFieldValueAnalysis.java
@@ -287,7 +287,7 @@
     // TODO(b/279877113): Extend this analysis to analyze the full remainder of this method.
     if (instancePut.getBlock().getSuccessors().isEmpty()) {
       InstructionListIterator instructionIterator =
-          instancePut.getBlock().listIterator(code, instancePut);
+          instancePut.getBlock().listIterator(code, instancePut.getNext());
       while (instructionIterator.hasNext()) {
         Instruction instruction = instructionIterator.next();
         if (instruction.readSet(appView, context).contains(field)) {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraintAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraintAnalysis.java
index fa31549..70eafed 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraintAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraintAnalysis.java
@@ -20,7 +20,6 @@
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.IfType;
 import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.JumpInstruction;
 import com.android.tools.r8.ir.code.StringSwitch;
@@ -78,21 +77,18 @@
 
     // Run a bounded depth-first traversal to collect the path constraints that lead to early
     // returns.
-    InstructionIterator instructionIterator =
-        code.entryBlock().iterator(code.getNumberOfArguments());
-    return analyzeInstructionsInBlock(code.entryBlock(), 0, 0, instructionIterator);
+    BasicBlock block = code.entryBlock();
+    Instruction firstNonArgument = block.getInstructions().getNth(code.getNumberOfArguments());
+    return analyzeInstructionsInBlock(block, 0, 0, firstNonArgument);
   }
 
   private SimpleInliningConstraintWithDepth analyzeInstructionsInBlock(
       BasicBlock block, int branchDepth, int instructionDepth) {
-    return analyzeInstructionsInBlock(block, branchDepth, instructionDepth, block.iterator());
+    return analyzeInstructionsInBlock(block, branchDepth, instructionDepth, block.entry());
   }
 
   private SimpleInliningConstraintWithDepth analyzeInstructionsInBlock(
-      BasicBlock block,
-      int branchDepth,
-      int instructionDepth,
-      InstructionIterator instructionIterator) {
+      BasicBlock block, int branchDepth, int instructionDepth, Instruction instruction) {
     if (!seen.add(block)
         || block.hasCatchHandlers()
         || block.exit().isThrow()
@@ -102,7 +98,6 @@
 
     // Move the instruction iterator forward to the block's jump instruction, while incrementing the
     // instruction depth of the depth-first traversal.
-    Instruction instruction = instructionIterator.next();
     SimpleInliningConstraint blockConstraint = AlwaysSimpleInliningConstraint.getInstance();
     while (!instruction.isJumpInstruction()) {
       assert !instruction.isArgument();
@@ -116,7 +111,7 @@
       } else {
         blockConstraint = blockConstraint.meet(instructionConstraint);
       }
-      instruction = instructionIterator.next();
+      instruction = instruction.getNext();
     }
 
     // If we have exceeded the threshold, then all paths from this instruction will not lead to any
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
index 3dce430..84bf5e1 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
@@ -154,7 +154,7 @@
         // Already removed.
         continue;
       }
-      IRCodeUtils.removeInstructionAndTransitiveInputsIfNotUsed(code, instruction);
+      IRCodeUtils.removeInstructionAndTransitiveInputsIfNotUsed(instruction);
     }
     code.removeRedundantBlocks();
     assert code.isConsistentSSA(appView);
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
index 1b3837a..4919305 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
@@ -341,7 +341,8 @@
       ProtoMessageInfo protoMessageInfo) {
     // Position iterator immediately before the call to newMessageInfo().
     BasicBlock block = newMessageInfoInvoke.getBlock();
-    InstructionListIterator instructionIterator = block.listIterator(code, newMessageInfoInvoke);
+    InstructionListIterator instructionIterator =
+        block.listIterator(code, newMessageInfoInvoke.getNext());
     Instruction previous = instructionIterator.previous();
     instructionIterator.setInsertionPosition(newMessageInfoInvoke.getPosition());
     assert previous == newMessageInfoInvoke;
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index 59fde05..5e5c563 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -47,7 +47,6 @@
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.Iterator;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
@@ -173,9 +172,8 @@
   // Catch handler information about which successors are catch handlers and what their guards are.
   private CatchHandlers<Integer> catchHandlers = CatchHandlers.EMPTY_INDICES;
 
-  // TODO(b/270398965): Replace LinkedList.
-  @SuppressWarnings("JdkObsolete")
-  private LinkedList<Instruction> instructions = new LinkedList<>();
+  // Linked list of instructions belonging to this block.
+  private final InstructionList instructions = new InstructionList(this);
 
   private int number = -1;
   private List<Phi> phis = new ArrayList<>();
@@ -202,6 +200,23 @@
   // Map of registers to current SSA value. Used during SSA numbering and cleared once filled.
   private Map<Integer, Value> currentDefinitions = new HashMap<>();
 
+  // Metadata shared by all blocks of an IRCode.
+  // TODO(b/376663044): Remove |metadata| parameter from methods in this class.
+  private IRMetadata metadata;
+
+  public BasicBlock(IRMetadata metadata) {
+    this.metadata = metadata;
+  }
+
+  public IRMetadata getMetadata() {
+    return metadata;
+  }
+
+  // Required when moving a block between code objects (inlining).
+  void setMetadata(IRMetadata metadata) {
+    this.metadata = metadata;
+  }
+
   public <BT, CT> TraversalContinuation<BT, CT> traverseNormalPredecessors(
       BiFunction<? super BasicBlock, ? super CT, TraversalContinuation<BT, CT>> fn,
       CT initialValue) {
@@ -524,7 +539,8 @@
       } else if (exit().isIf()) {
         if (indexOfNewBlock >= successors.size() - 2 && indexOfOldBlock >= successors.size() - 2) {
           // New and old are true target and fallthrough, replace last instruction with a goto.
-          Instruction instruction = getInstructions().removeLast();
+          Instruction instruction = instructions.getLast();
+          instructions.removeIgnoreValues(instruction);
           // Iterate in reverse order to ensure that POP instructions are inserted in correct order.
           for (int i = instruction.inValues().size() - 1; i >= 0; i--) {
             Value value = instruction.inValues().get(i);
@@ -536,7 +552,6 @@
               } else {
                 assert !(value instanceof StackValues);
                 Pop pop = new Pop(value);
-                pop.setBlock(this);
                 pop.setPosition(instruction.getPosition());
                 getInstructions().addLast(pop);
               }
@@ -546,7 +561,6 @@
             }
           }
           Instruction exit = new Goto();
-          exit.setBlock(this);
           exit.setPosition(instruction.getPosition());
           getInstructions().addLast(exit);
         } else if (indexOfOldBlock >= successors.size() - 2) {
@@ -738,7 +752,7 @@
     return nextInstructionNumber;
   }
 
-  public LinkedList<Instruction> getInstructions() {
+  public InstructionList getInstructions() {
     return instructions;
   }
 
@@ -797,22 +811,24 @@
   }
 
   public Instruction entry() {
-    return instructions.get(0);
+    return instructions.getFirst();
   }
 
   public JumpInstruction exit() {
     assert filled;
-    assert instructions.get(instructions.size() - 1).isJumpInstruction();
-    return instructions.get(instructions.size() - 1).asJumpInstruction();
+    assert instructions.getLast().isJumpInstruction();
+    return instructions.getLast().asJumpInstruction();
+  }
+
+  public Instruction getLastInstruction() {
+    return instructions.getLast();
   }
 
   public Instruction exceptionalExit() {
     assert hasCatchHandlers();
-    InstructionIterator it = iterator(instructions.size());
-    while (it.hasPrevious()) {
-      Instruction instruction = it.previous();
-      if (instruction.instructionTypeCanThrow()) {
-        return instruction;
+    for (Instruction ins = getLastInstruction(); ins != null; ins = ins.getPrev()) {
+      if (ins.instructionTypeCanThrow()) {
+        return ins;
       }
     }
     return null;
@@ -892,15 +908,9 @@
     phis.removeAll(phisToRemove);
   }
 
-  public void add(Instruction next, IRCode code) {
-    add(next, code.metadata());
-  }
-
-  public void add(Instruction next, IRMetadata metadata) {
+  public void add(Instruction next, IRMetadata unused_metadata) {
     assert !isFilled();
-    instructions.add(next);
-    metadata.record(next);
-    next.setBlock(this);
+    instructions.addLast(next);
   }
 
   public void close(IRBuilder builder) {
@@ -1493,49 +1503,11 @@
     return builder.toString();
   }
 
-  public void addPhiMove(Move move) {
-    // TODO(ager): Consider this more, is it always the case that we should add it before the
-    // exit instruction?
-    Instruction branch = exit();
-    instructions.set(instructions.size() - 1, move);
-    instructions.add(branch);
-  }
-
-  public void setInstructions(LinkedList<Instruction> instructions) {
-    this.instructions = instructions;
-  }
-
-  /**
-   * Remove a number of instructions. The instructions to remove are given as indexes in the
-   * instruction stream.
-   */
-  // TODO(b/270398965): Replace LinkedList.
-  @SuppressWarnings("JdkObsolete")
-  public void removeInstructions(List<Integer> toRemove) {
-    if (!toRemove.isEmpty()) {
-      LinkedList<Instruction> newInstructions = new LinkedList<>();
-      int nextIndex = 0;
-      for (Integer index : toRemove) {
-        assert index >= nextIndex;  // Indexes in toRemove must be sorted ascending.
-        newInstructions.addAll(instructions.subList(nextIndex, index));
-        instructions.get(index).clearBlock();
-        nextIndex = index + 1;
-      }
-      if (nextIndex < instructions.size()) {
-        newInstructions.addAll(instructions.subList(nextIndex, instructions.size()));
-      }
-      assert instructions.size() == newInstructions.size() + toRemove.size();
-      setInstructions(newInstructions);
-    }
-  }
-
   /**
    * Remove an instruction.
    */
   public void removeInstruction(Instruction toRemove) {
-    int index = instructions.indexOf(toRemove);
-    assert index >= 0;
-    removeInstructions(Collections.singletonList(index));
+    instructions.removeIgnoreValues(toRemove);
   }
 
   /**
@@ -1563,7 +1535,7 @@
    */
   public static BasicBlock createGotoBlock(
       int blockNumber, Position position, IRMetadata metadata) {
-    BasicBlock block = new BasicBlock();
+    BasicBlock block = new BasicBlock(metadata);
     block.add(new Goto(), metadata);
     block.close(null);
     block.setNumber(blockNumber);
@@ -1580,7 +1552,7 @@
    * @param theIf the if instruction
    */
   public static BasicBlock createIfBlock(int blockNumber, If theIf, IRMetadata metadata) {
-    BasicBlock block = new BasicBlock();
+    BasicBlock block = new BasicBlock(metadata);
     block.add(theIf, metadata);
     block.close(null);
     block.setNumber(blockNumber);
@@ -1598,7 +1570,7 @@
    */
   public static BasicBlock createIfBlock(
       int blockNumber, If theIf, IRMetadata metadata, Instruction... instructions) {
-    BasicBlock block = new BasicBlock();
+    BasicBlock block = new BasicBlock(metadata);
     for (Instruction instruction : instructions) {
       block.add(instruction, metadata);
     }
@@ -1610,7 +1582,7 @@
 
   public static BasicBlock createSwitchBlock(
       int blockNumber, IntSwitch theSwitch, IRMetadata metadata) {
-    BasicBlock block = new BasicBlock();
+    BasicBlock block = new BasicBlock(metadata);
     block.add(theSwitch, metadata);
     block.close(null);
     block.setNumber(blockNumber);
@@ -1621,14 +1593,14 @@
       IRCode code, Position position, DexType guard, AppView<?> appView) {
     TypeElement guardTypeLattice =
         TypeElement.fromDexType(guard, Nullability.definitelyNotNull(), appView);
-    BasicBlock block = new BasicBlock();
+    BasicBlock block = new BasicBlock(code.metadata());
     MoveException moveException =
         new MoveException(code.createValue(guardTypeLattice), guard, appView.options());
     moveException.setPosition(position);
     Throw throwInstruction = new Throw(moveException.outValue);
     throwInstruction.setPosition(position);
-    block.add(moveException, code);
-    block.add(throwInstruction, code);
+    block.instructions.addLast(moveException);
+    block.instructions.addLast(throwInstruction);
     block.close(null);
     block.setNumber(code.getNextBlockNumber());
     return block;
@@ -1779,9 +1751,9 @@
   // visible to exceptional successors.
   private boolean verifyNoValuesAfterThrowingInstruction() {
     if (hasCatchHandlers()) {
-      InstructionIterator iterator = iterator(instructions.size());
-      while (iterator.hasPrevious()) {
-        Instruction instruction = iterator.previous();
+      for (Instruction instruction = getLastInstruction();
+          instruction != null;
+          instruction = instruction.getPrev()) {
         if (instruction.instructionTypeCanThrow()) {
           return true;
         }
@@ -1791,58 +1763,53 @@
     return true;
   }
 
-  public BasicBlockInstructionIterator iterator() {
-    return new BasicBlockInstructionIterator(this);
+  public InstructionIterator iterator() {
+    return instructions.iterator();
   }
 
-  public BasicBlockInstructionIterator iterator(int index) {
-    return new BasicBlockInstructionIterator(this, index);
+  public InstructionIterator iterator(Instruction instruction) {
+    return instructions.iterator(instruction);
   }
 
-  public BasicBlockInstructionIterator iterator(Instruction instruction) {
-    return new BasicBlockInstructionIterator(this, instruction);
+  public BasicBlockInstructionListIterator listIterator(IRCode unused_code) {
+    return listIterator();
   }
 
-  public BasicBlockInstructionListIterator listIterator(IRCode code) {
-    return listIterator(code.metadata());
+  public BasicBlockInstructionListIterator listIterator() {
+    return new BasicBlockInstructionListIterator(this);
   }
 
-  public BasicBlockInstructionListIterator listIterator(IRMetadata metadata) {
-    return new BasicBlockInstructionListIterator(metadata, this);
+  public BasicBlockInstructionListIterator listIterator(IRCode unused_code, int index) {
+    // TODO(b/376663044): Convert uses of index to use Instruction instead.
+    return new BasicBlockInstructionListIterator(this, index);
   }
 
-  public BasicBlockInstructionListIterator listIterator(IRCode code, int index) {
-    return new BasicBlockInstructionListIterator(code.metadata(), this, index);
-  }
-
-  /**
-   * Creates an instruction list iterator starting at <code>instruction</code>.
-   *
-   * <p>The cursor will be positioned after <code>instruction</code>. Calling <code>next</code> on
-   * the returned iterator will return the instruction after <code>instruction</code>. Calling
-   * <code>previous</code> will return <code>instruction</code>.
-   */
-  public BasicBlockInstructionListIterator listIterator(IRCode code, Instruction instruction) {
-    return new BasicBlockInstructionListIterator(code.metadata(), this, instruction);
+  /** Creates an instruction list iterator starting at <code>firstInstructionToReturn</code>. */
+  public BasicBlockInstructionListIterator listIterator(
+      IRCode unused_code, Instruction firstInstructionToReturn) {
+    return new BasicBlockInstructionListIterator(this, firstInstructionToReturn);
   }
 
   /**
    * Creates a new empty block as a successor for this block.
    *
-   * The new block will have all the normal successors of the original block.
+   * <p>The new block will have all the normal successors of the original block.
    *
-   * The catch successors are either on the original block or the new block depending on the
+   * <p>The catch successors are either on the original block or the new block depending on the
    * value of <code>keepCatchHandlers</code>.
    *
-   * The current block still has all the instructions, and the new block is empty instruction-wise.
+   * <p>The current block still has all the instructions, and the new block is empty
+   * instruction-wise.
    *
    * @param blockNumber block number for new block
    * @param keepCatchHandlers keep catch successors on the original block
+   * @param firstInstructionOfNewBlock this and all following are moved to the new block
    * @return the new block
    */
-  BasicBlock createSplitBlock(int blockNumber, boolean keepCatchHandlers) {
+  BasicBlock createSplitBlock(
+      int blockNumber, boolean keepCatchHandlers, Instruction firstInstructionOfNewBlock) {
     boolean hadCatchHandlers = hasCatchHandlers();
-    BasicBlock newBlock = new BasicBlock();
+    BasicBlock newBlock = new BasicBlock(metadata);
     newBlock.setNumber(blockNumber);
 
     // Copy all successors including catch handlers to the new block, and update predecessors.
@@ -1862,6 +1829,10 @@
     // Link the two blocks
     link(newBlock);
 
+    if (firstInstructionOfNewBlock != null) {
+      newBlock.instructions.severFrom(firstInstructionOfNewBlock);
+    }
+
     // Mark the new block filled and sealed.
     newBlock.filled = true;
     newBlock.sealed = true;
@@ -1936,7 +1907,7 @@
       exceptionTypeLattice = move.getOutType();
       exceptionType = move.getExceptionType();
       assert move.getDebugValues().isEmpty();
-      getInstructions().remove(0);
+      instructions.removeIgnoreValues(move);
     }
     // Create new predecessor blocks.
     List<BasicBlock> newPredecessors = new ArrayList<>(predecessors.size());
@@ -1946,19 +1917,19 @@
         throw new CompilationError(
             "Invalid block structure: catch block reachable via non-exceptional flow.");
       }
-      BasicBlock newBlock = new BasicBlock();
+      BasicBlock newBlock = new BasicBlock(metadata);
       newBlock.setNumber(code.getNextBlockNumber());
       newPredecessors.add(newBlock);
       if (hasMoveException) {
         Value value = code.createValue(exceptionTypeLattice, move.getLocalInfo());
         values.add(value);
         MoveException newMove = new MoveException(value, exceptionType, options);
-        newBlock.add(newMove, code);
+        newBlock.instructions.addLast(newMove);
         newMove.setPosition(position);
       }
       Goto next = new Goto();
       next.setPosition(position);
-      newBlock.add(next, code);
+      newBlock.instructions.addLast(next);
       newBlock.close(null);
       newBlock.getMutableSuccessors().add(this);
       newBlock.getMutablePredecessors().add(predecessor);
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionIterator.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionIterator.java
deleted file mode 100644
index f0771c3..0000000
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionIterator.java
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.ir.code;
-
-import java.util.ListIterator;
-
-public class BasicBlockInstructionIterator implements InstructionIterator {
-
-  private final ListIterator<Instruction> instructionIterator;
-
-  BasicBlockInstructionIterator(BasicBlock block) {
-    this.instructionIterator = block.getInstructions().listIterator();
-  }
-
-  BasicBlockInstructionIterator(BasicBlock block, int index) {
-    this.instructionIterator = block.getInstructions().listIterator(index);
-  }
-
-  BasicBlockInstructionIterator(BasicBlock block, Instruction instruction) {
-    this(block);
-    nextUntil(x -> x == instruction);
-  }
-
-  @Override
-  public boolean hasPrevious() {
-    return instructionIterator.hasPrevious();
-  }
-
-  @Override
-  public Instruction previous() {
-    return instructionIterator.previous();
-  }
-
-  @Override
-  public boolean hasNext() {
-    return instructionIterator.hasNext();
-  }
-
-  @Override
-  public Instruction next() {
-    return instructionIterator.next();
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
index d1fc0ed..af87309 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
@@ -33,35 +33,33 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
+import java.util.NoSuchElementException;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.UnaryOperator;
 
 public class BasicBlockInstructionListIterator implements InstructionListIterator {
 
-  protected final BasicBlock block;
-  protected final ListIterator<Instruction> listIterator;
+  private Instruction next;
   protected Instruction current;
-  private Position position = null;
+  protected final BasicBlock block;
+  private final InstructionList instructionList;
+  private Position position;
 
-  private final IRMetadata metadata;
-
-  BasicBlockInstructionListIterator(IRMetadata metadata, BasicBlock block) {
-    this.block = block;
-    this.listIterator = block.getInstructions().listIterator();
-    this.metadata = metadata;
+  BasicBlockInstructionListIterator(BasicBlock block) {
+    this(block, 0);
   }
 
-  BasicBlockInstructionListIterator(IRMetadata metadata, BasicBlock block, int index) {
-    this.block = block;
-    this.listIterator = block.getInstructions().listIterator(index);
-    this.metadata = metadata;
+  BasicBlockInstructionListIterator(BasicBlock block, int index) {
+    // TODO(b/376663044): Convert uses of index to use Instruction instead.
+    this(block, index == block.size() ? null : block.getInstructions().getNth(index));
   }
 
-  BasicBlockInstructionListIterator(
-      IRMetadata metadata, BasicBlock block, Instruction instruction) {
-    this(metadata, block);
-    nextUntil(x -> x == instruction);
+  BasicBlockInstructionListIterator(BasicBlock block, Instruction firstInstructionToReturn) {
+    this.block = block;
+    this.instructionList = block.getInstructions();
+    this.current = firstInstructionToReturn == null ? null : firstInstructionToReturn.getPrev();
+    this.next = firstInstructionToReturn;
   }
 
   public BasicBlock getBlock() {
@@ -70,54 +68,59 @@
 
   @Override
   public boolean hasNext() {
-    return listIterator.hasNext();
+    return next != null;
   }
 
   @Override
   public Instruction next() {
-    current = listIterator.next();
-    return current;
+    Instruction ret = next;
+    if (ret == null) {
+      throw new NoSuchElementException();
+    }
+    assert ret.block == block : "Iterator invalidated: " + ret;
+    current = ret;
+    next = ret.next;
+    return ret;
   }
 
   @Override
   public int nextIndex() {
-    return listIterator.nextIndex();
+    throw new UnsupportedOperationException();
   }
 
   @Override
   public Instruction peekNext() {
-    // Reset current since listIterator.remove() changes based on whether next() or previous() was
-    // last called.
-    // E.g.: next() -> current=C
-    // peekNext(): next() -> current=D, previous() -> current=D
-    current = null;
-    return IteratorUtils.peekNext(listIterator);
+    assert next == null || next.block == block : "Iterator invalidated: " + next;
+    return next;
   }
 
   @Override
   public boolean hasPrevious() {
-    return listIterator.hasPrevious();
+    return next == null ? !instructionList.isEmpty() : next.prev != null;
   }
 
   @Override
   public Instruction previous() {
-    current = listIterator.previous();
-    return current;
+    Instruction ret = next == null ? instructionList.getLastOrNull() : next.prev;
+    if (ret == null) {
+      throw new NoSuchElementException();
+    }
+    assert ret.block == block : "Iterator invalidated: " + next;
+    current = ret;
+    next = ret;
+    return ret;
   }
 
   @Override
   public int previousIndex() {
-    return listIterator.previousIndex();
+    throw new UnsupportedOperationException();
   }
 
   @Override
   public Instruction peekPrevious() {
-    // Reset current since listIterator.remove() changes based on whether next() or previous() was
-    // last called.
-    // E.g.: previous() -> current=B
-    // peekPrevious(): previous() -> current=A, next() -> current=A
-    current = null;
-    return IteratorUtils.peekPrevious(listIterator);
+    Instruction ret = next == null ? instructionList.getLastOrNull() : next.prev;
+    assert ret == null || ret.block == block : "Iterator invalidated: " + next;
+    return ret;
   }
 
   @Override
@@ -153,13 +156,10 @@
    */
   @Override
   public void add(Instruction instruction) {
-    instruction.setBlock(block);
-    assert instruction.getBlock() == block;
     if (!instruction.hasPosition() && hasInsertionPosition()) {
       instruction.setPosition(getInsertionPosition());
     }
-    listIterator.add(instruction);
-    metadata.record(instruction);
+    instructionList.addBefore(instruction, next);
   }
 
   private boolean hasPriorThrowingInstruction() {
@@ -239,10 +239,14 @@
    */
   @Override
   public void set(Instruction instruction) {
-    instruction.setBlock(block);
-    assert instruction.getBlock() == block;
-    listIterator.set(instruction);
-    metadata.record(instruction);
+    if (current == null) {
+      throw new IllegalStateException();
+    }
+    instructionList.replace(current, instruction);
+    if (current == next) {
+      next = instruction;
+    }
+    current = instruction;
   }
 
   @Override
@@ -253,6 +257,19 @@
     }
   }
 
+  /** Updates |current| and |next|, and returns the old |current|. */
+  private Instruction removeHelper() {
+    Instruction target = current;
+    if (target == null) {
+      throw new IllegalStateException();
+    }
+    if (target == next) {
+      next = target.next;
+    }
+    current = null;
+    return target;
+  }
+
   /**
    * Remove the current instruction (aka the {@link Instruction} returned by the previous call to
    * {@link #next}.
@@ -264,74 +281,31 @@
    */
   @Override
   public void remove() {
-    if (current == null) {
-      throw new IllegalStateException();
-    }
-    assert current.outValue() == null || !current.outValue().isUsed();
-    assert current.getDebugValues().isEmpty();
-    for (int i = 0; i < current.inValues().size(); i++) {
-      Value value = current.inValues().get(i);
-      value.removeUser(current);
-    }
-    // These needs to stay to ensure that an optimization incorrectly not taking debug info into
-    // account still produces valid code when run without enabled assertions.
-    for (Value value : current.getDebugValues()) {
-      value.removeDebugUser(current);
-    }
-    if (current.getLocalInfo() != null) {
-      for (Instruction user : current.outValue().debugUsers()) {
-        user.removeDebugValue(current.outValue());
-      }
-    }
-    listIterator.remove();
-    current = null;
+    instructionList.removeAndDetachInValues(removeHelper());
   }
 
   @Override
   public void removeInstructionIgnoreOutValue() {
-    if (current == null) {
-      throw new IllegalStateException();
-    }
-    listIterator.remove();
-    current = null;
+    instructionList.removeIgnoreValues(removeHelper());
   }
 
   @Override
   public void removeOrReplaceByDebugLocalRead() {
-    if (current == null) {
-      throw new IllegalStateException();
-    }
-    if (current.getDebugValues().isEmpty()) {
-      remove();
-    } else {
-      replaceCurrentInstruction(new DebugLocalRead());
-    }
+    instructionList.removeOrReplaceByDebugLocalRead(removeHelper());
   }
 
   @Override
-  public void replaceCurrentInstruction(Instruction newInstruction, Set<Value> affectedValues) {
+  public void replaceCurrentInstruction(Instruction newInstruction, AffectedValues affectedValues) {
     if (current == null) {
       throw new IllegalStateException();
     }
-    for (Value value : current.inValues()) {
-      value.removeUser(current);
+    if (current == next) {
+      // TODO(b/376663044): This should not advance the cursor. Prior implementation used remove()
+      // and add() rather than set(), which causes replaced item to appear when iterating backwards.
+      // E.g.: Should be: next = newInstruction;
+      next = next.next;
     }
-    if (current.hasUsedOutValue()) {
-      assert newInstruction.outValue() != null;
-      if (affectedValues != null && !newInstruction.getOutType().equals(current.getOutType())) {
-        current.outValue().addAffectedValuesTo(affectedValues);
-      }
-      current.outValue().replaceUsers(newInstruction.outValue());
-    }
-    current.moveDebugValues(newInstruction);
-    newInstruction.setBlock(block);
-    if (!newInstruction.hasPosition()) {
-      newInstruction.setPosition(current.getPosition());
-    }
-    listIterator.remove();
-    listIterator.add(newInstruction);
-    current.clearBlock();
-    metadata.record(newInstruction);
+    instructionList.replace(current, newInstruction, affectedValues);
     current = newInstruction;
   }
 
@@ -508,7 +482,7 @@
 
   @Override
   public void replaceCurrentInstructionWithStaticGet(
-      AppView<?> appView, IRCode code, DexField field, Set<Value> affectedValues) {
+      AppView<?> appView, IRCode code, DexField field, AffectedValues affectedValues) {
     if (current == null) {
       throw new IllegalStateException();
     }
@@ -691,22 +665,17 @@
     // Get the position at which the block is being split.
     Position position = getPreviousPosition();
 
-    // Prepare the new block, placing the exception handlers on the block with the throwing
-    // instruction.
-    BasicBlock newBlock = block.createSplitBlock(code.getNextBlockNumber(), keepCatchHandlers);
-
     // Add a goto instruction.
-    Goto newGoto = new Goto(block);
-    listIterator.add(newGoto);
+    Goto newGoto = new Goto();
+    instructionList.addBefore(newGoto, next);
     newGoto.setPosition(position);
 
-    // Move all remaining instructions to the new block.
-    while (listIterator.hasNext()) {
-      Instruction instruction = listIterator.next();
-      newBlock.getInstructions().addLast(instruction);
-      instruction.setBlock(newBlock);
-      listIterator.remove();
-    }
+    // Prepare the new block, placing the exception handlers on the block with the throwing
+    // instruction.
+    BasicBlock newBlock =
+        block.createSplitBlock(code.getNextBlockNumber(), keepCatchHandlers, next);
+    next = null;
+    current = null;
 
     // Insert the new block in the block list right after the current block.
     if (blocksIterator == null) {
@@ -917,10 +886,8 @@
         entryBlockIterator = entryBlock.listIterator(code);
         // Insert cast instruction into the new block.
         inlineEntry.listIterator(code).add(castInstruction);
-        castInstruction.setBlock(inlineEntry);
         assert castInstruction.getBlock().getInstructions().size() == 2;
       } else {
-        castInstruction.setBlock(entryBlock);
         entryBlockIterator = entryBlock.listIterator(code);
         entryBlockIterator.add(castInstruction);
       }
@@ -1011,9 +978,12 @@
     assert IteratorUtils.peekNext(blocksIterator) == invokeBlock;
 
     // Insert inlinee blocks into the IR code of the callee, before the invoke block.
+    IRMetadata ourMetadata = block.getMetadata();
+    ourMetadata.merge(inlineEntry.getMetadata());
     for (BasicBlock bb : inlinee.blocks) {
       bb.setNumber(code.getNextBlockNumber());
       blocksIterator.add(bb);
+      bb.setMetadata(ourMetadata);
     }
 
     // If the invoke block had catch handlers copy those down to all inlined blocks.
@@ -1057,7 +1027,7 @@
       it.previous();
       return it;
     }
-    BasicBlock newExitBlock = new BasicBlock();
+    BasicBlock newExitBlock = new BasicBlock(code.metadata());
     newExitBlock.setNumber(code.getNextBlockNumber());
     Return newReturn;
     if (normalExits.get(0).exit().asReturn().isReturnVoid()) {
@@ -1090,7 +1060,7 @@
     }
     // The newly constructed return will be eliminated as part of inlining so we set position none.
     newReturn.setPosition(Position.none());
-    newExitBlock.add(newReturn, metadata);
+    newExitBlock.add(newReturn, code.metadata());
     for (BasicBlock exitBlock : normalExits) {
       InstructionListIterator it = exitBlock.listIterator(code, exitBlock.getInstructions().size());
       Instruction oldExit = it.previous();
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlockIterator.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlockIterator.java
index 0b9c54c..0411cd2 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlockIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlockIterator.java
@@ -105,12 +105,10 @@
       throw new IllegalStateException();
     }
     // Remove all instructions from the block before removing the block.
-    InstructionListIterator iterator = current.listIterator(code);
-    while (iterator.hasNext()) {
-      Instruction instruction = iterator.next();
-      instruction.clearDebugValues();
-      iterator.remove();
+    for (Instruction ins = current.entry(); ins != null; ins = ins.getNext()) {
+      ins.detachInValues();
     }
+    current.getInstructions().clear();
     listIterator.remove();
     current = null;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/DominatorTree.java b/src/main/java/com/android/tools/r8/ir/code/DominatorTree.java
index 20eadc9..e04ff68 100644
--- a/src/main/java/com/android/tools/r8/ir/code/DominatorTree.java
+++ b/src/main/java/com/android/tools/r8/ir/code/DominatorTree.java
@@ -27,7 +27,7 @@
 
   private final BasicBlock[] sorted;
   private BasicBlock[] doms;
-  private final BasicBlock normalExitBlock = new BasicBlock();
+  private final BasicBlock normalExitBlock;
 
   private final int unreachableStartIndex;
 
@@ -40,6 +40,7 @@
   public DominatorTree(IRCode code, Assumption assumption) {
     assert assumption != null;
     assert assumption == MAY_HAVE_UNREACHABLE_BLOCKS || code.getUnreachableBlocks().isEmpty();
+    normalExitBlock = new BasicBlock(code.metadata());
 
     ImmutableList<BasicBlock> blocks = code.topologicallySortedBlocks();
     // Add the internal exit block to the block list.
diff --git a/src/main/java/com/android/tools/r8/ir/code/FixedRegisterValue.java b/src/main/java/com/android/tools/r8/ir/code/FixedRegisterValue.java
index c1424b7..8e393fc 100644
--- a/src/main/java/com/android/tools/r8/ir/code/FixedRegisterValue.java
+++ b/src/main/java/com/android/tools/r8/ir/code/FixedRegisterValue.java
@@ -50,6 +50,19 @@
     return register;
   }
 
+  public boolean usesRegister(FixedRegisterValue other) {
+    if (register == other.getRegister()) {
+      return true;
+    }
+    if (getType().isWidePrimitive() && register + 1 == other.getRegister()) {
+      return true;
+    }
+    if (other.getType().isWidePrimitive() && register == other.getRegister() + 1) {
+      return true;
+    }
+    return false;
+  }
+
   @Override
   public boolean isDefinedByInstructionSatisfying(Predicate<Instruction> predicate) {
     return false;
diff --git a/src/main/java/com/android/tools/r8/ir/code/Goto.java b/src/main/java/com/android/tools/r8/ir/code/Goto.java
index 9beaff7..3c5a357 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Goto.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Goto.java
@@ -8,25 +8,10 @@
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.lightir.LirBuilder;
-import com.android.tools.r8.utils.ListUtils;
 import java.util.List;
 import java.util.ListIterator;
 
 public class Goto extends JumpInstruction {
-
-  public Goto() {
-    super();
-  }
-
-  public Goto(BasicBlock block) {
-    this();
-    setBlock(block);
-  }
-
-  public static Builder builder() {
-    return new Builder();
-  }
-
   @Override
   public int opcode() {
     return Opcodes.GOTO;
@@ -77,7 +62,7 @@
     // Avoids BasicBlock.exit(), since it will assert when block is invalid.
     if (myBlock != null
         && !myBlock.getSuccessors().isEmpty()
-        && ListUtils.last(myBlock.getInstructions()) == this) {
+        && myBlock.getInstructions().getLastOrNull() == this) {
       return super.toString() + "block " + getTarget().getNumberAsString();
     }
     return super.toString() + "block <unknown>";
@@ -129,24 +114,4 @@
   public void buildLir(LirBuilder<Value, ?> builder) {
     builder.addGoto(getTarget());
   }
-
-  public static class Builder extends BuilderBase<Builder, Goto> {
-
-    private BasicBlock target;
-
-    public Builder setTarget(BasicBlock target) {
-      this.target = target;
-      return self();
-    }
-
-    @Override
-    public Goto build() {
-      return amend(new Goto(target));
-    }
-
-    @Override
-    public Builder self() {
-      return this;
-    }
-  }
 }
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 307acdf..cbf7075 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
@@ -383,10 +383,10 @@
       if (hasSeenThrowingInstruction) {
         List<BasicBlock> successors = block.getSuccessors();
         if (successors.size() == 1 && ListUtils.first(successors).getPredecessors().size() > 1) {
-          BasicBlock splitBlock = block.createSplitBlock(getNextBlockNumber(), true);
-          Goto newGoto = new Goto(block);
+          BasicBlock splitBlock = block.createSplitBlock(getNextBlockNumber(), true, null);
+          Goto newGoto = new Goto();
           newGoto.setPosition(Position.none());
-          splitBlock.listIterator(this).add(newGoto);
+          splitBlock.getInstructions().addLast(newGoto);
           blockIterator.add(splitBlock);
         }
       }
@@ -1202,10 +1202,10 @@
   }
 
   public Argument getLastArgument() {
-    InstructionIterator instructionIterator = entryBlock().iterator(getNumberOfArguments() - 1);
-    Argument lastArgument = instructionIterator.next().asArgument();
+    Instruction lastArgInstr = entryBlock().getInstructions().getNth(getNumberOfArguments() - 1);
+    Argument lastArgument = lastArgInstr.asArgument();
     assert lastArgument != null;
-    assert !instructionIterator.peekNext().isArgument();
+    assert !lastArgInstr.getNext().isArgument();
     return lastArgument;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
index 13e393a..6eb4a80 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
@@ -104,7 +104,7 @@
 
   @Override
   public void replaceCurrentInstructionWithStaticGet(
-      AppView<?> appView, IRCode code, DexField field, Set<Value> affectedValues) {
+      AppView<?> appView, IRCode code, DexField field, AffectedValues affectedValues) {
     instructionIterator.replaceCurrentInstructionWithStaticGet(
         appView, code, field, affectedValues);
   }
@@ -278,7 +278,7 @@
   }
 
   @Override
-  public void replaceCurrentInstruction(Instruction newInstruction, Set<Value> affectedValues) {
+  public void replaceCurrentInstruction(Instruction newInstruction, AffectedValues affectedValues) {
     instructionIterator.replaceCurrentInstruction(newInstruction, affectedValues);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCodeUtils.java b/src/main/java/com/android/tools/r8/ir/code/IRCodeUtils.java
index e918ad4..4bb02dd 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCodeUtils.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCodeUtils.java
@@ -100,7 +100,7 @@
     } else {
       assert false;
     }
-    internalRemoveInstructionAndTransitiveInputsIfNotUsed(code, worklist);
+    internalRemoveInstructionAndTransitiveInputsIfNotUsed(worklist);
   }
 
   /**
@@ -109,14 +109,12 @@
    *
    * <p>Use with caution!
    */
-  public static void removeInstructionAndTransitiveInputsIfNotUsed(
-      IRCode code, Instruction instruction) {
-    internalRemoveInstructionAndTransitiveInputsIfNotUsed(
-        code, DequeUtils.newArrayDeque(instruction));
+  public static void removeInstructionAndTransitiveInputsIfNotUsed(Instruction instruction) {
+    internalRemoveInstructionAndTransitiveInputsIfNotUsed(DequeUtils.newArrayDeque(instruction));
   }
 
   private static void internalRemoveInstructionAndTransitiveInputsIfNotUsed(
-      IRCode code, Deque<InstructionOrPhi> worklist) {
+      Deque<InstructionOrPhi> worklist) {
     Set<InstructionOrPhi> removed = Sets.newIdentityHashSet();
     while (!worklist.isEmpty()) {
       InstructionOrPhi instructionOrPhi = worklist.removeFirst();
@@ -145,7 +143,7 @@
       } else {
         Instruction current = instructionOrPhi.asInstruction();
         if (!current.hasOutValue() || !current.outValue().hasAnyUsers()) {
-          current.getBlock().listIterator(code, current).removeOrReplaceByDebugLocalRead();
+          current.removeOrReplaceByDebugLocalRead();
           for (Value inValue : current.inValues()) {
             worklist.add(inValue.isPhi() ? inValue.asPhi() : inValue.definition);
           }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index d55ada3..7a67608 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -28,6 +28,7 @@
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.DeadCodeRemover.DeadInstructionResult;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
@@ -48,13 +49,15 @@
 
 public abstract class Instruction
     implements AbstractInstruction, InstructionOrPhi, MaterializingInstructionsInfo {
-
-  protected Value outValue = null;
+  // Package-private to be used by InstructionList.
+  BasicBlock block;
+  Instruction prev;
+  Instruction next;
+  protected Value outValue;
   protected final List<Value> inValues = new ArrayList<>();
-  private BasicBlock block = null;
   private int number = -1;
-  private Set<Value> debugValues = null;
-  private Position position = null;
+  private Set<Value> debugValues;
+  private Position position;
 
   protected Instruction(Value outValue) {
     setOutValue(outValue);
@@ -82,6 +85,18 @@
     return position != null;
   }
 
+  public boolean hasPrev() {
+    return prev != null;
+  }
+
+  public Instruction getPrev() {
+    return prev;
+  }
+
+  public Instruction getNext() {
+    return next;
+  }
+
   @Override
   public final Position getPosition() {
     assert position != null;
@@ -341,32 +356,56 @@
     return block;
   }
 
-  /**
-   * Set the basic block of this instruction. See IRBuilder.
-   */
-  public void setBlock(BasicBlock block) {
-    assert block != null;
-    this.block = block;
+  /** Prepares instruction for removal by removing its in-values. */
+  public Instruction detachInValues() {
+    for (Value value : inValues) {
+      value.removeUser(this);
+    }
+    // These needs to stay to ensure that an optimization incorrectly not taking debug info into
+    // account still produces valid code when run without enabled assertions.
+    if (debugValues != null) {
+      for (Value value : debugValues) {
+        value.removeDebugUser(this);
+      }
+    }
+    if (getLocalInfo() != null) {
+      for (Instruction user : outValue.debugUsers()) {
+        user.removeDebugValue(outValue);
+      }
+    }
+    return this;
   }
 
-  /**
-   * Clear the basic block of this instruction. Use when removing an instruction from a block.
-   */
-  public void clearBlock() {
-    assert block != null;
-    block = null;
+  public void removeOrReplaceByDebugLocalRead() {
+    block.getInstructions().removeOrReplaceByDebugLocalRead(this);
   }
 
-  public void removeOrReplaceByDebugLocalRead(IRCode code) {
-    getBlock().listIterator(code, this).removeOrReplaceByDebugLocalRead();
+  // TODO(b/376663044): Delete.
+  public void removeOrReplaceByDebugLocalRead(IRCode unused_code) {
+    block.getInstructions().removeOrReplaceByDebugLocalRead(this);
   }
 
-  public void replace(Instruction newInstruction, IRCode code) {
-    replace(newInstruction, code, null);
+  public void removeIgnoreValues() {
+    block.getInstructions().removeIgnoreValues(this);
   }
 
-  public void replace(Instruction newInstruction, IRCode code, Set<Value> affectedValues) {
-    getBlock().listIterator(code, this).replaceCurrentInstruction(newInstruction, affectedValues);
+  public void replace(Instruction newInstruction) {
+    block.getInstructions().replace(this, newInstruction);
+  }
+
+  // TODO(b/376663044): Delete.
+  public void replace(Instruction newInstruction, IRCode unused_code) {
+    block.getInstructions().replace(this, newInstruction);
+  }
+
+  public void replace(Instruction newInstruction, AffectedValues affectedValues) {
+    block.getInstructions().replace(this, newInstruction, affectedValues);
+  }
+
+  // TODO(b/376663044): Delete.
+  public void replace(
+      Instruction newInstruction, IRCode unused_code, AffectedValues affectedValues) {
+    block.getInstructions().replace(this, newInstruction, affectedValues);
   }
 
   /**
@@ -960,6 +999,10 @@
     return null;
   }
 
+  public boolean isExit() {
+    return isJumpInstruction();
+  }
+
   public boolean isJumpInstruction() {
     return false;
   }
@@ -1630,6 +1673,20 @@
     // Intentionally empty.
   }
 
+  /** Returns whether the given instruction is encountered via continuous calls to getNext(). */
+  public boolean comesBefore(Instruction target) {
+    assert target != this;
+    assert target.block == block; // Probably a bug if this does not hold.
+    Instruction cur = next;
+    while (cur != null) {
+      if (cur == target) {
+        return true;
+      }
+      cur = cur.next;
+    }
+    return false;
+  }
+
   public static class SideEffectAssumption {
 
     public static final SideEffectAssumption NONE = new SideEffectAssumption();
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionList.java b/src/main/java/com/android/tools/r8/ir/code/InstructionList.java
new file mode 100644
index 0000000..f95b67a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionList.java
@@ -0,0 +1,302 @@
+// Copyright (c) 2024, 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.code;
+
+import com.android.tools.r8.ir.optimize.AffectedValues;
+import java.util.Spliterator;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+/**
+ * Linked list of instructions using Instruction.prev and Instruction.next fields.
+ *
+ * <pre>
+ * Design notes:
+ * - There is a 1:1 relationship between a BasicBlock and InstructionList.
+ *   - We might want to just merge this into BasicBlock.
+ * - Updates IRMetadata and sets Instruction.block fields when instructions are added.
+ * - Does not implement Collection / List interfaces to better reflect that this is a
+ *   special-purpose collection, where operations sometimes mutate the state of elements.
+ * - Does not implement a non-assert "contains" method. Use Instruction.block to check containment.
+ * - Does not clear next/prev fields upon removal.
+ *   - Because there has not yet been a need to reuse instructions (except for the special case of
+ *     block splitting).
+ *   - |block| is cleared, and asserts prevent instructions from being added that already have a
+ *     block set.
+ * </pre>
+ */
+public class InstructionList implements Iterable<Instruction> {
+  private final BasicBlock ownerBlock;
+  private Instruction head;
+  private Instruction tail;
+  private int size;
+
+  public InstructionList(BasicBlock ownerBlock) {
+    this.ownerBlock = ownerBlock;
+  }
+
+  public int size() {
+    return size;
+  }
+
+  public boolean isEmpty() {
+    return head == null;
+  }
+
+  public Instruction getFirst() {
+    assert !isEmpty();
+    return head;
+  }
+
+  public Instruction getFirstOrNull() {
+    return head;
+  }
+
+  /** Returns the instruction at the given index. This is O(n). */
+  public Instruction getNth(int n) {
+    assert n >= 0 && n < size : "n=" + n + " size=" + size;
+    if (n > size / 2) {
+      Instruction ret = tail;
+      n = size - n - 1;
+      for (int i = 0; i < n; ++i) {
+        ret = ret.prev;
+      }
+      return ret;
+    }
+    Instruction ret = head;
+    for (int i = 0; i < n; ++i) {
+      ret = ret.next;
+    }
+    return ret;
+  }
+
+  public Instruction getLast() {
+    assert !isEmpty();
+    return tail;
+  }
+
+  public Instruction getLastOrNull() {
+    return tail;
+  }
+
+  public void addFirst(Instruction newInstruction) {
+    addBefore(newInstruction, head);
+  }
+
+  public void addLast(Instruction newInstruction) {
+    addBefore(newInstruction, null);
+  }
+
+  /**
+   * Inserts newInstruction before existingInstruction. If existingInstruction is null, adds at the
+   * end.
+   */
+  public void addBefore(Instruction newInstruction, Instruction existingInstruction) {
+    assert newInstruction.block == null;
+    if (existingInstruction != null) {
+      assert linearScanFinds(existingInstruction);
+    }
+    if (size == 0) {
+      assert existingInstruction == null;
+      head = newInstruction;
+      tail = newInstruction;
+    } else if (existingInstruction == null) {
+      // Add to end.
+      newInstruction.prev = tail;
+      tail.next = newInstruction;
+      tail = newInstruction;
+    } else {
+      Instruction prevInstruction = existingInstruction.prev;
+      newInstruction.prev = prevInstruction;
+      newInstruction.next = existingInstruction;
+      existingInstruction.prev = newInstruction;
+      if (prevInstruction == null) {
+        assert head == existingInstruction;
+        head = newInstruction;
+      } else {
+        prevInstruction.next = newInstruction;
+      }
+    }
+
+    size += 1;
+    newInstruction.block = ownerBlock;
+    ownerBlock.getMetadata().record(newInstruction);
+  }
+
+  /** Adopt all instructions from another list (use this to split blocks). */
+  public void severFrom(Instruction firstInstructionToMove) {
+    assert isEmpty();
+    BasicBlock sourceBlock = firstInstructionToMove.block;
+    InstructionList sourceList = sourceBlock.getInstructions();
+    // So far no need to sever between methods.
+    assert sourceBlock.getMetadata() == ownerBlock.getMetadata();
+
+    head = firstInstructionToMove;
+    tail = sourceList.tail;
+
+    Instruction lastInstructionToRemain = firstInstructionToMove.prev;
+    sourceList.tail = lastInstructionToRemain;
+    if (lastInstructionToRemain == null) {
+      sourceList.head = null;
+    } else {
+      lastInstructionToRemain.next = null;
+      firstInstructionToMove.prev = null;
+    }
+
+    int count = 0;
+    BasicBlock newBlock = ownerBlock;
+    for (Instruction current = firstInstructionToMove; current != null; current = current.next) {
+      count += 1;
+      current.block = newBlock;
+    }
+    size = count;
+    sourceList.size -= count;
+  }
+
+  public void replace(Instruction oldInstruction, Instruction newInstruction) {
+    replace(oldInstruction, newInstruction, null);
+  }
+
+  /**
+   * Replace the oldInstruction with newInstruction.
+   *
+   * <p>Removes in-values from oldInstruction.
+   *
+   * <p>If the current instruction produces an out-value the new instruction must also produce an
+   * out-value, and all uses of the current instructions out-value will be replaced by the new
+   * instructions out-value.
+   *
+   * <p>The debug information of the current instruction will be attached to the new instruction.
+   */
+  public void replace(
+      Instruction target, Instruction newInstruction, AffectedValues affectedValues) {
+    for (Value value : target.inValues()) {
+      value.removeUser(target);
+    }
+    if (target.hasUsedOutValue()) {
+      assert newInstruction.outValue() != null;
+      if (affectedValues != null && !newInstruction.getOutType().equals(target.getOutType())) {
+        target.outValue().addAffectedValuesTo(affectedValues);
+      }
+      target.outValue().replaceUsers(newInstruction.outValue());
+    }
+    target.moveDebugValues(newInstruction);
+    if (!newInstruction.hasPosition()) {
+      newInstruction.setPosition(target.getPosition());
+    }
+
+    addBefore(newInstruction, target);
+    removeIgnoreValues(target);
+  }
+
+  /**
+   * Safe removal function that will insert a DebugLocalRead to take over the debug values if any
+   * are associated with the current instruction.
+   */
+  public void removeOrReplaceByDebugLocalRead(Instruction target) {
+    if (target.getDebugValues().isEmpty()) {
+      removeAndDetachInValues(target);
+    } else {
+      replace(target, new DebugLocalRead());
+    }
+  }
+
+  /**
+   * Remove the given instruction.
+   *
+   * <p>The instruction will be removed and uses of its in-values removed.
+   *
+   * <p>If the current instruction produces an out-value this out value must not have any users.
+   */
+  public void removeAndDetachInValues(Instruction target) {
+    assert target.outValue() == null || !target.outValue().isUsed();
+    removeIgnoreValues(target.detachInValues());
+  }
+
+  /** Removes without doing any validation of in / out values. */
+  public void removeIgnoreValues(Instruction target) {
+    assert linearScanFinds(target);
+    target.block = null;
+    Instruction prev = target.prev;
+    Instruction next = target.next;
+    if (head == target) {
+      head = next;
+    }
+    if (tail == target) {
+      tail = prev;
+    }
+    if (prev != null) {
+      prev.next = next;
+    }
+    if (next != null) {
+      next.prev = prev;
+    }
+    size -= 1;
+  }
+
+  /** Removes all instructions. Instructions removed in this way may not be reused. */
+  public void clear() {
+    head = null;
+    tail = null;
+    size = 0;
+  }
+
+  // Non-assert uses should check instruction.block for containment. */
+  private boolean linearScanFinds(Instruction instruction) {
+    for (Instruction cur = head; cur != null; cur = cur.next) {
+      if (cur == instruction) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public Stream<Instruction> stream() {
+    return StreamSupport.stream(new SpliteratorImpl(), false);
+  }
+
+  @Override
+  public BasicBlockInstructionListIterator iterator() {
+    return new BasicBlockInstructionListIterator(ownerBlock);
+  }
+
+  public BasicBlockInstructionListIterator iterator(Instruction firstInstructionToReturn) {
+    return new BasicBlockInstructionListIterator(ownerBlock, firstInstructionToReturn);
+  }
+
+  private class SpliteratorImpl implements Spliterator<Instruction> {
+    Instruction next = head;
+
+    @Override
+    public boolean tryAdvance(Consumer<? super Instruction> action) {
+      if (next == null) {
+        return false;
+      }
+      action.accept(next);
+      next = next.next;
+      return true;
+    }
+
+    @Override
+    public Spliterator<Instruction> trySplit() {
+      return null;
+    }
+
+    @Override
+    public long estimateSize() {
+      return size;
+    }
+
+    @Override
+    public long getExactSizeIfKnown() {
+      return size;
+    }
+
+    @Override
+    public int characteristics() {
+      return SIZED | DISTINCT | NONNULL;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
index c1e8aa6..b9c54f1 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
@@ -98,7 +98,7 @@
     next();
   }
 
-  /** See {@link #replaceCurrentInstruction(Instruction, Set)}. */
+  /** See {@link #replaceCurrentInstruction(Instruction, AffectedValues)}. */
   default void replaceCurrentInstruction(Instruction newInstruction) {
     replaceCurrentInstruction(newInstruction, null);
   }
@@ -119,7 +119,7 @@
    * @param newInstruction the instruction to insert instead of the current.
    * @param affectedValues if non-null, all users of the out value will be added to this set.
    */
-  void replaceCurrentInstruction(Instruction newInstruction, Set<Value> affectedValues);
+  void replaceCurrentInstruction(Instruction newInstruction, AffectedValues affectedValues);
 
   // Do not show a deprecation warning for InstructionListIterator.remove().
   @SuppressWarnings("deprecation")
@@ -229,7 +229,7 @@
   void replaceCurrentInstructionWithNullCheck(AppView<?> appView, Value object);
 
   void replaceCurrentInstructionWithStaticGet(
-      AppView<?> appView, IRCode code, DexField field, Set<Value> affectedValues);
+      AppView<?> appView, IRCode code, DexField field, AffectedValues affectedValues);
 
   void replaceCurrentInstructionWithThrow(
       AppView<?> appView,
diff --git a/src/main/java/com/android/tools/r8/ir/code/Invoke.java b/src/main/java/com/android/tools/r8/ir/code/Invoke.java
index 4406d9d..d16fdc6 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Invoke.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Invoke.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.code;
 
+import static com.google.common.base.Predicates.alwaysTrue;
+
 import com.android.tools.r8.cf.LoadStoreHelper;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.dex.code.DexInstruction;
@@ -19,7 +21,10 @@
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.conversion.DexBuilder;
+import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
 import com.android.tools.r8.utils.BooleanUtils;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
 import java.util.List;
 import java.util.Set;
 
@@ -157,9 +162,10 @@
   }
 
   protected int getRegisterForInvokeRange(DexBuilder builder, Value argument) {
-    return builder.getOptions().getTestingOptions().enableLiveIntervalsSplittingForInvokeRange
-        ? builder.allocatedRegister(argument, getNumber())
-        : builder.argumentOrAllocateRegister(argument, getNumber());
+    if (argumentsAreConsecutivePinnedInputArguments(builder.getRegisterAllocator())) {
+      return builder.getArgumentRegister(argument);
+    }
+    return builder.allocatedRegister(argument, getNumber());
   }
 
   protected void addInvokeAndMoveResult(DexInstruction instruction, DexBuilder builder) {
@@ -223,6 +229,15 @@
   }
 
   private boolean argumentsAreConsecutiveInputArguments() {
+    return argumentsAreConsecutiveInputArgumentsThatMatches(alwaysTrue());
+  }
+
+  private boolean argumentsAreConsecutivePinnedInputArguments(
+      LinearScanRegisterAllocator registerAllocator) {
+    return argumentsAreConsecutiveInputArgumentsThatMatches(registerAllocator::isPinnedArgument);
+  }
+
+  private boolean argumentsAreConsecutiveInputArgumentsThatMatches(Predicate<Value> predicate) {
     if (arguments().isEmpty()) {
       return false;
     }
@@ -237,28 +252,24 @@
       }
       current = next;
     }
-    return true;
+    return Iterables.all(arguments(), predicate);
   }
 
+  // Used to decide if this invoke should be emitted as invoke/range.
   protected boolean needsRangedInvoke(DexBuilder builder) {
+    if (arguments().size() == 1) {
+      // Prefer invoke-range since this does not impose any constraints on the operand register.
+      return true;
+    }
     if (requiredArgumentRegisters() > 5) {
       // No way around using an invoke-range instruction.
       return true;
     }
-    // By using an invoke-range instruction when there is only one argument, we avoid having to
-    // satisfy the constraint that the argument register(s) must fit in 4 bits.
-    boolean registersGuaranteedToBeConsecutive =
-        arguments().size() == 1 || argumentsAreConsecutiveInputArguments();
-    if (!registersGuaranteedToBeConsecutive) {
-      // No way that we will need an invoke-range.
-      return false;
+    if (argumentsAreConsecutivePinnedInputArguments(builder.getRegisterAllocator())) {
+      // Use the arguments from their input registers.
+      return true;
     }
-    // If we could use an invoke-range instruction, but all the registers fit in 4 bits, then we
-    // use a non-range invoke.
-    assert verifyInvokeRangeArgumentsAreConsecutive(builder);
-    int registerStart = getRegisterForInvokeRange(builder, getFirstArgument());
-    int registerEnd = registerStart + requiredArgumentRegisters() - 1;
-    return registerEnd > Constants.U4BIT_MAX;
+    return false;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
index 0885e72..99b0a1b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
@@ -64,7 +64,7 @@
   }
 
   @Override
-  public void replaceCurrentInstruction(Instruction newInstruction, Set<Value> affectedValues) {
+  public void replaceCurrentInstruction(Instruction newInstruction, AffectedValues affectedValues) {
     currentBlockIterator.replaceCurrentInstruction(newInstruction, affectedValues);
   }
 
@@ -133,7 +133,7 @@
 
   @Override
   public void replaceCurrentInstructionWithStaticGet(
-      AppView<?> appView, IRCode code, DexField field, Set<Value> affectedValues) {
+      AppView<?> appView, IRCode code, DexField field, AffectedValues affectedValues) {
     currentBlockIterator.replaceCurrentInstructionWithStaticGet(
         appView, code, field, affectedValues);
   }
@@ -322,7 +322,7 @@
     if (target == null || target.size() < 2) {
       return null;
     }
-    return target.getInstructions().get(target.size() - 2);
+    return target.getLastInstruction().getPrev();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/Monitor.java b/src/main/java/com/android/tools/r8/ir/code/Monitor.java
index 025df81..430f8a4 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Monitor.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Monitor.java
@@ -42,14 +42,6 @@
     return inValues.get(0);
   }
 
-  public boolean isEnter() {
-    return type == MonitorType.ENTER;
-  }
-
-  public boolean isExit() {
-    return type == MonitorType.EXIT;
-  }
-
   @Override
   public void buildDex(DexBuilder builder) {
     // If the monitor object is an argument, we use the argument register for all the monitor
@@ -96,7 +88,7 @@
 
   @Override
   public boolean isMonitorEnter() {
-    return isEnter();
+    return type == MonitorType.ENTER;
   }
 
   @Override
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 2d4b2ee..480be85 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
@@ -41,6 +41,7 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Inc;
 import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionList;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.JumpInstruction;
@@ -69,7 +70,6 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
@@ -79,7 +79,6 @@
 
   private static final int PEEPHOLE_OPTIMIZATION_PASSES = 2;
   private static final int SUFFIX_SHARING_OVERHEAD = 30;
-  private static final int IINC_PATTERN_SIZE = 4;
 
   public final AppView<?> appView;
   private final ProgramMethod method;
@@ -234,15 +233,13 @@
     Set<UninitializedThisLocalRead> uninitializedThisLocalReads = Sets.newIdentityHashSet();
     for (BasicBlock exitBlock : code.blocks) {
       if (exitBlock.exit().isThrow() && !exitBlock.hasCatchHandlers()) {
-        LinkedList<Instruction> instructions = exitBlock.getInstructions();
-        Instruction throwing = instructions.removeLast();
+        InstructionList instructions = exitBlock.getInstructions();
+        Instruction throwing = instructions.getLast();
         assert throwing.isThrow();
         UninitializedThisLocalRead read = new UninitializedThisLocalRead(code.getThis());
         read.setPosition(throwing.getPosition());
         uninitializedThisLocalReads.add(read);
-        read.setBlock(exitBlock);
-        instructions.addLast(read);
-        instructions.addLast(throwing);
+        instructions.addBefore(read, throwing);
       }
     }
     return uninitializedThisLocalReads;
@@ -322,7 +319,6 @@
         it.previous();
         Value constValue = code.createValue(inValue.getType());
         Instruction newInstruction = new ConstNumber(constValue, -1);
-        newInstruction.setBlock(block);
         newInstruction.setPosition(current.getPosition());
         it.add(newInstruction);
         it.next();
@@ -473,44 +469,43 @@
   @SuppressWarnings("ReferenceEquality")
   private void rewriteIincPatterns() {
     for (BasicBlock block : code.blocks) {
-      InstructionListIterator it = block.listIterator(code);
-      // Test that we have enough instructions for iinc.
-      while (IINC_PATTERN_SIZE <= block.getInstructions().size() - it.nextIndex()) {
-        Instruction loadOrConst1 = it.next();
-        if (!loadOrConst1.isLoad() && !loadOrConst1.isConstNumber()) {
+      InstructionList instructions = block.getInstructions();
+      for (Instruction ins = instructions.getFirst(); ins != null; ins = ins.getNext()) {
+        boolean isLoad = ins.isLoad();
+        if (!isLoad && !ins.isConstNumber()) {
           continue;
         }
         Load load;
         ConstNumber constNumber;
-        if (loadOrConst1.isLoad()) {
-          load = loadOrConst1.asLoad();
-          constNumber = it.next().asConstNumber();
+        Instruction nextInstruction = ins.getNext();
+        if (isLoad) {
+          load = ins.asLoad();
+          constNumber = nextInstruction.asConstNumber();
         } else {
-          load = it.next().asLoad();
-          constNumber = loadOrConst1.asConstNumber();
+          load = nextInstruction.asLoad();
+          constNumber = ins.asConstNumber();
         }
-        Instruction add = it.next().asAdd();
-        Instruction store = it.next().asStore();
-        // Reset pointer to load.
-        it.previous();
-        it.previous();
-        it.previous();
-        it.previous();
         if (load == null
             || constNumber == null
-            || add == null
-            || store == null
             || constNumber.getOutType() != TypeElement.getInt()) {
-          it.next();
+          continue;
+        }
+        Instruction add = nextInstruction.getNext();
+        if (add == null) {
+          break;
+        }
+        Instruction store = add.getNext();
+        if (store == null) {
+          break;
+        }
+        if (!add.isAdd() || !store.isStore()) {
           continue;
         }
         int increment = constNumber.getIntValue();
         if (increment < Byte.MIN_VALUE || Byte.MAX_VALUE < increment) {
-          it.next();
           continue;
         }
         if (getLocalRegister(load.src()) != getLocalRegister(store.outValue())) {
-          it.next();
           continue;
         }
         Position position = add.getPosition();
@@ -519,16 +514,15 @@
             || position != store.getPosition()) {
           continue;
         }
-        it.removeInstructionIgnoreOutValue();
-        it.next();
-        it.removeInstructionIgnoreOutValue();
-        it.next();
-        it.removeInstructionIgnoreOutValue();
-        it.next();
+
         Inc inc = new Inc(store.outValue(), load.inValues().get(0), increment);
         inc.setPosition(position);
-        inc.setBlock(block);
-        it.set(inc);
+        instructions.addBefore(inc, store);
+
+        load.removeIgnoreValues();
+        constNumber.removeIgnoreValues();
+        add.removeIgnoreValues();
+        store.removeIgnoreValues();
       }
     }
   }
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 bdeeb96..6f835bd 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
@@ -58,6 +58,7 @@
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.InstructionList;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.IntSwitch;
 import com.android.tools.r8.ir.code.JumpInstruction;
@@ -67,13 +68,13 @@
 import com.android.tools.r8.ir.code.Return;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.passes.TrivialGotosCollapser;
+import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.android.tools.r8.lightir.ByteUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOutputMode;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
@@ -99,7 +100,7 @@
   private final BytecodeMetadata.Builder<DexInstruction> bytecodeMetadataBuilder;
 
   // The register allocator providing register assignments for the code to build.
-  private final RegisterAllocator registerAllocator;
+  private final LinearScanRegisterAllocator registerAllocator;
 
   private final InternalOptions options;
   private final MethodConversionOptions conversionOptions;
@@ -138,7 +139,7 @@
   public DexBuilder(
       IRCode ir,
       BytecodeMetadataProvider bytecodeMetadataProvider,
-      RegisterAllocator registerAllocator,
+      LinearScanRegisterAllocator registerAllocator,
       InternalOptions options) {
     this(
         ir,
@@ -158,7 +159,7 @@
     this.appView = registerAllocator.getAppView();
     this.ir = ir;
     this.bytecodeMetadataBuilder = BytecodeMetadata.builder(bytecodeMetadataProvider);
-    this.registerAllocator = registerAllocator;
+    this.registerAllocator = (LinearScanRegisterAllocator) registerAllocator;
     this.options = options;
     this.conversionOptions = conversionOptions;
     if (isBuildingForComparison()) {
@@ -664,6 +665,10 @@
     return registerAllocator.getArgumentOrAllocateRegisterForValue(value, instructionNumber);
   }
 
+  public int getArgumentRegister(Value value) {
+    return registerAllocator.getArgumentRegisterForValue(value);
+  }
+
   public void addGoto(com.android.tools.r8.ir.code.Goto jump) {
     if (jump.getTarget() != nextBlock) {
       add(jump, new GotoInfo(jump));
@@ -942,8 +947,7 @@
       item = tryItems.get(i);
       coalescedTryItems.add(item);
       // Trim the range start for non-throwing instructions when starting a new range.
-      List<com.android.tools.r8.ir.code.Instruction> instructions = blocksWithHandlers.get(i)
-          .getInstructions();
+      InstructionList instructions = blocksWithHandlers.get(i).getInstructions();
       for (com.android.tools.r8.ir.code.Instruction insn : instructions) {
         if (insn.instructionTypeCanThrow()) {
           item.start = getInfo(insn).getOffset();
@@ -1028,10 +1032,9 @@
 
   private int trimEnd(BasicBlock block) {
     // Trim the range end for non-throwing instructions when end has been computed.
-    List<com.android.tools.r8.ir.code.Instruction> instructions = block.getInstructions();
-    for (com.android.tools.r8.ir.code.Instruction insn : Lists.reverse(instructions)) {
-      if (insn.instructionTypeCanThrow()) {
-        Info info = getInfo(insn);
+    for (Instruction ins = block.getLastInstruction(); ins != null; ins = ins.getPrev()) {
+      if (ins.instructionTypeCanThrow()) {
+        Info info = getInfo(ins);
         return info.getOffset() + info.getSize();
       }
     }
@@ -1078,7 +1081,7 @@
     return options;
   }
 
-  public RegisterAllocator getRegisterAllocator() {
+  public LinearScanRegisterAllocator getRegisterAllocator() {
     return registerAllocator;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index 2c84747..8e0b85a 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -282,9 +282,8 @@
     }
   }
 
-  public static class BlockInfo {
-
-    BasicBlock block = new BasicBlock();
+  public class BlockInfo {
+    BasicBlock block = new BasicBlock(metadata);
     IntSet normalPredecessors = new IntArraySet();
     IntSet normalSuccessors = new IntArraySet();
     IntSet exceptionalPredecessors = new IntArraySet();
@@ -697,14 +696,13 @@
     // Insert definitions for all uninitialized local values.
     if (uninitializedDebugLocalValues != null) {
       Position position = entryBlock.getPosition();
-      InstructionListIterator it = entryBlock.listIterator(metadata);
+      InstructionListIterator it = entryBlock.listIterator();
       it.nextUntil(i -> !i.isArgument());
       it.previous();
       for (List<Value> values : uninitializedDebugLocalValues.values()) {
         for (Value value : values) {
           if (value.isUsed()) {
             Instruction def = new DebugLocalUninitialized(value);
-            def.setBlock(entryBlock);
             def.setPosition(position);
             it.add(def);
           }
@@ -797,7 +795,7 @@
       return;
     }
     for (BasicBlock block : blocks) {
-      InstructionListIterator it = block.listIterator(metadata);
+      InstructionListIterator it = block.listIterator();
       Position current = null;
       while (it.hasNext()) {
         Instruction instruction = it.next();
@@ -1760,8 +1758,7 @@
   }
 
   public void addMoveResult(int dest) {
-    List<Instruction> instructions = currentBlock.getInstructions();
-    Invoke invoke = instructions.get(instructions.size() - 1).asInvoke();
+    Invoke invoke = currentBlock.getInstructions().getLast().asInvoke();
     assert invoke.outValue() == null;
     assert invoke.instructionTypeCanThrow();
     DexType outType = invoke.getReturnType();
@@ -2410,7 +2407,7 @@
         Set<BasicBlock> moveExceptionTargets = Sets.newIdentityHashSet();
         catchHandlers.forEach(
             (exceptionType, targetOffset) -> {
-              BasicBlock header = new BasicBlock();
+              BasicBlock header = new BasicBlock(currentBlock.getMetadata());
               header.incrementUnfilledPredecessorCount();
               ssaWorklist.add(
                   new MoveExceptionWorklistItem(
@@ -2669,7 +2666,7 @@
   }
 
   private static BasicBlock createSplitEdgeBlock(BasicBlock source, BasicBlock target) {
-    BasicBlock splitBlock = new BasicBlock();
+    BasicBlock splitBlock = new BasicBlock(source.getMetadata());
     splitBlock.incrementUnfilledPredecessorCount();
     splitBlock.getMutablePredecessors().add(source);
     splitBlock.getMutableSuccessors().add(target);
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 edac43f..c971967 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
@@ -16,7 +16,6 @@
 import com.android.tools.r8.ir.optimize.PeepholeOptimizer;
 import com.android.tools.r8.ir.optimize.RuntimeWorkaroundCodeRewriter;
 import com.android.tools.r8.ir.regalloc.LinearScanRegisterAllocator;
-import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Timing;
 
@@ -46,7 +45,7 @@
     workaroundBugs(code, timing);
     code.traceBlocks();
     // Perform register allocation.
-    RegisterAllocator registerAllocator = performRegisterAllocation(code, method, timing);
+    LinearScanRegisterAllocator registerAllocator = performRegisterAllocation(code, method, timing);
     return new DexBuilder(code, bytecodeMetadataProvider, registerAllocator, options).build();
   }
 
@@ -66,7 +65,7 @@
   }
 
   @SuppressWarnings("UnusedVariable")
-  private RegisterAllocator performRegisterAllocation(
+  private LinearScanRegisterAllocator performRegisterAllocation(
       IRCode code, DexEncodedMethod method, Timing timing) {
     // Always perform dead code elimination before register allocation. The register allocator
     // does not allow dead code (to make sure that we do not waste registers for unneeded values).
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 e8b022d..e9b1aa4 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
@@ -1079,7 +1079,7 @@
     AffectedValues affectedValues = new AffectedValues();
     for (UnusedArgument unusedArgument : unusedArguments) {
       InstructionListIterator instructionIterator =
-          unusedArgument.getBlock().listIterator(code, unusedArgument);
+          unusedArgument.getBlock().listIterator(code, unusedArgument.getNext());
       if (unusedArgument.outValue().hasAnyUsers()) {
         // This is an unused argument with a default value. The unused argument is an operand of the
         // phi. This use is eliminated after constant propagation + branch pruning. We eliminate the
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
index 221adce..990257e 100644
--- 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
@@ -28,6 +28,7 @@
 import com.android.tools.r8.ir.code.IfType;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.InstructionList;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.IntSwitch;
 import com.android.tools.r8.ir.code.InvokeStatic;
@@ -525,21 +526,20 @@
                 ConstNumber cstToUse = trueNumber.isIntegerOne() ? trueNumber : falseNumber;
                 BasicBlock phiBlock = phi.getBlock();
                 Position phiPosition = phiBlock.getPosition();
-                int insertIndex = 0;
+                InstructionList instructions = phiBlock.getInstructions();
+                Instruction prevHead = instructions.getFirst();
                 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);
+                  instructions.addBefore(cstToUse, prevHead);
                 }
                 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);
+                instructions.addBefore(newInstruction, prevHead);
                 deadPhis++;
               }
             }
@@ -565,7 +565,7 @@
 
     int instructionSize = b.getInstructions().size();
     if (b.exit().isGoto() && (instructionSize == 2 || instructionSize == 3)) {
-      Instruction constInstruction = b.getInstructions().get(instructionSize - 2);
+      Instruction constInstruction = b.getInstructions().getLast().getPrev();
       if (constInstruction.isConstNumber()) {
         if (!constInstruction.asConstNumber().isIntegerOne()
             && !constInstruction.asConstNumber().isIntegerZero()) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
index a213f7f..2bb1a5a 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
@@ -143,7 +143,7 @@
             ConstNumber newConstant =
                 ConstNumber.copyOf(code, constValue.definition.asConstNumber());
             newConstant.setPosition(currentInstruction.getPosition());
-            newConstant.setBlock(currentInstruction.getBlock());
+            currentInstruction.getBlock();
             currentInstruction.replaceValue(constValue, newConstant.outValue());
             constValue.removeUser(currentInstruction);
             instructionIterator.previous();
@@ -274,9 +274,7 @@
                   for (Instruction user : constantValue.uniqueUsers()) {
                     ConstNumber newCstNum = ConstNumber.copyOf(code, constNumber);
                     newCstNum.setPosition(user.getPosition());
-                    InstructionListIterator iterator = user.getBlock().listIterator(code, user);
-                    iterator.previous();
-                    iterator.add(newCstNum);
+                    user.getBlock().getInstructions().addBefore(newCstNum, user);
                     user.replaceValue(constantValue, newCstNum.outValue());
                   }
                   constantValue.clearUsers();
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 e408515..f2f808e 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
@@ -517,14 +517,13 @@
         assert !instructionIterator.getBlock().hasCatchHandlers();
         for (Value inValue : copy.asNewArrayFilled().inValues()) {
           instructionIterator.add(inValue.getDefinition());
-          inValue.getDefinition().setBlock(instructionIterator.getBlock());
+          inValue.getDefinition();
           inValue.getDefinition().setPosition(newArrayEmpty.getPosition());
         }
       } else {
         assert false;
         return elementValue;
       }
-      copy.setBlock(instructionIterator.getBlock());
       copy.setPosition(newArrayEmpty.getPosition());
       instructionIterator.add(copy);
       addToRemove(elementValue.getDefinition());
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
index 1e327f8..69fb225 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
@@ -413,11 +413,11 @@
 
     private void patchControlFlow(IRCode code, BasicBlock comparisonBlock) {
       assert loopExit.getPhis().isEmpty(); // Edges should be split.
-      comparisonBlock.replaceLastInstruction(new Goto(loopBodyEntry), code);
+      comparisonBlock.replaceLastInstruction(new Goto(), code);
       comparisonBlock.removeSuccessor(loopExit);
 
       backPredecessor.replaceSuccessor(comparisonBlock, loopExit);
-      backPredecessor.replaceLastInstruction(new Goto(loopExit), code);
+      backPredecessor.replaceLastInstruction(new Goto(), code);
       comparisonBlock.removePredecessor(backPredecessor);
       loopExit.replacePredecessor(comparisonBlock, backPredecessor);
     }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ParentConstructorHoistingCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ParentConstructorHoistingCodeRewriter.java
index 43d249f..7b74732 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/ParentConstructorHoistingCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ParentConstructorHoistingCodeRewriter.java
@@ -23,7 +23,6 @@
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.TraversalContinuation;
 import com.android.tools.r8.utils.WorkList;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import java.util.ArrayDeque;
 import java.util.Deque;
@@ -96,15 +95,14 @@
         return;
       }
       // Change the instruction order and continue the hoisting.
-      List<Instruction> newInstructionOrder =
-          ImmutableList.<Instruction>builderWithExpectedSize(constants.size() + 2)
-              .addAll(constants)
-              .add(invoke)
-              .add(previousInstruction)
-              .build();
-      instructionIterator.next();
-      instructionIterator.set(newInstructionOrder);
-      IteratorUtils.skip(instructionIterator, -newInstructionOrder.size() - 1);
+      int numInstructions = constants.size() + 2;
+      for (int i = 0; i < numInstructions; ++i) {
+        instructionIterator.next().removeIgnoreValues();
+      }
+      instructionIterator.addAll(constants);
+      instructionIterator.add(invoke);
+      instructionIterator.add(previousInstruction);
+      IteratorUtils.skip(instructionIterator, -numInstructions);
     }
   }
 
@@ -121,8 +119,10 @@
     }
 
     // Remove the constants and the invoke from the block.
-    constants.forEach(constant -> block.getInstructions().removeFirst());
-    block.getInstructions().removeFirst();
+    int numToRemove = constants.size() + 1;
+    for (int i = 0; i < numToRemove; ++i) {
+      block.entry().removeIgnoreValues();
+    }
 
     // Add the constants and the invoke before the exit instruction in the predecessor block.
     InstructionListIterator predecessorInstructionIterator =
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/RedundantConstNumberRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/RedundantConstNumberRemover.java
index f031b5c..07b007c 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/RedundantConstNumberRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/RedundantConstNumberRemover.java
@@ -103,11 +103,10 @@
 
       if (ifInstruction.isZeroTest()) {
         changed |=
-            replaceDominatedConstNumbers(0, lhs, trueTarget, constantsByValue, code, dominatorTree);
+            replaceDominatedConstNumbers(0, lhs, trueTarget, constantsByValue, dominatorTree);
         if (lhs.knownToBeBoolean()) {
           changed |=
-              replaceDominatedConstNumbers(
-                  1, lhs, falseTarget, constantsByValue, code, dominatorTree);
+              replaceDominatedConstNumbers(1, lhs, falseTarget, constantsByValue, dominatorTree);
         }
       } else {
         assert rhs != null;
@@ -119,7 +118,6 @@
                   rhs,
                   trueTarget,
                   constantsByValue,
-                  code,
                   dominatorTree);
           if (lhs.knownToBeBoolean() && rhs.knownToBeBoolean()) {
             changed |=
@@ -128,7 +126,6 @@
                     rhs,
                     falseTarget,
                     constantsByValue,
-                    code,
                     dominatorTree);
           }
         } else {
@@ -140,7 +137,6 @@
                   lhs,
                   trueTarget,
                   constantsByValue,
-                  code,
                   dominatorTree);
           if (lhs.knownToBeBoolean() && rhs.knownToBeBoolean()) {
             changed |=
@@ -149,7 +145,6 @@
                     lhs,
                     falseTarget,
                     constantsByValue,
-                    code,
                     dominatorTree);
           }
         }
@@ -202,7 +197,6 @@
       Value newValue,
       BasicBlock dominator,
       LazyBox<Long2ReferenceMap<List<ConstNumber>>> constantsByValueSupplier,
-      IRCode code,
       LazyBox<DominatorTree> dominatorTree) {
     if (newValue.hasLocalInfo()) {
       // We cannot replace a constant with a value that has local info, because that could change
@@ -254,7 +248,7 @@
       if (dominatorTree.computeIfAbsent().dominatedBy(block, dominator)) {
         if (newValue.getType().lessThanOrEqual(value.getType(), appView)) {
           value.replaceUsers(newValue);
-          block.listIterator(code, constNumber).removeOrReplaceByDebugLocalRead();
+          constNumber.removeOrReplaceByDebugLocalRead();
           constantWithValueIterator.remove();
           changed = true;
         } else if (value.getType().isNullType()) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/StringSwitchRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/StringSwitchRemover.java
index 74fa808..4eb842c 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/StringSwitchRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/StringSwitchRemover.java
@@ -280,7 +280,7 @@
         newBlocksWithStrings.add(newBlock);
         if (previous == null) {
           // Replace the string-switch instruction by a goto instruction.
-          block.exit().replace(new Goto(newBlock), code);
+          block.exit().replace(new Goto(), code);
           block.link(newBlock);
         } else {
           // Set the fallthrough block for the previously added if-instruction.
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/SwitchCaseEliminator.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/SwitchCaseEliminator.java
index 4a1a1d4..5b95f17 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/SwitchCaseEliminator.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/SwitchCaseEliminator.java
@@ -23,7 +23,6 @@
 class SwitchCaseEliminator {
 
   private final BasicBlock block;
-  private final BasicBlock defaultTarget;
   private final InstructionListIterator iterator;
   private final Switch theSwitch;
 
@@ -35,7 +34,6 @@
 
   SwitchCaseEliminator(Switch theSwitch, InstructionListIterator iterator) {
     this.block = theSwitch.getBlock();
-    this.defaultTarget = theSwitch.fallthroughBlock();
     this.iterator = iterator;
     this.theSwitch = theSwitch;
   }
@@ -147,8 +145,7 @@
 
   private void replaceSwitchByGoto() {
     assert !hasAlwaysHitCase() || alwaysHitTarget != null;
-    BasicBlock target = hasAlwaysHitCase() ? alwaysHitTarget : defaultTarget;
-    iterator.replaceCurrentInstruction(new Goto(target));
+    iterator.replaceCurrentInstruction(new Goto());
   }
 
   private void replaceSwitchByOptimizedSwitch(
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
index 4a2d298..3b1d253 100644
--- 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
@@ -115,7 +115,7 @@
                   typeAnalysis -> typeAnalysis.setKeepRedundantBlocksAfterAssumeRemoval(true));
             }
             if (block.size() != blockSizeBeforeAssumeRemoval) {
-              it = previous != null ? block.listIterator(code, previous) : block.listIterator(code);
+              it = block.listIterator(code, previous != null ? previous.getNext() : block.entry());
             }
           }
         } else if (current.isInstanceOf()) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/AssertionsRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/AssertionsRewriter.java
index d002613..92fd99c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/AssertionsRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/AssertionsRewriter.java
@@ -495,7 +495,7 @@
                     dexItemFactory.createMethod(configuration.getAssertionHandler()),
                     null,
                     ImmutableList.of(throwInstruction.exception())));
-            Goto gotoBlockAfterAssertion = new Goto(throwingBlock);
+            Goto gotoBlockAfterAssertion = new Goto();
             gotoBlockAfterAssertion.setPosition(throwInstruction.getPosition());
             throwingBlock.link(throwSuccessorAfterHandler.get(throwInstruction));
             iterator.add(gotoBlockAfterAssertion);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/BasicBlockInstructionsEquivalence.java b/src/main/java/com/android/tools/r8/ir/optimize/BasicBlockInstructionsEquivalence.java
index b57fd9b..cf599a6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/BasicBlockInstructionsEquivalence.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/BasicBlockInstructionsEquivalence.java
@@ -7,12 +7,12 @@
 import com.android.tools.r8.ir.code.CatchHandlers;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionList;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.google.common.base.Equivalence;
 import java.util.Arrays;
-import java.util.Iterator;
 import java.util.List;
 
 class BasicBlockInstructionsEquivalence extends Equivalence<BasicBlock> {
@@ -30,19 +30,19 @@
   }
 
   private boolean hasIdenticalInstructions(BasicBlock first, BasicBlock second) {
-    List<Instruction> instructions0 = first.getInstructions();
-    List<Instruction> instructions1 = second.getInstructions();
+    InstructionList instructions0 = first.getInstructions();
+    InstructionList instructions1 = second.getInstructions();
     if (instructions0.size() != instructions1.size()) {
       return false;
     }
-    Iterator<Instruction> it0 = instructions0.iterator();
-    Iterator<Instruction> it1 = instructions1.iterator();
-    while (it0.hasNext()) {
-      Instruction i0 = it0.next();
-      Instruction i1 = it1.next();
+    Instruction i0 = instructions0.getFirstOrNull();
+    Instruction i1 = instructions1.getFirstOrNull();
+    while (i0 != null) {
       if (!i0.identicalAfterRegisterAllocation(i1, allocator, conversionOptions)) {
         return false;
       }
+      i0 = i0.getNext();
+      i1 = i1.getNext();
     }
 
     if (!allocator.hasEqualTypesAtEntry(first, second)) {
@@ -93,21 +93,21 @@
   }
 
   private int computeHash(BasicBlock basicBlock) {
-    List<Instruction> instructions = basicBlock.getInstructions();
+    InstructionList instructions = basicBlock.getInstructions();
     int hash = instructions.size();
     int i = 0;
-    for (Instruction instruction : instructions) {
+    for (Instruction inst = instructions.getFirstOrNull(); inst != null; inst = inst.getNext()) {
       if (++i > MAX_HASH_INSTRUCTIONS) {
         break;
       }
       int hashPart = 0;
-      if (instruction.outValue() != null && instruction.outValue().needsRegister()) {
-        hashPart += allocator.getRegisterForValue(instruction.outValue(), instruction.getNumber());
+      if (inst.outValue() != null && inst.outValue().needsRegister()) {
+        hashPart += allocator.getRegisterForValue(inst.outValue(), inst.getNumber());
       }
-      for (Value inValue : instruction.inValues()) {
+      for (Value inValue : inst.inValues()) {
         hashPart = hashPart << 4;
         if (inValue.needsRegister()) {
-          hashPart += allocator.getRegisterForValue(inValue, instruction.getNumber());
+          hashPart += allocator.getRegisterForValue(inValue, inst.getNumber());
         }
       }
       hash = hash * 3 + hashPart;
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 aa4bafe..f204dae 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
@@ -14,7 +14,6 @@
 import com.android.tools.r8.ir.code.DebugLocalsChange;
 import com.android.tools.r8.ir.code.IRCode;
 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.Move;
 import com.android.tools.r8.ir.code.Position;
@@ -230,12 +229,14 @@
         Instruction instruction = it.next();
         if (instruction.isMove()) {
           Move move = instruction.asMove();
+          int dst = allocator.getRegisterForValue(move.dest(), move.getNumber());
           if (unneededMoves.contains(move)) {
-            int dst = allocator.getRegisterForValue(move.dest(), move.getNumber());
             int src = allocator.getRegisterForValue(move.src(), move.getNumber());
             int mappedSrc = mapping.getOrDefault(src, src);
             mapping.put(dst, mappedSrc);
             it.removeInstructionIgnoreOutValue();
+          } else {
+            mapping.remove(dst);
           }
         } else if (instruction.isDebugLocalsChange()) {
           DebugLocalsChange change = instruction.asDebugLocalsChange();
@@ -257,15 +258,13 @@
     IntSet clobberedRegisters = new IntOpenHashSet();
     // Backwards instruction scan collecting the registers used by actual instructions.
     boolean inEntrySpillMoves = false;
-    InstructionIterator it = block.iterator(block.getInstructions().size());
-    while (it.hasPrevious()) {
-      Instruction instruction = it.previous();
-      if (instruction == postSpillLocalsChange) {
+    for (Instruction ins = block.getLastInstruction(); ins != null; ins = ins.getPrev()) {
+      if (ins == postSpillLocalsChange) {
         inEntrySpillMoves = true;
       }
       // If this is a move in the block-entry spill moves check if it is unneeded.
-      if (inEntrySpillMoves && instruction.isMove()) {
-        Move move = instruction.asMove();
+      if (inEntrySpillMoves && ins.isMove()) {
+        Move move = ins.asMove();
         int dst = allocator.getRegisterForValue(move.dest(), move.getNumber());
         int src = allocator.getRegisterForValue(move.src(), move.getNumber());
         if (!usedRegisters.contains(dst) && !clobberedRegisters.contains(src)) {
@@ -273,18 +272,17 @@
           continue;
         }
       }
-      if (instruction.outValue() != null && instruction.outValue().needsRegister()) {
-        int register =
-            allocator.getRegisterForValue(instruction.outValue(), instruction.getNumber());
+      if (ins.outValue() != null && ins.outValue().needsRegister()) {
+        int register = allocator.getRegisterForValue(ins.outValue(), ins.getNumber());
         // The register is defined anew, so uses before this are on distinct values.
         usedRegisters.remove(register);
         // Mark it clobbered to avoid any uses in locals after this point to become invalid.
         clobberedRegisters.add(register);
       }
-      if (!instruction.inValues().isEmpty()) {
-        for (Value inValue : instruction.inValues()) {
+      if (!ins.inValues().isEmpty()) {
+        for (Value inValue : ins.inValues()) {
           if (inValue.needsRegister()) {
-            int register = allocator.getRegisterForValue(inValue, instruction.getNumber());
+            int register = allocator.getRegisterForValue(inValue, ins.getNumber());
             // Record the register as being used.
             usedRegisters.add(register);
           }
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 7a18075..a4e941c 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
@@ -252,10 +252,7 @@
       for (Instruction oldInstruction : entry.getValue()) {
         if (oldInstruction != newInstruction) {
           oldInstruction.outValue().replaceUsers(newInstruction.outValue());
-          oldInstruction
-              .getBlock()
-              .listIterator(code, oldInstruction)
-              .removeOrReplaceByDebugLocalRead();
+          oldInstruction.removeOrReplaceByDebugLocalRead();
 
           // If the removed instruction is an insertion point for another constant, then record that
           // the constant should instead be inserted at the point where the removed instruction has
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index d73ab2a..add17fb 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -660,7 +660,7 @@
         code.prepareBlocksForCatchHandlers();
 
         // Create a block for holding the monitor-exit instruction.
-        BasicBlock monitorExitBlock = new BasicBlock();
+        BasicBlock monitorExitBlock = new BasicBlock(code.metadata());
         monitorExitBlock.setNumber(code.getNextBlockNumber());
 
         // For each block in the code that may throw, add a catch-all handler targeting the
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java
index 84fe7f8..3f22277 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java
@@ -11,7 +11,7 @@
 import com.android.tools.r8.ir.code.Goto;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.InstructionList;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Value;
@@ -26,7 +26,6 @@
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.IdentityHashMap;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
@@ -157,17 +156,15 @@
 
       // Remove the instruction from the normal successors.
       for (BasicBlock normalSuccessor : normalSuccessors) {
-        normalSuccessor.getInstructions().removeFirst();
+        normalSuccessor.entry().removeIgnoreValues();
       }
 
       // Move the instruction into the predecessor.
       if (instruction.isJumpInstruction()) {
         // Replace jump instruction in predecessor with the jump instruction from the normal
         // successors.
-        LinkedList<Instruction> instructions = block.getInstructions();
-        instructions.removeLast();
-        instructions.add(instruction);
-        instruction.setBlock(block);
+        block.getLastInstruction().removeIgnoreValues();
+        block.getInstructions().addLast(instruction);
 
         // Take a copy of the old normal successors before performing a destructive update.
         List<BasicBlock> oldNormalSuccessors = new ArrayList<>(normalSuccessors);
@@ -191,8 +188,7 @@
         }
       } else {
         // Insert instruction before the jump instruction in the predecessor.
-        block.getInstructions().listIterator(block.getInstructions().size() - 1).add(instruction);
-        instruction.setBlock(block);
+        block.getInstructions().addBefore(instruction, block.getLastInstruction());
 
         // Update locals-at-entry if needed.
         if (instruction.isDebugLocalsChange()) {
@@ -238,7 +234,7 @@
     BasicBlock normalExit = null;
     List<BasicBlock> normalExits = code.computeNormalExitBlocks();
     if (normalExits.size() > 1) {
-      normalExit = new BasicBlock();
+      normalExit = new BasicBlock(code.metadata());
       normalExit.getMutablePredecessors().addAll(normalExits);
       blocks = new ArrayList<>(code.blocks);
       blocks.add(normalExit);
@@ -259,8 +255,7 @@
           if (pred.exit().isGoto()
               && pred.getSuccessors().size() == 1
               && pred.getInstructions().size() > 1) {
-            List<Instruction> instructions = pred.getInstructions();
-            Instruction lastInstruction = instructions.get(instructions.size() - 2);
+            Instruction lastInstruction = pred.getLastInstruction().getPrev();
             List<BasicBlock> value = lastInstructionToBlocks.computeIfAbsent(
                 equivalence.wrap(lastInstruction), (k) -> new ArrayList<>());
             value.add(pred);
@@ -297,7 +292,7 @@
           }
           BasicBlock newBlock =
               createAndInsertBlockForSuffix(
-                  code.getNextBlockNumber(),
+                  code,
                   commonSuffixSize,
                   predsWithSameLastInstruction,
                   block == normalExit ? null : block,
@@ -318,7 +313,7 @@
   }
 
   private static BasicBlock createAndInsertBlockForSuffix(
-      int blockNumber,
+      IRCode code,
       int suffixSize,
       List<BasicBlock> preds,
       BasicBlock successorBlock,
@@ -326,49 +321,57 @@
     BasicBlock first = preds.get(0);
     assert (successorBlock != null && first.exit().isGoto())
         || (successorBlock == null && first.exit().isReturn());
-    BasicBlock newBlock = new BasicBlock();
-    newBlock.setNumber(blockNumber);
-    InstructionIterator from = first.iterator(first.getInstructions().size());
+    BasicBlock newBlock = new BasicBlock(code.metadata());
+    newBlock.setNumber(code.getNextBlockNumber());
     Int2ReferenceMap<DebugLocalInfo> newBlockEntryLocals = null;
     if (first.getLocalsAtEntry() != null) {
       newBlockEntryLocals = new Int2ReferenceOpenHashMap<>(first.getLocalsAtEntry());
       int prefixSize = first.getInstructions().size() - suffixSize;
-      InstructionIterator it = first.iterator();
+      Instruction instruction = first.entry();
       for (int i = 0; i < prefixSize; i++) {
-        Instruction instruction = it.next();
         if (instruction.isDebugLocalsChange()) {
           instruction.asDebugLocalsChange().apply(newBlockEntryLocals);
         }
+        instruction = instruction.getNext();
       }
     }
 
     allocator.addNewBlockToShareIdenticalSuffix(newBlock, suffixSize, preds);
 
     boolean movedThrowingInstruction = false;
+    Instruction instruction = first.getLastInstruction();
     for (int i = 0; i < suffixSize; i++) {
-      Instruction instruction = from.previous();
       movedThrowingInstruction = movedThrowingInstruction || instruction.instructionTypeCanThrow();
-      newBlock.getInstructions().addFirst(instruction);
-      instruction.setBlock(newBlock);
+      instruction = instruction.getPrev();
     }
+    newBlock
+        .getInstructions()
+        .severFrom(instruction == null ? first.entry() : instruction.getNext());
     if (movedThrowingInstruction && first.hasCatchHandlers()) {
       newBlock.transferCatchHandlers(first);
     }
+
     for (BasicBlock pred : preds) {
-      Position lastPosition = pred.getPosition();
-      LinkedList<Instruction> instructions = pred.getInstructions();
-      for (int i = 0; i < suffixSize; i++) {
-        instructions.removeLast();
+      Position lastPosition;
+      InstructionList instructions = pred.getInstructions();
+      if (pred == first) {
+        // Already removed from first via severFrom().
+        lastPosition = newBlock.getPosition();
+      } else {
+        lastPosition = pred.getPosition();
+        for (int i = 0; i < suffixSize; i++) {
+          instructions.removeIgnoreValues(instructions.getLast());
+        }
       }
-      for (Instruction instruction : pred.getInstructions()) {
-        if (instruction.getPosition().isSome()) {
-          lastPosition = instruction.getPosition();
+      for (Instruction ins = instructions.getLastOrNull(); ins != null; ins = ins.getPrev()) {
+        if (ins.getPosition().isSome()) {
+          lastPosition = ins.getPosition();
+          break;
         }
       }
       Goto jump = new Goto();
-      jump.setBlock(pred);
       jump.setPosition(lastPosition);
-      instructions.add(jump);
+      instructions.addLast(jump);
       newBlock.getMutablePredecessors().add(pred);
       if (successorBlock != null) {
         pred.replaceSuccessor(successorBlock, newBlock);
@@ -411,15 +414,15 @@
     if (!Objects.equals(localsAtBlockExit(block0), localsAtBlockExit(block1))) {
       return 0;
     }
-    InstructionIterator it0 = block0.iterator(block0.getInstructions().size());
-    InstructionIterator it1 = block1.iterator(block1.getInstructions().size());
+    Instruction i0 = block0.getLastInstruction();
+    Instruction i1 = block1.getLastInstruction();
     int suffixSize = 0;
-    while (it0.hasPrevious() && it1.hasPrevious()) {
-      Instruction i0 = it0.previous();
-      Instruction i1 = it1.previous();
+    while (i0 != null && i1 != null) {
       if (!i0.identicalAfterRegisterAllocation(i1, allocator, code.getConversionOptions())) {
         return suffixSize;
       }
+      i0 = i0.getPrev();
+      i1 = i1.getPrev();
       suffixSize++;
     }
     return suffixSize;
@@ -464,9 +467,8 @@
             assert !otherPred.getPredecessors().contains(pred);
             otherPred.getMutablePredecessors().add(pred);
             Goto exit = new Goto();
-            exit.setBlock(pred);
             exit.setPosition(otherPred.getPosition());
-            pred.getInstructions().add(exit);
+            pred.getInstructions().addLast(exit);
           } else {
             blockToIndex.put(wrapper, predIndex);
           }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/PhiOptimizations.java b/src/main/java/com/android/tools/r8/ir/optimize/PhiOptimizations.java
index 9a5e3b9..ec3a5a4 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/PhiOptimizations.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/PhiOptimizations.java
@@ -7,7 +7,6 @@
 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.InstructionIterator;
 import com.android.tools.r8.ir.code.Load;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Store;
@@ -86,10 +85,8 @@
   private static int getStackHeightAtInstructionBackwards(Instruction instruction) {
     int stackHeight = 0;
     BasicBlock block = instruction.getBlock();
-    InstructionIterator it = block.iterator(block.getInstructions().size() - 1);
-    while (it.hasPrevious()) {
-      Instruction current = it.previous();
-      if (current == instruction) {
+    for (Instruction ins = block.exit().getPrev(); ins != null; ins = ins.getPrev()) {
+      if (ins == instruction) {
         break;
       }
       stackHeight -= PeepholeHelper.numberOfValuesPutOnStack(instruction);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
index aa435cf..543d157 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
@@ -406,7 +406,7 @@
             } else if (instruction.isInitClass()) {
               handleInitClass(it, instruction.asInitClass());
             } else if (instruction.isMonitor()) {
-              if (instruction.asMonitor().isEnter()) {
+              if (instruction.isMonitorEnter()) {
                 killAllNonFinalActiveFields();
               }
             } else if (instruction.isInvokeDirect()) {
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 bcb57ce..06f3c5f 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
@@ -65,7 +65,6 @@
       // Split out the last recursive call in its own block.
       InstructionListIterator splitIterator =
           lastSelfRecursiveCall.getBlock().listIterator(code, lastSelfRecursiveCall);
-      splitIterator.previous();
       BasicBlock newBlock = splitIterator.split(code, 1);
       // Generate rethrow block.
       DexType guard = appView.dexItemFactory().throwableType;
@@ -251,7 +250,6 @@
               && target.getNormalPredecessors().size() > 1
               && target.getNormalSuccessors().size() > 1) {
             Instruction fixit = new AlwaysMaterializingNop();
-            fixit.setBlock(handler);
             fixit.setPosition(handler.getPosition());
             handler.getInstructions().addFirst(fixit);
           }
@@ -362,12 +360,10 @@
     // Forced definition of const-zero
     Value fixitValue = code.createValue(TypeElement.getInt());
     Instruction fixitDefinition = new AlwaysMaterializingDefinition(fixitValue);
-    fixitDefinition.setBlock(addBefore.getBlock());
     fixitDefinition.setPosition(addBefore.getPosition());
     it.add(fixitDefinition);
     // Forced user of the forced definition to ensure it has a user and thus live range.
     Instruction fixitUser = new AlwaysMaterializingUser(fixitValue);
-    fixitUser.setBlock(addBefore.getBlock());
     fixitUser.setPosition(addBefore.getPosition());
     it.add(fixitUser);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/FieldValueHelper.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/FieldValueHelper.java
index 0e185da..a7cf6e1 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/FieldValueHelper.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/FieldValueHelper.java
@@ -14,7 +14,6 @@
 import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.IRCode;
 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.Phi;
 import com.android.tools.r8.ir.code.Phi.RegisterReadType;
@@ -125,14 +124,9 @@
 
   @SuppressWarnings("ReferenceEquality")
   private Value getValueDefinedInTheBlock(BasicBlock block, Instruction stopAt) {
-    InstructionIterator iterator =
-        stopAt == null ? block.iterator(block.getInstructions().size()) : block.iterator(stopAt);
-
     Instruction valueProducingInsn = null;
-    while (iterator.hasPrevious()) {
-      Instruction instruction = iterator.previous();
-      assert instruction != null;
-
+    Instruction instruction = stopAt != null ? stopAt : block.getLastInstruction();
+    do {
       if (instruction == root
           || (instruction.isInstancePut()
               && instruction.asInstancePut().getField() == field
@@ -140,7 +134,8 @@
         valueProducingInsn = instruction;
         break;
       }
-    }
+      instruction = instruction.getPrev();
+    } while (instruction != null);
 
     if (valueProducingInsn == null) {
       return null;
@@ -151,7 +146,7 @@
 
     assert root == valueProducingInsn;
     if (defaultValue == null) {
-      InstructionListIterator it = block.listIterator(code, root);
+      InstructionListIterator it = block.listIterator(code, root.getNext());
       // If we met newInstance it means that default value is supposed to be used.
       if (field.type.isPrimitiveType()) {
         defaultValue =
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
index 403719e..da709f6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
@@ -36,7 +36,6 @@
 import com.android.tools.r8.ir.code.AliasedValueConfiguration;
 import com.android.tools.r8.ir.code.AssumeAndCheckCastAliasedValueConfiguration;
 import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.BasicBlockInstructionListIterator;
 import com.android.tools.r8.ir.code.BasicBlockIterator;
 import com.android.tools.r8.ir.code.CheckCast;
 import com.android.tools.r8.ir.code.IRCode;
@@ -634,7 +633,7 @@
       if (user.isInstanceOf()) {
         InstanceOf instanceOf = user.asInstanceOf();
         InstructionListIterator instructionIterator =
-            user.getBlock().listIterator(code, instanceOf);
+            user.getBlock().listIterator(code, instanceOf.getNext());
         instructionIterator.replaceCurrentInstructionWithConstBoolean(
             code, appView.appInfo().isSubtype(eligibleClass.getType(), instanceOf.type()));
         continue;
@@ -802,8 +801,7 @@
       affectedValues.addAll(newValue.affectedValues());
     }
     if (replacement != null) {
-      BasicBlockInstructionListIterator it = fieldRead.getBlock().listIterator(code, fieldRead);
-      it.replaceCurrentInstruction(replacement, affectedValues);
+      fieldRead.replace(replacement, affectedValues);
     } else {
       removeInstruction(fieldRead);
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
index 8b566cf..5452d6e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
@@ -296,8 +296,7 @@
       }
 
       if (ordinalToTargetMap.isEmpty()) {
-        switchInsn.replace(
-            Goto.builder().setTarget(block.getUniqueNormalSuccessor()).build(), code);
+        switchInsn.replace(new Goto(), code);
       } else {
         int[] keys = ordinalToTargetMap.keySet().toIntArray();
         Arrays.sort(keys);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
index caf6e7e..0a54d91 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
@@ -1305,16 +1305,12 @@
           returnValue = null;
         }
         Invoke outlineInvoke = new InvokeStatic(outlineMethod, returnValue, in);
-        outlineInvoke.setBlock(lastInstruction.getBlock());
+        lastInstruction.getBlock();
         outlineInvoke.setPosition(
             positionBuilder.hasOutlinePositions()
                 ? positionBuilder.build()
                 : Position.syntheticNone());
-        InstructionListIterator endIterator =
-            lastInstruction.getBlock().listIterator(code, lastInstruction);
-        Instruction instructionBeforeEnd = endIterator.previous();
-        assert instructionBeforeEnd == lastInstruction;
-        endIterator.set(outlineInvoke);
+        lastInstruction.replace(outlineInvoke);
         if (outlineInvoke.hasOutValue()
             && returnValue.getType().isReferenceType()
             && returnValue.getType().nullability().isDefinitelyNotNull()) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/peepholes/PeepholeHelper.java b/src/main/java/com/android/tools/r8/ir/optimize/peepholes/PeepholeHelper.java
index 5fef766..c0ea809 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/peepholes/PeepholeHelper.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/peepholes/PeepholeHelper.java
@@ -58,18 +58,12 @@
       InstructionListIterator it, List<Instruction> instructions) {
     assert !instructions.isEmpty();
     for (Instruction instruction : instructions) {
+      // Add duplicate users to offset the users being removed by removeOrReplaceByDebugLocalRead().
       for (Value inValue : instruction.inValues()) {
         inValue.addUser(instruction);
       }
+      instruction.removeOrReplaceByDebugLocalRead();
       it.add(instruction);
     }
-    Instruction current = it.nextUntil(i -> i == instructions.get(0));
-    for (int i = 0; i < instructions.size(); i++) {
-      assert current == instructions.get(i);
-      it.removeOrReplaceByDebugLocalRead();
-      current = it.next();
-    }
-    it.previousUntil(i -> i == instructions.get(instructions.size() - 1));
-    it.next();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/InsertMovesResult.java b/src/main/java/com/android/tools/r8/ir/regalloc/InsertMovesResult.java
new file mode 100644
index 0000000..574ebee
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/InsertMovesResult.java
@@ -0,0 +1,25 @@
+// Copyright (c) 2024, 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.regalloc;
+
+public class InsertMovesResult {
+
+  private final LinearScanRegisterAllocator allocator;
+  private final int numberOfParallelMoveTemporaryRegisters;
+
+  public InsertMovesResult(
+      LinearScanRegisterAllocator allocator, int numberOfParallelMoveTemporaryRegisters) {
+    this.allocator = allocator;
+    this.numberOfParallelMoveTemporaryRegisters = numberOfParallelMoveTemporaryRegisters;
+  }
+
+  public int getNumberOfParallelMoveTemporaryRegisters() {
+    return numberOfParallelMoveTemporaryRegisters;
+  }
+
+  public void revert() {
+    allocator.removeSpillAndPhiMoves();
+    allocator.removeParallelMoveTemporaryRegisters();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
index 9e54599..7216583 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.ir.code.IRCode.INSTRUCTION_NUMBER_DELTA;
 import static com.android.tools.r8.ir.regalloc.LiveIntervals.NO_REGISTER;
+import static com.google.common.base.Predicates.alwaysTrue;
 
 import com.android.tools.r8.cf.FixedLocalValue;
 import com.android.tools.r8.dex.Constants;
@@ -29,6 +30,7 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Invoke;
 import com.android.tools.r8.ir.code.Move;
+import com.android.tools.r8.ir.code.MoveException;
 import com.android.tools.r8.ir.code.NumericType;
 import com.android.tools.r8.ir.code.Or;
 import com.android.tools.r8.ir.code.Phi;
@@ -41,7 +43,9 @@
 import com.android.tools.r8.ir.regalloc.RegisterPositions.RegisterType;
 import com.android.tools.r8.utils.ArrayUtils;
 import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.IntObjPredicate;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.LinkedHashSetUtils;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.SetUtils;
@@ -60,10 +64,14 @@
 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.IntRBTreeSet;
 import it.unimi.dsi.fastutil.ints.IntSet;
+import it.unimi.dsi.fastutil.ints.IntSortedSet;
 import it.unimi.dsi.fastutil.objects.Reference2IntArrayMap;
 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.Collection;
 import java.util.Collections;
 import java.util.Comparator;
@@ -75,8 +83,6 @@
 import java.util.Map;
 import java.util.PriorityQueue;
 import java.util.Set;
-import java.util.TreeSet;
-import java.util.function.BiPredicate;
 import java.util.function.Predicate;
 
 /**
@@ -195,13 +201,11 @@
   private Map<BasicBlock, LiveAtEntrySets> liveAtEntrySets;
   // The value of the first argument, or null if the method has no arguments.
   protected Value firstArgumentValue;
-  // The value of the last argument, or null if the method has no arguments.
-  private Value lastArgumentValue;
 
   // The current register allocation mode.
   private ArgumentReuseMode mode;
   // The set of registers that are free for allocation.
-  private TreeSet<Integer> freeRegisters = new TreeSet<>();
+  private IntSortedSet freeRegisters = new IntRBTreeSet();
   // The max register number used.
   private int maxRegisterNumber = -1;
 
@@ -252,6 +256,10 @@
             && isDedicatedMoveExceptionRegisterInLastLocalRegister());
   }
 
+  private boolean isDedicatedMoveExceptionRegister(int register) {
+    return hasDedicatedMoveExceptionRegister() && register == getMoveExceptionRegister();
+  }
+
   private boolean isDedicatedMoveExceptionRegisterInFirstLocalRegister() {
     assert hasDedicatedMoveExceptionRegister();
     if (mode.is4Bit() || mode.is16Bit()) {
@@ -349,7 +357,6 @@
       while (it.hasNext()) {
         Instruction instruction = it.next();
         if (instruction.isDebugLocalRead()) {
-          instruction.clearDebugValues();
           it.remove();
         }
       }
@@ -668,14 +675,46 @@
   // Compute a table that for each register numbers contains the number of previous register
   // numbers that were unused. This table is then used to slide down the actual registers
   // used to fill the gaps.
-  private boolean computeUnusedRegisters() {
+  private void computeUnusedRegisters() {
+    unusedRegisters = internalComputeUnusedRegisters();
+  }
+
+  private void recomputeUnusedRegisters() {
+    int[] newUnusedRegisters = internalComputeUnusedRegisters();
+    assert verifyNoUsesOfPreviouslyUnusedRegisters(newUnusedRegisters);
+    unusedRegisters = newUnusedRegisters;
+  }
+
+  private int[] internalComputeUnusedRegisters() {
     if (mode.is4Bit() || registersUsed() == 0) {
-      return false;
+      return null;
     }
     // Compute the table based on the set of used registers.
     IntSet usedRegisters = computeUsedRegisters();
-    unusedRegisters = computeUnusedRegistersFromUsedRegisters(usedRegisters);
-    return ArrayUtils.lastOrDefault(unusedRegisters, 0) > 0;
+    return computeUnusedRegistersFromUsedRegisters(usedRegisters);
+  }
+
+  private boolean verifyNoChangesToUnusedRegisters() {
+    assert Arrays.equals(unusedRegisters, internalComputeUnusedRegisters());
+    return true;
+  }
+
+  private boolean verifyNoUsesOfPreviouslyUnusedRegisters(int[] newUnusedRegisters) {
+    // We only recompute the unused registers when at least one argument live intervals was unsplit,
+    // thus we always compute a non-null unused registers result.
+    assert unusedRegisters != null;
+    assert newUnusedRegisters != null;
+    assert unusedRegisters.length == newUnusedRegisters.length;
+    int previousNumberOfUnusedRegisters = 0;
+    int previousNumberOfNewUnusedRegisters = 0;
+    for (int i = 0; i < unusedRegisters.length; i++) {
+      boolean wasRegisterUnused = previousNumberOfUnusedRegisters != unusedRegisters[i];
+      boolean isRegisterUnused = previousNumberOfNewUnusedRegisters != newUnusedRegisters[i];
+      assert !wasRegisterUnused || isRegisterUnused;
+      previousNumberOfUnusedRegisters = unusedRegisters[i];
+      previousNumberOfNewUnusedRegisters = newUnusedRegisters[i];
+    }
+    return true;
   }
 
   private IntSet computeUsedRegisters() {
@@ -689,8 +728,10 @@
     }
     // Additionally, we have used temporary registers for parallel move scheduling, those
     // are used as well.
-    for (int i = firstParallelMoveTemporary; i < maxRegisterNumber + 1; i++) {
-      usedRegisters.add(i);
+    if (firstParallelMoveTemporary != NO_REGISTER) {
+      for (int i = firstParallelMoveTemporary; i < maxRegisterNumber + 1; i++) {
+        usedRegisters.add(i);
+      }
     }
     return usedRegisters;
   }
@@ -704,12 +745,13 @@
   }
 
   private int[] computeUnusedRegistersFromUsedRegisters(IntSet usedRegisters) {
-    assert firstParallelMoveTemporary != NO_REGISTER;
     int firstLocalRegister = numberOfArgumentRegisters + getMoveExceptionOffsetForLocalRegisters();
     assert verifyRegistersBeforeFirstLocalRegisterAreUsed(firstLocalRegister, usedRegisters);
-    int numberOfParallelMoveTemporaryRegisters = registersUsed() - firstParallelMoveTemporary;
+    int registersUsed = unadjustedRegistersUsed();
+    int numberOfParallelMoveTemporaryRegisters =
+        firstParallelMoveTemporary != NO_REGISTER ? registersUsed - firstParallelMoveTemporary : 0;
     int numberOfLocalRegisters =
-        registersUsed() - firstLocalRegister - numberOfParallelMoveTemporaryRegisters;
+        registersUsed - firstLocalRegister - numberOfParallelMoveTemporaryRegisters;
     int unused = 0;
     int[] unusedRegisters = new int[numberOfLocalRegisters];
     for (int i = 0; i < numberOfLocalRegisters; i++) {
@@ -742,6 +784,10 @@
     return numberOfRegister;
   }
 
+  private int unadjustedRegistersUsed() {
+    return maxRegisterNumber + 1;
+  }
+
   @Override
   public int getRegisterForValue(Value value, int instructionNumber) {
     if (value.isFixedRegisterValue()) {
@@ -763,13 +809,19 @@
 
   @Override
   public int getArgumentOrAllocateRegisterForValue(Value value, int instructionNumber) {
-    if (value.isArgument()) {
-      return getRegisterForIntervals(value.getLiveIntervals());
+    if (isPinnedArgument(value)) {
+      return getArgumentRegisterForValue(value);
     }
     return getRegisterForValue(value, instructionNumber);
   }
 
   @Override
+  public int getArgumentRegisterForValue(Value value) {
+    assert value.isArgument();
+    return getRegisterForIntervals(value.getLiveIntervals().getSplitParent());
+  }
+
+  @Override
   public InternalOptions options() {
     return appView.options();
   }
@@ -807,7 +859,7 @@
     this.mode = mode;
 
     if (retry) {
-      clearRegisterAssignments(mode);
+      clearRegisterAssignments();
       removeSpillAndPhiMoves();
     }
 
@@ -815,15 +867,55 @@
 
     boolean succeeded = performLinearScan(mode);
     if (succeeded) {
-      insertMoves();
-      // Now that we know the max register number we can compute whether it is safe to use
-      // argument registers in place. If it is, we redo move insertion to get rid of the moves
-      // caused by splitting of the argument registers.
-      if (unsplitArguments()) {
-        removeSpillAndPhiMoves();
-        insertMoves();
-      }
+      InsertMovesResult insertMovesResult = insertMoves();
+
+      // We only compute unused information for local registers (temporary registers for parallel
+      // move scheduling and argument registers are never unused). Therefore, we can already compute
+      // unused registers now. This can help lead to more aggressive argument unsplitting, since
+      // this effectively lowers the real argument registers.
       computeUnusedRegisters();
+
+      // After having finished move insertion (which can allocate temporary registers for parallel
+      // move scheduling), we now know the final registers of the arguments. If we have moved some
+      // arguments down to low registers, but the input argument register itself ended up being in a
+      // low register, then we can avoid the move into a low register by just using the argument
+      // register directly. This is achieved by updating the register assignment to argument split
+      // live intervals.
+      UnsplitArgumentsResult unsplitArgumentsResult = unsplitArguments();
+      if (unsplitArgumentsResult != null) {
+        // If any changes were made, we need to redo move insertion.
+        insertMovesResult.revert();
+        InsertMovesResult newInsertMovesResult = insertMoves();
+        int iterations = 0;
+
+        // In some cases, the new move insertion may lead to more temporary registers being used for
+        // parallel move scheduling. This is rare (e.g., never happens when compiling JetNews). If
+        // that happened, the argument registers are now in higher registers, meaning we may have
+        // invalidated the argument unsplitting. We therefore (partially) revert the argument
+        // unsplitting and redo move insertion.
+        while (newInsertMovesResult.getNumberOfParallelMoveTemporaryRegisters()
+            > insertMovesResult.getNumberOfParallelMoveTemporaryRegisters()) {
+          assert iterations < 5;
+          boolean changed = unsplitArgumentsResult.revertPartial();
+          if (changed) {
+            // We invalidated the unsplit arguments optimization (or some of it). Redo move
+            // insertion and check again.
+            insertMovesResult = newInsertMovesResult;
+            insertMovesResult.revert();
+            newInsertMovesResult = insertMoves();
+          } else {
+            // Although we used more parallel move temporary registers this did not invalidate the
+            // unsplit arguments result.
+            break;
+          }
+          iterations++;
+        }
+        if (unsplitArgumentsResult.isFullyReverted()) {
+          assert verifyNoChangesToUnusedRegisters();
+        } else {
+          recomputeUnusedRegisters();
+        }
+      }
     } else {
       assert mode.is4Bit();
     }
@@ -879,6 +971,8 @@
     assert !result.is8Bit() || highestUsedRegister() <= Constants.U8BIT_MAX;
     assert !result.is16Bit() || highestUsedRegister() <= Constants.U16BIT_MAX;
 
+    new MoveSorter(code).sortMovesForSuffixSharing();
+
     return result;
   }
 
@@ -886,46 +980,40 @@
   // we can get the argument into low enough registers at uses that require low numbers. After
   // register allocation we can check if it is safe to just use the argument register itself
   // for all uses and thereby avoid moving argument values around.
-  private boolean unsplitArguments() {
+  private UnsplitArgumentsResult unsplitArguments() {
     if (mode.is4Bit()) {
-      return false;
+      return null;
     }
-    boolean argumentRegisterUnsplit = false;
+    Reference2IntMap<LiveIntervals> originalRegisterAssignment = new Reference2IntOpenHashMap<>();
+    originalRegisterAssignment.defaultReturnValue(NO_REGISTER);
     for (Value current = firstArgumentValue;
         current != null;
         current = current.getNextConsecutive()) {
       LiveIntervals intervals = current.getLiveIntervals();
+      int conservativeRealRegisterEnd = realRegisterNumberFromAllocated(intervals.getRegisterEnd());
       assert !mode.hasRegisterConstraint(intervals)
           || (mode.is8BitRefinement()
               && intervals.getRegisterEnd() < numberOf4BitArgumentRegisters);
-      boolean canUseArgumentRegister = true;
-      boolean couldUseArgumentRegister = true;
       for (LiveIntervals child : intervals.getSplitChildren()) {
-        int registerConstraint = child.getRegisterLimit();
-        if (registerConstraint < Constants.U16BIT_MAX) {
-          couldUseArgumentRegister = false;
-
-          if (registerConstraint < highestUsedRegister()) {
-            canUseArgumentRegister = false;
-            break;
-          }
-        }
-      }
-      if (canUseArgumentRegister && !couldUseArgumentRegister) {
-        // Only return true if there is a constrained use where it turns out that we can use the
-        // original argument register. This way we will not unnecessarily redo move insertion.
-        argumentRegisterUnsplit = true;
-        for (LiveIntervals child : intervals.getSplitChildren()) {
+        if (!child.isInvokeRangeIntervals()
+            && conservativeRealRegisterEnd <= child.getRegisterLimit()
+            && child.getRegister() != intervals.getRegister()) {
+          originalRegisterAssignment.put(child, child.getRegister());
           child.clearRegisterAssignment();
           child.setRegister(intervals.getRegister());
-          child.setSpilled(false);
+          // If the child could be spilled then we would need to unset it here + update
+          // UnsplitArgumentsResult#revertPartial to account for this.
+          assert !child.isSpilled();
         }
       }
     }
-    return argumentRegisterUnsplit;
+    if (!originalRegisterAssignment.isEmpty()) {
+      return new UnsplitArgumentsResult(this, originalRegisterAssignment);
+    }
+    return null;
   }
 
-  private void removeSpillAndPhiMoves() {
+  void removeSpillAndPhiMoves() {
     for (BasicBlock block : code.blocks) {
       InstructionListIterator it = block.listIterator(code);
       while (it.hasNext()) {
@@ -937,6 +1025,13 @@
     }
   }
 
+  void removeParallelMoveTemporaryRegisters() {
+    if (firstParallelMoveTemporary != NO_REGISTER) {
+      maxRegisterNumber = firstParallelMoveTemporary - 1;
+      firstParallelMoveTemporary = NO_REGISTER;
+    }
+  }
+
   private static boolean isSpillInstruction(Instruction instruction) {
     Value outValue = instruction.outValue();
     if (outValue != null && outValue.isFixedRegisterValue()) {
@@ -951,20 +1046,22 @@
     return false;
   }
 
-  private void clearRegisterAssignments(ArgumentReuseMode mode) {
+  private void clearRegisterAssignments() {
     freeRegisters.clear();
     maxRegisterNumber = -1;
     active.clear();
     expiredHere.clear();
+    firstParallelMoveTemporary = NO_REGISTER;
     inactive.clear();
     unhandled.clear();
     moveExceptionIntervals.clear();
     for (LiveIntervals intervals : liveIntervals) {
-      if (mode.is8BitRefinement() || mode.is8BitRetry() || mode.is16Bit()) {
-        intervals.undoSplits();
+      intervals.undoSplits();
+      if (intervals.hasRegister()) {
         intervals.setSpilled(false);
       }
       intervals.clearRegisterAssignment();
+      intervals.unsetIsInvokeRangeIntervals();
     }
   }
 
@@ -1017,8 +1114,8 @@
     unhandled.addAll(liveIntervals);
 
     processArgumentLiveIntervals();
-    allocateRegistersForMoveExceptionIntervals();
-    splitLiveIntervalsForInvokeRange();
+    boolean hasInvokeRangeLiveIntervals = splitLiveIntervalsForInvokeRange();
+    allocateRegistersForMoveExceptionIntervals(hasInvokeRangeLiveIntervals);
 
     // Go through each unhandled live interval and find a register for it.
     while (!unhandled.isEmpty()) {
@@ -1028,14 +1125,11 @@
       setHintForDestRegOfCheckCast(unhandledInterval);
       setHintToPromote2AddrInstruction(unhandledInterval);
 
-      // If this interval value is the src of an argument move. Fix the registers for the
-      // consecutive arguments now and add hints to the move sources. This looks forward
-      // and propagate hints backwards to avoid many moves in connection with ranged invokes.
-      if (options().getTestingOptions().enableLiveIntervalsSplittingForInvokeRange) {
-        allocateRegistersForInvokeRangeSplits(unhandledInterval);
-      } else {
-        allocateArgumentIntervalsWithSrc(unhandledInterval);
-      }
+      // If this interval value has an invoke/rangerange user, then fix the registers for the
+      // consecutive arguments now and add hints to the live intervals leading up to this
+      // invoke/range. This looks forward and propagate hints backwards to avoid many moves in
+      // connection with ranged invokes.
+      allocateRegistersForInvokeRangeSplits(unhandledInterval);
       if (unhandledInterval.getRegister() != NO_REGISTER) {
         // The value itself is in the chain that has now gotten registers allocated.
         continue;
@@ -1044,15 +1138,13 @@
       advanceStateToLiveIntervals(unhandledInterval);
 
       // Perform the actual allocation.
-      if (unhandledInterval.isLinked() && !unhandledInterval.isArgumentInterval()) {
-        assert !options().getTestingOptions().enableLiveIntervalsSplittingForInvokeRange;
-        allocateLinkedIntervals(unhandledInterval, false);
-      } else if (!allocateSingleInterval(unhandledInterval)) {
+      if (!allocateSingleInterval(unhandledInterval)) {
         return false;
       }
 
       expiredHere.clear();
     }
+    assert invariantsHold(mode);
     return true;
   }
 
@@ -1061,7 +1153,7 @@
         argumentValue != null;
         argumentValue = argumentValue.getNextConsecutive()) {
       LiveIntervals argumentInterval = argumentValue.getLiveIntervals();
-      assert argumentInterval.getRegister() != NO_REGISTER;
+      assert argumentInterval.hasRegister();
       unhandled.remove(argumentInterval);
       if (!mode.hasRegisterConstraint(argumentInterval)) {
         // All the argument intervals are active in the beginning and have preallocated registers.
@@ -1100,7 +1192,7 @@
     }
   }
 
-  private void allocateRegistersForMoveExceptionIntervals() {
+  private void allocateRegistersForMoveExceptionIntervals(boolean hasInvokeRangeLiveIntervals) {
     // We have to be careful when it comes to the register allocated for a move exception
     // instruction. For move exception instructions there is no place to put spill or
     // restore moves. The move exception instruction has to be the first instruction in a
@@ -1109,50 +1201,68 @@
     // When we allow argument reuse we do not allow any splitting, therefore we cannot get into
     // trouble with move exception registers. When argument reuse is disallowed we block a fixed
     // register to be used only by move exception instructions.
-    if (mode.is8Bit() || mode.is16Bit()) {
-      // Force all move exception ranges to start out with the exception in a fixed register.
-      for (BasicBlock block : code.blocks) {
-        Instruction instruction = block.entry();
-        if (instruction.isMoveException()) {
-          LiveIntervals intervals = instruction.outValue().getLiveIntervals();
-          unhandled.remove(intervals);
-          moveExceptionIntervals.add(intervals);
-          intervals.setRegister(getMoveExceptionRegister());
-        }
+    if (mode.is4Bit() && !hasInvokeRangeLiveIntervals) {
+      return;
+    }
+    // Force all move exception ranges to start out with the exception in a fixed register.
+    for (BasicBlock block : code.blocks(block -> block.entry().isMoveException())) {
+      MoveException moveException = block.entry().asMoveException();
+      LiveIntervals intervals = moveException.outValue().getLiveIntervals();
+      if (intervals.getValue().hasAnyUsers()) {
+        LiveIntervals split = intervals.splitAfter(intervals.getValue().getDefinition());
+        unhandled.add(split);
       }
-      if (hasDedicatedMoveExceptionRegister()) {
-        int moveExceptionRegister = getMoveExceptionRegister();
-        assert moveExceptionRegister == maxRegisterNumber + 1;
-        increaseCapacity(moveExceptionRegister, true);
+      if (intervals.getStart() < moveException.getNumber()) {
+        intervals = intervals.splitBefore(moveException);
+      } else {
+        unhandled.remove(intervals);
       }
-      // Split their live ranges which will force another register if used.
-      for (LiveIntervals intervals : moveExceptionIntervals) {
-        if (intervals.getUses().size() > 1) {
-          LiveIntervals split =
-              intervals.splitBefore(intervals.getFirstUse() + INSTRUCTION_NUMBER_DELTA);
-          unhandled.add(split);
-        }
-      }
-      for (LiveIntervals intervals : moveExceptionIntervals) {
-        assert intervals.getRegisterLimit() == Constants.U8BIT_MAX;
-      }
+      moveExceptionIntervals.add(intervals);
+      intervals.setRegister(getMoveExceptionRegister());
+    }
+    if (hasDedicatedMoveExceptionRegister()) {
+      int moveExceptionRegister = getMoveExceptionRegister();
+      assert moveExceptionRegister == maxRegisterNumber + 1;
+      increaseCapacity(moveExceptionRegister, false);
     }
   }
 
-  private void splitLiveIntervalsForInvokeRange() {
-    if (!options().getTestingOptions().enableLiveIntervalsSplittingForInvokeRange) {
-      return;
-    }
+  private boolean splitLiveIntervalsForInvokeRange() {
+    boolean hasInvokeRangeLiveIntervals = false;
     for (LiveIntervals intervals : liveIntervals) {
       Value value = intervals.getValue();
-      for (Invoke invoke : value.<Invoke>uniqueUsers(this::isInvokeRange)) {
+      for (Invoke invoke : value.<Invoke>uniqueUsers(this::needsInvokeRangeLiveIntervals)) {
         LiveIntervals overlappingIntervals = intervals.getSplitCovering(invoke.getNumber());
-        LiveIntervals invokeRangeIntervals = overlappingIntervals.splitBefore(invoke);
-        if (invoke.getNumber() < invokeRangeIntervals.getEnd()) {
-          invokeRangeIntervals.splitAfter(invoke);
+        LiveIntervals invokeRangeIntervals;
+        if (overlappingIntervals.getStart() == toGapPosition(invoke.getNumber())) {
+          invokeRangeIntervals = overlappingIntervals;
+        } else {
+          invokeRangeIntervals = overlappingIntervals.splitBefore(invoke);
+          unhandled.add(invokeRangeIntervals);
         }
+        invokeRangeIntervals.setIsInvokeRangeIntervals();
+        if (invoke.getNumber() + 1 < invokeRangeIntervals.getEnd()) {
+          LiveIntervals successorIntervals = invokeRangeIntervals.splitAfter(invoke);
+          unhandled.add(successorIntervals);
+        }
+        hasInvokeRangeLiveIntervals = true;
       }
     }
+    return hasInvokeRangeLiveIntervals;
+  }
+
+  private boolean needsInvokeRangeLiveIntervals(Instruction instruction) {
+    Invoke invoke = instruction.asInvoke();
+    if (invoke == null || invoke.requiredArgumentRegisters() <= 5) {
+      return false;
+    }
+    if (argumentsAreAlreadyLinked(invoke)
+        && Iterables.all(
+            invoke.arguments(),
+            argument -> isPinnedArgumentRegister(argument.getLiveIntervals()))) {
+      return false;
+    }
+    return true;
   }
 
   private void advanceStateToLiveIntervals(LiveIntervals unhandledInterval) {
@@ -1200,7 +1310,7 @@
   }
 
   private boolean invariantsHold(ArgumentReuseMode mode) {
-    TreeSet<Integer> computedFreeRegisters = new TreeSet<>();
+    IntSortedSet computedFreeRegisters = new IntRBTreeSet();
     for (int register = 0; register <= maxRegisterNumber; ++register) {
       computedFreeRegisters.add(register);
     }
@@ -1212,22 +1322,18 @@
             computedFreeRegisters.remove(register);
           });
     }
-    if (mode.is8Bit() || mode.is16Bit()) {
-      // Each time an argument interval is active, we currently require that it is present in its
-      // original, incoming argument register.
-      for (LiveIntervals activeIntervals : active) {
-        if (activeIntervals.isArgumentInterval()
-            && activeIntervals != activeIntervals.getSplitParent()) {
-          LiveIntervals parent = activeIntervals.getSplitParent();
-          if (parent.getRegister() != activeIntervals.getRegister()) {
-            activeIntervals
-                .getSplitParent()
-                .forEachRegister(
-                    register -> {
-                      assert computedFreeRegisters.contains(register);
-                      computedFreeRegisters.remove(register);
-                    });
-          }
+    // All active argument intervals that are pinned must be present in its original, incoming
+    // argument register.
+    for (LiveIntervals activeIntervals : active) {
+      if (isPinnedArgumentRegister(activeIntervals)) {
+        assert !mode.is4Bit() || activeIntervals.getValue().isThis();
+        LiveIntervals parent = activeIntervals.getSplitParent();
+        if (parent.getRegister() != activeIntervals.getRegister()) {
+          parent.forEachRegister(
+              register -> {
+                assert computedFreeRegisters.contains(register);
+                computedFreeRegisters.remove(register);
+              });
         }
       }
     }
@@ -1265,14 +1371,17 @@
     for (Value argumentValue = firstArgumentValue;
         argumentValue != null;
         argumentValue = argumentValue.getNextConsecutive()) {
-      assert !interval.hasConflictingRegisters(argumentValue.getLiveIntervals())
-          || !argumentValue.getLiveIntervals().anySplitOverlaps(interval);
+      LiveIntervals argumentIntervals = argumentValue.getLiveIntervals();
+      assert interval.getSplitParent() == argumentIntervals
+          || !isPinnedArgumentRegister(argumentIntervals)
+          || !interval.hasConflictingRegisters(argumentIntervals)
+          || !argumentIntervals.anySplitOverlaps(interval);
     }
     return true;
   }
 
   private void setHintForDestRegOfCheckCast(LiveIntervals unhandledInterval) {
-    if (unhandledInterval.getHint() != null) {
+    if (unhandledInterval.hasHint()) {
       return;
     }
     Value value = unhandledInterval.getValue();
@@ -1292,7 +1401,7 @@
    * that is the left interval or the right interval if possible when intervals do not overlap.
    */
   private void setHintToPromote2AddrInstruction(LiveIntervals unhandledInterval) {
-    if (unhandledInterval.getHint() != null) {
+    if (unhandledInterval.hasHint()) {
       return;
     }
     Value value = unhandledInterval.getValue();
@@ -1320,90 +1429,21 @@
    * allocated and have been moved from unhandled to inactive. The move sources have their hints
    * updated. The rest of the register allocation state is unchanged.
    */
-  // TODO(b/270398965): Replace LinkedList.
   @SuppressWarnings("JdkObsolete")
-  private void allocateArgumentIntervalsWithSrc(LiveIntervals srcInterval) {
-    Value value = srcInterval.getValue();
-    for (Instruction instruction : value.uniqueUsers()) {
-      // If there is a move user that is an argument move, we allocate the consecutive
-      // registers for the argument intervals and propagate the selected registers back as
-      // hints to the sources.
-      if (instruction.isMove() && instruction.asMove().dest().isLinked()) {
-        Move move = instruction.asMove();
-        Value dest = move.dest();
-        LiveIntervals destIntervals = dest.getLiveIntervals();
-        if (destIntervals.getRegister() == NO_REGISTER) {
-          // Save the current register allocation state so we can restore it at the end.
-          TreeSet<Integer> savedFreeRegisters = new TreeSet<>(freeRegisters);
-          int savedMaxRegisterNumber = maxRegisterNumber;
-          List<LiveIntervals> savedInactive = new LinkedList<>(inactive);
-
-          // Add all the active intervals to the inactive set. When allocating linked intervals we
-          // check all inactive intervals and exclude the registers for overlapping inactive
-          // intervals.
-          for (LiveIntervals active : active) {
-            // TODO(ager): We could allow the use of all the currently active registers for the
-            // ranged invoke (by adding the registers for all the active intervals to freeRegisters
-            // here). That could lead to lower register pressure. However, it would also often mean
-            // that we cannot allocate the right argument register to the current unhandled
-            // interval. Size measurements on GMSCore indicate that blocking the current active
-            // registers works the best for code size.
-            if (active.isArgumentInterval()) {
-              // Allow the ranged invoke to use argument registers if free. This improves register
-              // allocation for bridge methods that forwards all of their arguments after check-cast
-              // checks on their types.
-              freeOccupiedRegistersForIntervals(active);
-            }
-            inactive.add(active);
-          }
-
-          // Allocate the argument intervals.
-          unhandled.remove(destIntervals);
-          // Since we are going to do a look-ahead, there may be argument live interval splits,
-          // which are currently unhandled, but would be inactive at the invoke-range instruction.
-          // Thus, the implementation of allocateLinkedIntervals needs to exclude the argument
-          // registers for which there exists a split that overlaps with one of the inputs to the
-          // invoke-range instruction. We handle this situation by setting the following flag.
-          boolean excludeUnhandledOverlappingArgumentIntervals = !mode.is4Bit();
-          unhandled.add(srcInterval);
-          allocateLinkedIntervals(destIntervals, excludeUnhandledOverlappingArgumentIntervals);
-          active.remove(destIntervals);
-          unhandled.remove(srcInterval);
-          // Restore the register allocation state.
-          freeRegisters = savedFreeRegisters;
-          // In case maxRegisterNumber has changed, update freeRegisters.
-          for (int i = savedMaxRegisterNumber + 1; i <= maxRegisterNumber; i++) {
-            freeRegisters.add(i);
-          }
-
-          inactive = savedInactive;
-          // Move all the argument intervals to the inactive set.
-          LiveIntervals current = destIntervals.getStartOfConsecutive();
-          while (current != null) {
-            assert !inactive.contains(current);
-            assert !active.contains(current);
-            assert !unhandled.contains(current);
-            inactive.add(current);
-            current = current.getNextConsecutive();
-          }
-        }
-      }
-    }
-  }
-
   private void allocateRegistersForInvokeRangeSplits(LiveIntervals unhandledIntervals) {
-    // Since we are going to do a look-ahead, there may be argument live interval splits,
-    // which are currently unhandled, but would be inactive at the invoke-range instruction.
-    // Thus, the implementation of allocateLinkedIntervals needs to exclude the argument
-    // registers for which there exists a split that overlaps with one of the inputs to the
-    // invoke-range instruction. We handle this situation by setting the following flag.
-    boolean excludeUnhandledOverlappingArgumentIntervals = !mode.is4Bit();
-
     Value value = unhandledIntervals.getValue();
-    for (Invoke invoke : value.<Invoke>uniqueUsers(this::isInvokeRange)) {
+    for (Invoke invoke : value.<Invoke>uniqueUsers(this::needsInvokeRangeLiveIntervals)) {
       LiveIntervals overlappingIntervals =
           unhandledIntervals.getSplitParent().getSplitCovering(invoke);
-      if (overlappingIntervals.getRegister() != NO_REGISTER) {
+      if (overlappingIntervals.hasRegister()) {
+        assert invoke.arguments().stream()
+            .allMatch(
+                invokeArgument -> {
+                  LiveIntervals overlappingInvokeArgumentIntervals =
+                      invokeArgument.getLiveIntervals().getSplitCovering(invoke);
+                  assert overlappingInvokeArgumentIntervals.hasRegister();
+                  return true;
+                });
         continue;
       }
       List<LiveIntervals> intervalsList =
@@ -1412,105 +1452,146 @@
               invokeArgument -> {
                 LiveIntervals overlappingInvokeArgumentIntervals =
                     invokeArgument.getLiveIntervals().getSplitCovering(invoke);
-                assert overlappingIntervals.getRegister() == NO_REGISTER;
-                assert overlappingIntervals.getStart() == invoke.getNumber() - 1;
-                assert overlappingIntervals.getEnd() == invoke.getNumber();
+                assert !overlappingInvokeArgumentIntervals.hasRegister();
+                assert overlappingInvokeArgumentIntervals.getStart() == invoke.getNumber() - 1;
+                assert overlappingInvokeArgumentIntervals.getEnd() == invoke.getNumber()
+                    || overlappingInvokeArgumentIntervals.getEnd() == invoke.getNumber() + 1;
                 return overlappingInvokeArgumentIntervals;
               });
-      allocateLinkedIntervals(
-          overlappingIntervals, excludeUnhandledOverlappingArgumentIntervals, intervalsList);
-    }
-  }
 
-  private void allocateLinkedIntervals(
-      LiveIntervals unhandledInterval, boolean excludeUnhandledOverlappingArgumentIntervals) {
-    List<LiveIntervals> intervalsList = new ArrayList<>();
-    for (LiveIntervals intervals = unhandledInterval.getStartOfConsecutive();
-        intervals != null;
-        intervals = intervals.getNextConsecutive()) {
-      intervalsList.add(intervals);
-    }
-    allocateLinkedIntervals(
-        unhandledInterval, excludeUnhandledOverlappingArgumentIntervals, intervalsList);
-  }
+      // Save the current register allocation state so we can restore it at the end.
+      IntSortedSet savedFreeRegisters = new IntRBTreeSet(freeRegisters);
+      int savedMaxRegisterNumber = maxRegisterNumber;
 
-  private void allocateLinkedIntervals(
-      LiveIntervals unhandledInterval,
-      boolean excludeUnhandledOverlappingArgumentIntervals,
-      List<LiveIntervals> intervalsList) {
-    LiveIntervals start = ListUtils.first(intervalsList);
-
-    // Exclude the registers that overlap the start of one of the live ranges we are
-    // going to assign registers to now.
-    IntSet excludedRegisters = new IntArraySet();
-    for (LiveIntervals inactiveIntervals : inactive) {
-      if (Iterables.any(intervalsList, inactiveIntervals::overlaps)) {
-        excludeRegistersForInterval(inactiveIntervals, excludedRegisters);
-      }
-    }
-    if (excludeUnhandledOverlappingArgumentIntervals) {
-      // Exclude the argument registers for which there exists a split that overlaps with one of
-      // the inputs to the invoke-range instruction.
-      for (Value argument = firstArgumentValue;
-          argument != null;
-          argument = argument.getNextConsecutive()) {
-        LiveIntervals argumentLiveIntervals = argument.getLiveIntervals();
-        if (liveIntervalsHasUnhandledSplitOverlappingAnyOf(argumentLiveIntervals, intervalsList)) {
-          excludeRegistersForInterval(argumentLiveIntervals, excludedRegisters);
+      // Simulate adding all the active intervals to the inactive set by blocking their register if
+      // they overlap with any of the invoke/range intervals.
+      for (LiveIntervals active : active) {
+        // We could allow the use of all the currently active registers for the ranged invoke (by
+        // adding the registers for all the active intervals to freeRegisters here). That could lead
+        // to lower register pressure. However, it would also often mean that we cannot allocate the
+        // right argument register to the current unhandled interval. Size measurements on GMSCore
+        // indicate that blocking the current active registers works the best for code size.
+        if (Iterables.any(intervalsList, active::overlaps)) {
+          excludeRegistersForInterval(active);
+        } else if (active.isArgumentInterval()) {
+          // Allow the ranged invoke to use argument registers if free. This improves register
+          // allocation for bridge methods that forwards all of their arguments after check-cast
+          // checks on their types.
+          freeOccupiedRegistersForIntervals(active);
         }
       }
+
+      unhandled.removeAll(intervalsList);
+      allocateLinkedIntervals(intervalsList, invoke);
+
+      // Restore the register allocation state.
+      freeRegisters = savedFreeRegisters;
+      // In case maxRegisterNumber has changed, update freeRegisters.
+      for (int i = savedMaxRegisterNumber + 1; i <= maxRegisterNumber; i++) {
+        freeRegisters.add(i);
+      }
+      // Move all the argument intervals to the inactive set.
+      inactive.addAll(intervalsList);
     }
-    // Exclude move exception register if the first interval overlaps a move exception interval.
-    // It is not necessary to check the remaining consecutive intervals, since we always use
-    // register 0 (after remapping) for the argument register.
-    if (hasDedicatedMoveExceptionRegister()) {
-      boolean canUseMoveExceptionRegisterForLinkedIntervals =
-          isDedicatedMoveExceptionRegisterInFirstLocalRegister()
-              && !overlapsMoveExceptionInterval(start);
-      if (!canUseMoveExceptionRegisterForLinkedIntervals
-          && freeRegisters.remove(getMoveExceptionRegister())) {
-        excludedRegisters.add(getMoveExceptionRegister());
+  }
+
+  private void allocateLinkedIntervals(List<LiveIntervals> intervalsList, Invoke invoke) {
+    LiveIntervals start = ListUtils.first(intervalsList);
+
+    boolean consecutiveArguments =
+        IterableUtils.allWithPrevious(
+            intervalsList,
+            (current, previous) ->
+                previous == null
+                    || current.getSplitParent().getPreviousConsecutive()
+                        == previous.getSplitParent());
+    boolean consecutivePinnedArguments =
+        consecutiveArguments && Iterables.all(intervalsList, this::isPinnedArgumentRegister);
+
+    int nextRegister;
+    if (consecutivePinnedArguments) {
+      // We can use the arguments from their input registers.
+      nextRegister = start.getSplitParent().getRegister();
+    } else {
+      // Ensure that there is a free register for the out value (or two consecutive registers if
+      // wide).
+      int numberOfRegisters = getNumberOfRequiredRegisters(intervalsList);
+      int numberOfOutRegisters = invoke.hasOutValue() ? invoke.outValue().requiredRegisters() : 0;
+      if (numberOfOutRegisters > 0
+          && numberOfRegisters + numberOfOutRegisters - 1 > Constants.U4BIT_MAX) {
+        int firstLocalRegister = numberOfArgumentRegisters;
+        if (hasDedicatedMoveExceptionRegister()
+            && isDedicatedMoveExceptionRegisterInFirstLocalRegister()) {
+          firstLocalRegister++;
+        }
+        ensureCapacity(firstLocalRegister + numberOfOutRegisters - 1);
+        for (int i = 0; i < numberOfOutRegisters; i++) {
+          freeRegisters.remove(firstLocalRegister + i);
+        }
+      }
+
+      // Exclude the registers that overlap the start of one of the live ranges we are going to
+      // assign registers to now.
+      for (LiveIntervals inactiveIntervals : inactive) {
+        if (Iterables.any(intervalsList, inactiveIntervals::overlaps)) {
+          excludeRegistersForInterval(inactiveIntervals);
+        }
+      }
+
+      if (consecutiveArguments
+          && registerRangeIsFree(start.getSplitParent().getRegister(), numberOfRegisters)) {
+        // For consecutive arguments we always to use the input argument registers, if they are
+        // free.
+        nextRegister = start.getSplitParent().getRegister();
+      } else {
+        // Exclude the pinned argument registers for which there exists a split that overlaps with
+        // one of the inputs to the invoke-range instruction.
+        for (Value argument = firstArgumentValue;
+            argument != null;
+            argument = argument.getNextConsecutive()) {
+          LiveIntervals argumentLiveIntervals = argument.getLiveIntervals();
+          if (isPinnedArgumentRegister(argumentLiveIntervals)
+              && liveIntervalsOverlappingAnyOf(argumentLiveIntervals, intervalsList)) {
+            excludeRegistersForInterval(argumentLiveIntervals);
+          }
+        }
+        // Exclude move exception register if the first interval overlaps a move exception interval.
+        // It is not necessary to check the remaining consecutive intervals, since we always use
+        // register 0 (after remapping) for the argument register.
+        if (hasDedicatedMoveExceptionRegister()) {
+          boolean canUseMoveExceptionRegisterForLinkedIntervals =
+              isDedicatedMoveExceptionRegisterInFirstLocalRegister()
+                  && (!start.isLiveAtMoveExceptionEntry() || !overlapsMoveExceptionInterval(start));
+          if (!canUseMoveExceptionRegisterForLinkedIntervals) {
+            freeRegisters.remove(getMoveExceptionRegister());
+          }
+        }
+        // Select registers.
+        nextRegister = getFreeConsecutiveRegisters(numberOfRegisters);
       }
     }
-    // Select registers.
-    int numberOfRegisters = getNumberOfRequiredRegisters(intervalsList);
-    int nextRegister = getFreeConsecutiveRegisters(numberOfRegisters);
+
+    // Assign registers.
     for (LiveIntervals current : intervalsList) {
       current.setRegister(nextRegister);
       assert verifyRegisterAssignmentNotConflictingWithArgument(current);
-      // Propagate hints to the move sources.
-      Value value = current.getValue();
-      if (value.isDefinedByInstructionSatisfying(Instruction::isMove)) {
-        Move move = value.getDefinition().asMove();
-        LiveIntervals intervals = move.src().getLiveIntervals();
-        intervals.setHint(current, unhandled);
-      }
-      if (current != unhandledInterval) {
-        // Only the start of unhandledInterval has been reached at this point. All other live
-        // intervals in the chain have been assigned registers but their start has not yet been
-        // reached. Therefore, they belong in the inactive set.
-        unhandled.remove(current);
-        inactive.add(current);
-      }
       nextRegister += current.requiredRegisters();
     }
 
-    assert unhandledInterval.getRegister() != NO_REGISTER;
-    takeFreeRegistersForIntervals(unhandledInterval);
-    active.add(unhandledInterval);
-    // Include the registers for inactive ranges that we had to exclude for this allocation.
-    freeRegisters.addAll(excludedRegisters);
-
-    if (options().getTestingOptions().enableLiveIntervalsSplittingForInvokeRange) {
-      for (LiveIntervals intervals : intervalsList) {
-        LiveIntervals parentIntervals = intervals.getSplitParent();
-        parentIntervals.setHint(intervals, unhandled);
-        for (LiveIntervals siblingIntervals : parentIntervals.getSplitChildren()) {
-          if (siblingIntervals != intervals && !siblingIntervals.hasRegister()) {
-            siblingIntervals.setHint(intervals, unhandled);
-          }
+    // Add hints.
+    for (LiveIntervals intervals : intervalsList) {
+      LiveIntervals parentIntervals = intervals.getSplitParent();
+      parentIntervals.setHint(intervals, unhandled);
+      for (LiveIntervals siblingIntervals : parentIntervals.getSplitChildren()) {
+        if (siblingIntervals != intervals && !siblingIntervals.hasRegister()) {
+          siblingIntervals.setHint(intervals, unhandled);
         }
       }
+      Value value = intervals.getValue();
+      if (value.isDefinedByInstructionSatisfying(Instruction::isMove)) {
+        Move move = value.getDefinition().asMove();
+        move.src().getLiveIntervals().setHint(intervals, unhandled);
+      }
     }
   }
 
@@ -1522,13 +1603,13 @@
     return requiredRegisters;
   }
 
-  // Returns true if intervals has an unhandled split, which overlaps with chain or any of its
-  // consecutives.
-  private boolean liveIntervalsHasUnhandledSplitOverlappingAnyOf(
+  // Returns true if intervals has a split, which overlaps with any of the live intervals in the
+  // given list.
+  private boolean liveIntervalsOverlappingAnyOf(
       LiveIntervals intervals, List<LiveIntervals> intervalsList) {
     assert intervals == intervals.getSplitParent();
     for (LiveIntervals split : intervals.getSplitChildren()) {
-      if (unhandled.contains(split) && Iterables.any(intervalsList, split::overlaps)) {
+      if (Iterables.any(intervalsList, split::overlaps)) {
         return true;
       }
     }
@@ -1554,7 +1635,7 @@
       return intervals.getSplitParent().getRegister();
     }
 
-    TreeSet<Integer> previousFreeRegisters = new TreeSet<>(freeRegisters);
+    IntSortedSet previousFreeRegisters = new IntRBTreeSet(freeRegisters);
     int previousMaxRegisterNumber = maxRegisterNumber;
     freeRegisters.removeAll(expiredHere);
     if (excludedRegisters != null) {
@@ -1657,7 +1738,8 @@
 
     // Check for overlap with the move exception interval.
     boolean overlapsMoveExceptionInterval =
-        hasDedicatedMoveExceptionRegister()
+        intervals.isLiveAtMoveExceptionEntry()
+            && hasDedicatedMoveExceptionRegister()
             && (register == getMoveExceptionRegister()
                 || (intervals.getType().isWide() && register + 1 == getMoveExceptionRegister()))
             && overlapsMoveExceptionInterval(intervals);
@@ -1712,7 +1794,7 @@
 
   // Is the array-get array register the same as the first register we are
   // allocating for the result?
-  private boolean isArrayGetArrayRegister(LiveIntervals intervals, int register) {
+  private boolean isArrayGetArrayRegister(int register, LiveIntervals intervals) {
     assert needsArrayGetWideWorkaround(intervals);
     Value array = intervals.getValue().definition.asArrayGet().array();
     int arrayReg =
@@ -1752,7 +1834,7 @@
 
   // Is one of the cmp-long argument registers the same as the register we are
   // allocating for the result?
-  private boolean isSingleResultOverlappingLongOperands(LiveIntervals intervals, int register) {
+  private boolean isSingleResultOverlappingLongOperands(int register, LiveIntervals intervals) {
     assert needsSingleResultOverlappingLongOperandsWorkaround(intervals);
     if (intervals.getValue().definition.isCmp()) {
       Value left = intervals.getValue().definition.asCmp().leftValue();
@@ -1818,7 +1900,7 @@
   }
 
   private boolean isLongResultOverlappingLongOperands(
-      LiveIntervals unhandledInterval, int register) {
+      int register, LiveIntervals unhandledInterval) {
     assert needsLongResultOverlappingLongOperandsWorkaround(unhandledInterval);
     Value left = unhandledInterval.getValue().definition.asBinop().leftValue();
     Value right = unhandledInterval.getValue().definition.asBinop().rightValue();
@@ -1897,7 +1979,7 @@
 
     // Just use the argument register if an argument split has no register constraint. That will
     // avoid move generation for the argument.
-    if (unhandledInterval.isArgumentInterval()) {
+    if (isPinnedArgumentRegister(unhandledInterval)) {
       if (registerConstraint == Constants.U16BIT_MAX
           || (mode.is8Bit() && registerConstraint == Constants.U8BIT_MAX)) {
         int argumentRegister = unhandledInterval.getSplitParent().getRegister();
@@ -2014,7 +2096,19 @@
       freePositions.setBlocked(0);
     }
 
-    if (!mode.is4Bit()) {
+    if (mode.is4Bit()) {
+      // We may block the receiver register.
+      if (firstArgumentValue != null
+          && isPinnedArgumentRegister(firstArgumentValue.getLiveIntervals())) {
+        firstArgumentValue.getLiveIntervals().forEachRegister(freePositions::setBlocked);
+      }
+      // But not any of the other argument registers.
+      for (Value argument = firstArgumentValue;
+          argument != null;
+          argument = argument.getNextConsecutive()) {
+        assert !isPinnedArgumentRegister(argument.getLiveIntervals()) || argument.isThis();
+      }
+    } else {
       // Generally argument reuse is not allowed and we block all the argument registers so that
       // arguments are never free.
       //
@@ -2055,10 +2149,12 @@
     // place to put a spill move (because the move exception instruction has to be the
     // first instruction in the handler block).
     if (hasDedicatedMoveExceptionRegister()) {
-      if (unhandledInterval.getRegisterLimit() == Constants.U4BIT_MAX
+      if (!mode.is4Bit()
+          && unhandledInterval.getRegisterLimit() == Constants.U4BIT_MAX
           && isDedicatedMoveExceptionRegisterInLastLocalRegister()) {
         freePositions.setBlocked(getMoveExceptionRegister());
-      } else if (overlapsMoveExceptionInterval(unhandledInterval)) {
+      } else if (unhandledInterval.isLiveAtMoveExceptionEntry()
+          && overlapsMoveExceptionInterval(unhandledInterval)) {
         int moveExceptionRegister = getMoveExceptionRegister();
         if (moveExceptionRegister <= registerConstraint) {
           freePositions.setBlocked(moveExceptionRegister);
@@ -2107,17 +2203,50 @@
 
   // Attempt to use the register hint for the unhandled interval in order to avoid generating
   // moves.
-  private boolean useRegisterHint(LiveIntervals unhandledInterval, int registerConstraint,
-      RegisterPositions freePositions, boolean needsRegisterPair) {
+  private boolean useRegisterHint(
+      LiveIntervals unhandledInterval,
+      int registerConstraint,
+      RegisterPositions freePositions,
+      boolean needsRegisterPair) {
     // If the unhandled interval has a hint we give it that register if it is available without
     // spilling. For phis we also use the hint before looking at the operand registers. The
     // phi could have a hint from an argument moves which it seems more important to honor in
     // practice.
-    Integer hint = unhandledInterval.getHint();
-    if (hint != null) {
-      if (tryHint(unhandledInterval, registerConstraint, freePositions, needsRegisterPair, hint)) {
-        return true;
-      }
+    IntSet triedHints = new IntArraySet();
+    if (unhandledInterval.hasHint()
+        && triedHints.add(unhandledInterval.getHint())
+        && tryHint(
+            unhandledInterval,
+            registerConstraint,
+            freePositions,
+            needsRegisterPair,
+            unhandledInterval.getHint())) {
+      return true;
+    }
+
+    LiveIntervals previousSplit = unhandledInterval.getPreviousSplit();
+    if (previousSplit != null
+        && triedHints.add(previousSplit.getRegister())
+        && tryHint(
+            unhandledInterval,
+            registerConstraint,
+            freePositions,
+            needsRegisterPair,
+            previousSplit.getRegister())) {
+      return true;
+    }
+
+    LiveIntervals nextSplit = unhandledInterval.getNextSplit();
+    if (nextSplit != null
+        && nextSplit.hasRegister()
+        && triedHints.add(nextSplit.getRegister())
+        && tryHint(
+            unhandledInterval,
+            registerConstraint,
+            freePositions,
+            needsRegisterPair,
+            nextSplit.getRegister())) {
+      return true;
     }
 
     // If there is no hint or it cannot be applied we search for a good register for phis using
@@ -2131,12 +2260,11 @@
       for (int i = 0; i < operands.size(); i++) {
         LiveIntervals intervals = operands.get(i).getLiveIntervals();
         if (intervals.hasSplits()) {
-          BasicBlock pred = phi.getBlock().getPredecessors().get(i);
+          BasicBlock pred = phi.getBlock().getPredecessor(i);
           intervals = intervals.getSplitCovering(pred.exit().getNumber());
         }
-        int operandRegister = intervals.getRegister();
-        if (operandRegister != NO_REGISTER) {
-          map.add(operandRegister);
+        if (intervals.hasRegister()) {
+          map.add(intervals.getRegister());
         }
       }
       for (Multiset.Entry<Integer> entry : Multisets.copyHighestCountFirst(map).entrySet()) {
@@ -2165,7 +2293,7 @@
       return false;
     }
     if (freePositions.isBlocked(register, needsRegisterPair)) {
-      return tryAllocateBlockedHint(unhandledInterval);
+      return tryAllocateBlockedHint(unhandledInterval, register);
     }
     int freePosition = freePositions.get(register);
     if (needsRegisterPair) {
@@ -2176,39 +2304,80 @@
     }
     // Check for overlapping long registers issue.
     if (needsLongResultOverlappingLongOperandsWorkaround(unhandledInterval)
-        && isLongResultOverlappingLongOperands(unhandledInterval, register)) {
+        && isLongResultOverlappingLongOperands(register, unhandledInterval)) {
       return false;
     }
     // Check for aget-wide bug in recent Art VMs.
     if (needsArrayGetWideWorkaround(unhandledInterval)
-        && isArrayGetArrayRegister(unhandledInterval, register)) {
+        && isArrayGetArrayRegister(register, unhandledInterval)) {
       return false;
     }
     assignFreeRegisterToUnhandledInterval(unhandledInterval, register);
     return true;
   }
 
-  private boolean tryAllocateBlockedHint(LiveIntervals unhandledInterval) {
+  private boolean tryAllocateBlockedHint(LiveIntervals unhandledInterval, int candidate) {
     if (!options().getTestingOptions().enableRegisterHintsForBlockedRegisters) {
       return false;
     }
     LiveIntervals nextSplit = unhandledInterval.getNextSplit();
-    int candidate = nextSplit != null ? nextSplit.getRegister() : NO_REGISTER;
-    if (candidate == NO_REGISTER || unhandledInterval.getEnd() != nextSplit.getStart()) {
+    int alternativeHint = nextSplit != null ? nextSplit.getRegister() : NO_REGISTER;
+    if (candidate != alternativeHint) {
       return false;
     }
-    // Find the value occupying the register of interest.
+    if (needsArrayGetWideWorkaround(unhandledInterval)
+        || needsLongResultOverlappingLongOperandsWorkaround(unhandledInterval)) {
+      return false;
+    }
+    if (isArgumentRegister(candidate)) {
+      for (Value argument = firstArgumentValue;
+          argument != null;
+          argument = argument.getNextConsecutive()) {
+        if (isPinnedArgument(argument)) {
+          return false;
+        }
+      }
+    }
+    if (isDedicatedMoveExceptionRegister(candidate)) {
+      return false;
+    }
+    if (!getLiveIntervalsWithRegister(
+            inactive, unhandledInterval, candidate, unhandledInterval::overlaps)
+        .isEmpty()) {
+      return false;
+    }
+    // Find the value occupying the register of interest. Note that the current live intervals may
+    // be blocked by an inactive (overlapping) live intervals.
     Collection<LiveIntervals> blockingIntervals =
-        getLiveIntervalsWithRegister(unhandledInterval, candidate);
+        getLiveIntervalsWithRegister(active, unhandledInterval, candidate);
     assert !blockingIntervals.isEmpty();
-    if (blockingIntervals.size() > 1) {
+    if (blockingIntervals.size() != 1) {
+      // Validate that not finding any blocking live intervals means the current live intervals is
+      // blocked by an inactive live intervals.
       return false;
     }
     LiveIntervals blockingInterval = blockingIntervals.iterator().next();
+    if (unhandledInterval.getType().isWide()) {
+      if (blockingInterval.getRegister() != candidate || !blockingInterval.getType().isWide()) {
+        // Conservatively bail out. It could be that the low-half of the register pair is blocked by
+        // an inactive live intervals.
+        return false;
+      }
+    }
+    if (isArgumentRegister(candidate) && isPinnedArgumentRegister(blockingInterval)) {
+      return false;
+    }
+    if (toInstructionPosition(blockingInterval.getStart())
+        == toInstructionPosition(unhandledInterval.getStart())) {
+      return false;
+    }
     if (hasConstrainedUseInRange(
         blockingInterval, unhandledInterval.getStart(), unhandledInterval.getEnd())) {
       return false;
     }
+    if (!expiredHere.isEmpty()) {
+      return false;
+    }
     LiveIntervals split = blockingInterval.splitBefore(unhandledInterval.getStart());
     freeOccupiedRegistersForIntervals(blockingInterval);
     assignFreeRegisterToUnhandledInterval(unhandledInterval, blockingInterval.getRegister());
@@ -2217,12 +2386,20 @@
     return true;
   }
 
-  private Collection<LiveIntervals> getLiveIntervalsWithRegister(
-      LiveIntervals unhandledInterval, int register) {
+  private static Collection<LiveIntervals> getLiveIntervalsWithRegister(
+      List<LiveIntervals> intervalsList, LiveIntervals unhandledInterval, int register) {
+    return getLiveIntervalsWithRegister(intervalsList, unhandledInterval, register, alwaysTrue());
+  }
+
+  private static Collection<LiveIntervals> getLiveIntervalsWithRegister(
+      List<LiveIntervals> intervalsList,
+      LiveIntervals unhandledInterval,
+      int register,
+      Predicate<LiveIntervals> predicate) {
     LiveIntervals intervalsWithRegister = null;
     boolean isWide = unhandledInterval.getType().isWide();
-    for (LiveIntervals intervals : active) {
-      if (!intervals.usesRegister(register, isWide)) {
+    for (LiveIntervals intervals : intervalsList) {
+      if (!intervals.usesRegister(register, isWide) || !predicate.test(intervals)) {
         continue;
       }
       if (!isWide || intervals.usesBothRegisters(register, register + 1)) {
@@ -2233,6 +2410,9 @@
       }
       intervalsWithRegister = intervals;
     }
+    if (intervalsWithRegister != null) {
+      return Collections.singleton(intervalsWithRegister);
+    }
     return Collections.emptyList();
   }
 
@@ -2257,14 +2437,14 @@
     // phi and do not have hints yet.
     for (Phi phi : value.uniquePhiUsers()) {
       LiveIntervals phiIntervals = phi.getLiveIntervals();
-      if (phiIntervals.getHint() == null) {
+      if (!phiIntervals.hasHint()) {
         phiIntervals.setHint(intervals, unhandled);
         for (int i = 0; i < phi.getOperands().size(); i++) {
           Value operand = phi.getOperand(i);
           LiveIntervals operandIntervals = operand.getLiveIntervals();
           BasicBlock pred = phi.getBlock().getPredecessors().get(i);
           operandIntervals = operandIntervals.getSplitCovering(pred.exit().getNumber());
-          if (operandIntervals.getHint() == null) {
+          if (!operandIntervals.hasHint()) {
             operandIntervals.setHint(intervals, unhandled);
           }
         }
@@ -2343,7 +2523,7 @@
 
   private int handleWorkaround(
       Predicate<LiveIntervals> workaroundNeeded,
-      BiPredicate<LiveIntervals, Integer> workaroundNeededForCandidate,
+      IntObjPredicate<LiveIntervals> workaroundNeededForCandidate,
       int candidate,
       LiveIntervals unhandledInterval,
       int registerConstraint,
@@ -2352,7 +2532,7 @@
       RegisterType type) {
     if (workaroundNeeded.test(unhandledInterval)) {
       int lastCandidate = candidate;
-      while (workaroundNeededForCandidate.test(unhandledInterval, candidate)) {
+      while (workaroundNeededForCandidate.test(candidate, unhandledInterval)) {
         // Make the unusable register unavailable for allocation and try again.
         freePositions.setBlockedTemporarily(candidate);
         candidate =
@@ -2470,7 +2650,8 @@
 
     // Disallow reuse of the move exception register if we have reserved one.
     if (hasDedicatedMoveExceptionRegister()) {
-      if (unhandledInterval.getRegisterLimit() == Constants.U4BIT_MAX
+      if (!mode.is4Bit()
+          && unhandledInterval.getRegisterLimit() == Constants.U4BIT_MAX
           && isDedicatedMoveExceptionRegisterInLastLocalRegister()) {
         usePositions.setBlocked(getMoveExceptionRegister());
       } else if (overlapsMoveExceptionInterval(unhandledInterval)) {
@@ -2480,10 +2661,8 @@
 
     // Treat active and inactive linked argument intervals as pinned. They cannot be given another
     // register at their uses.
-    blockLinkedRegisters(
-        active, unhandledInterval, registerConstraint, usePositions, blockedPositions);
-    blockLinkedRegisters(inactive, unhandledInterval, registerConstraint, usePositions,
-        blockedPositions);
+    blockInvokeRangeIntervals(
+        unhandledInterval, registerConstraint, usePositions, blockedPositions);
 
     // Get the register (pair) that has the highest use position.
     boolean needsRegisterPair = unhandledInterval.getType().isWide();
@@ -2789,31 +2968,42 @@
     }
   }
 
-  private void blockLinkedRegisters(
-      List<LiveIntervals> intervalsList, LiveIntervals interval, int registerConstraint,
-      RegisterPositions usePositions, RegisterPositions blockedPositions) {
-    for (LiveIntervals other : intervalsList) {
-      if (other.isLinked()) {
-        int register = other.getRegister();
-        if (register <= registerConstraint && other.overlaps(interval)) {
-          for (int i = 0; i < other.requiredRegisters(); i++) {
-            if (register + i <= registerConstraint) {
-              int firstUse = other.firstUseAfter(interval.getStart());
-              if (firstUse < blockedPositions.get(register + i)) {
-                blockedPositions.set(register + i, firstUse, other);
-                // If we start blocking registers other than linked arguments, we might need to
-                // explicitly update the use positions as well as blocked positions.
-                assert usePositions.isBlocked(register + i)
-                    || usePositions.get(register + i) <= blockedPositions.get(register + i);
+  private void blockInvokeRangeIntervals(
+      LiveIntervals unhandledInterval,
+      int registerConstraint,
+      RegisterPositions usePositions,
+      RegisterPositions blockedPositions) {
+    // TODO(b/302281605): The only way there can be active invoke-range intervals is if we have a
+    //  live intervals that have been split right before the invoke range instruction. If we had a
+    //  mapping from instruction number to the invoke range instruction, we could find the invoke
+    //  range live intervals directly without scanning all active intervals. Moreover, we could
+    //  avoid checking if the intervals overlap, since they clearly do.
+    for (LiveIntervals intervals : Iterables.concat(active, inactive)) {
+      if (!intervals.isInvokeRangeIntervals()) {
+        continue;
+      }
+      int registerStart = intervals.getRegister();
+      if (registerStart <= registerConstraint && intervals.overlaps(unhandledInterval)) {
+        intervals.forEachRegister(
+            register -> {
+              if (register <= registerConstraint) {
+                int firstUse = intervals.firstUseAfter(unhandledInterval.getStart());
+                if (firstUse < blockedPositions.get(register)) {
+                  blockedPositions.set(register, firstUse, intervals);
+                  // If we start blocking registers other than linked arguments, we might need to
+                  // explicitly update the use positions as well as blocked positions.
+                  assert usePositions.isBlocked(register)
+                      || usePositions.get(register) <= blockedPositions.get(register);
+                }
               }
-            }
-          }
-        }
+            });
       }
     }
   }
 
-  private void insertMoves() {
+  // Returns the number of added parallel move temporary registers.
+  private InsertMovesResult insertMoves() {
+    assert firstParallelMoveTemporary == NO_REGISTER;
     computeRematerializableBits();
 
     SpillMoveSet spillMoves = new SpillMoveSet(this, code, appView);
@@ -2826,7 +3016,7 @@
             split != null;
             split = sortedChildren.poll()) {
           int position = split.getStart();
-          if (!isPinnedArgumentRegister(split)) {
+          if (!canSkipArgumentMove(split)) {
             spillMoves.addSpillOrRestoreMove(toGapPosition(position), split, current);
           }
           current = split;
@@ -2835,8 +3025,14 @@
     }
 
     resolveControlFlow(spillMoves);
-    firstParallelMoveTemporary = maxRegisterNumber + 1;
-    maxRegisterNumber += spillMoves.scheduleAndInsertMoves(maxRegisterNumber + 1);
+    int firstParallelMoveTemporaryRegister = maxRegisterNumber + 1;
+    int numberOfParallelMoveTemporaryRegisters =
+        spillMoves.scheduleAndInsertMoves(firstParallelMoveTemporaryRegister);
+    if (numberOfParallelMoveTemporaryRegisters > 0) {
+      firstParallelMoveTemporary = firstParallelMoveTemporaryRegister;
+      maxRegisterNumber += numberOfParallelMoveTemporaryRegisters;
+    }
+    return new InsertMovesResult(this, numberOfParallelMoveTemporaryRegisters);
   }
 
   private void computeRematerializableBits() {
@@ -2881,7 +3077,7 @@
           LiveIntervals parentInterval = value.getLiveIntervals();
           LiveIntervals fromIntervals = parentInterval.getSplitCovering(fromInstruction);
           LiveIntervals toIntervals = parentInterval.getSplitCovering(toInstruction);
-          if (isPinnedArgumentRegister(toIntervals)) {
+          if (canSkipArgumentMove(toIntervals)) {
             // No need to add resolution moves to pinned argument registers.
             continue;
           }
@@ -2910,19 +3106,43 @@
     }
   }
 
+  public boolean isPinnedArgument(Value value) {
+    return value.isArgument() && isPinnedArgumentRegister(value.getLiveIntervals());
+  }
+
   boolean isPinnedArgumentRegister(LiveIntervals intervals) {
     if (!intervals.isArgumentInterval()) {
       return false;
     }
-    assert intervals.getRegister() != NO_REGISTER;
+    LiveIntervals parentIntervals = intervals.getSplitParent();
+    assert parentIntervals.hasRegister();
+    if (mode.is4Bit()) {
+      // We don't pin argument registers in 4 bit mode, unless we have to.
+      if (options().shouldCompileMethodInDebugMode(code.context())
+          || options().canHaveThisTypeVerifierBug()
+          || options().canHaveThisJitCodeDebuggingBug()) {
+        return parentIntervals.getValue().isThis();
+      }
+      return false;
+    }
+    return true;
+  }
+
+  public boolean isArgumentRegister(int register) {
+    return register < numberOfArgumentRegisters;
+  }
+
+  boolean canSkipArgumentMove(LiveIntervals intervals) {
+    if (!isPinnedArgumentRegister(intervals)) {
+      return false;
+    }
+    assert intervals.hasRegister();
     if (intervals.getRegister() >= numberOfArgumentRegisters) {
       return false;
     }
-    if (mode.is8BitRefinement()) {
-      // An 8 bit argument register could be moved to a 4 bit argument register.
-      if (intervals.getRegister() != intervals.getSplitParent().getRegister()) {
-        return false;
-      }
+    // An argument register could be moved to another argument register.
+    if (intervals.getRegister() != intervals.getSplitParent().getRegister()) {
+      return false;
     }
     return true;
   }
@@ -2976,23 +3196,27 @@
 
   private void computeLiveRanges() {
     computeLiveRanges(appView, code, liveAtEntrySets, liveIntervals);
+    boolean hasMoveException = false;
+    for (BasicBlock block : code.blocks(block -> block.entry().isMoveException())) {
+      for (Value value : liveAtEntrySets.get(block).liveValues) {
+        value.getLiveIntervals().setIsLiveAtMoveExceptionEntry();
+      }
+      hasMoveException = true;
+    }
     // Art VMs before Android M assume that the register for the receiver never changes its value.
     // This assumption is used during verification. Allowing the receiver register to be
     // overwritten can therefore lead to verification errors. If we could be targeting one of these
     // VMs we block the receiver register throughout the method.
     if ((options().canHaveThisTypeVerifierBug() || options().canHaveThisJitCodeDebuggingBug())
-        && !code.method().accessFlags.isStatic()) {
-      for (Instruction instruction : code.entryBlock().getInstructions()) {
-        if (instruction.isArgument() && instruction.outValue().isThis()) {
-          Value thisValue = instruction.outValue();
-          LiveIntervals thisIntervals = thisValue.getLiveIntervals();
-          thisIntervals.getRanges().clear();
-          thisIntervals.addRange(new LiveRange(0, code.getNextInstructionNumber()));
-          for (LiveAtEntrySets values : liveAtEntrySets.values()) {
-            values.liveValues.add(thisValue);
-          }
-          return;
-        }
+        && !code.method().getAccessFlags().isStatic()) {
+      LiveIntervals thisIntervals = firstArgumentValue.getLiveIntervals();
+      thisIntervals.getRanges().clear();
+      thisIntervals.addRange(new LiveRange(0, code.getNextInstructionNumber()));
+      for (LiveAtEntrySets values : liveAtEntrySets.values()) {
+        values.liveValues.add(firstArgumentValue);
+      }
+      if (hasMoveException) {
+        thisIntervals.setIsLiveAtMoveExceptionEntry();
       }
     }
   }
@@ -3029,9 +3253,9 @@
         }
       }
       LinkedHashSetUtils.addAll(live, phiOperands);
-      List<Instruction> instructions = block.getInstructions();
+      int numInstructionsDelta = block.getInstructions().size() * INSTRUCTION_NUMBER_DELTA;
       for (Value value : live) {
-        int end = block.entry().getNumber() + instructions.size() * INSTRUCTION_NUMBER_DELTA;
+        int end = block.entry().getNumber() + numInstructionsDelta;
         // Make sure that phi operands do not overlap the phi live range. The phi operand is
         // not live until the next instruction, but only until the gap before the next instruction
         // where the phi value takes over.
@@ -3040,10 +3264,8 @@
         }
         addLiveRange(value, block, end, liveIntervals, code);
       }
-      InstructionIterator iterator = block.iterator(block.getInstructions().size());
-      while (iterator.hasPrevious()) {
-        Instruction instruction = iterator.previous();
-        Value definition = instruction.outValue();
+      for (Instruction ins = block.getLastInstruction(); ins != null; ins = ins.getPrev()) {
+        Value definition = ins.outValue();
         if (definition != null) {
           // For instructions that define values which have no use create a live range covering
           // the instruction. This will typically be instructions that can have side effects even
@@ -3056,23 +3278,23 @@
             addLiveRange(
                 definition,
                 block,
-                instruction.getNumber() + INSTRUCTION_NUMBER_DELTA - 1,
+                ins.getNumber() + INSTRUCTION_NUMBER_DELTA - 1,
                 liveIntervals,
                 code);
-            assert !code.getConversionOptions().isGeneratingClassFiles() || instruction.isArgument()
+            assert !code.getConversionOptions().isGeneratingClassFiles() || ins.isArgument()
                 : "Arguments should be the only potentially unused local in CF";
           }
           live.remove(definition);
         }
-        for (Value use : instruction.inValues()) {
+        for (Value use : ins.inValues()) {
           if (use.needsRegister()) {
-            assert unconstrainedForCf(instruction.maxInValueRegister(), code);
+            assert unconstrainedForCf(ins.maxInValueRegister(), code);
             if (!live.contains(use)) {
               live.add(use);
-              addLiveRange(use, block, instruction.getNumber(), liveIntervals, code);
+              addLiveRange(use, block, ins.getNumber(), liveIntervals, code);
             }
             if (code.getConversionOptions().isGeneratingDex()) {
-              int inConstraint = instruction.maxInValueRegister();
+              int inConstraint = ins.maxInValueRegister();
               LiveIntervals useIntervals = use.getLiveIntervals();
               // Arguments are always kept in their original, incoming register. For every
               // unconstrained use of an argument we therefore use its incoming register.
@@ -3086,9 +3308,9 @@
               // it in the argument register, the register allocator would use two registers for the
               // argument but in reality only use one.
               boolean isUnconstrainedArgumentUse =
-                  use.isArgument() && inConstraint == Constants.U16BIT_MAX;
+                  use.isArgument() && inConstraint == Constants.U16BIT_MAX && !isInvokeRange(ins);
               if (!isUnconstrainedArgumentUse) {
-                useIntervals.addUse(new LiveIntervalsUse(instruction.getNumber(), inConstraint));
+                useIntervals.addUse(new LiveIntervalsUse(ins.getNumber(), inConstraint));
               }
             }
           }
@@ -3100,24 +3322,20 @@
         // 'r1 <- check-cast r0' maps to 'move r1, r0; check-cast r1' and when that
         // happens r1 could be clobbered on the exceptional edge if r1 initially contained
         // a value that is used in the exceptional code.
-        if (instruction.instructionTypeCanThrow()) {
+        if (ins.instructionTypeCanThrow()) {
           for (Value use : liveAtThrowingInstruction) {
             if (use.needsRegister() && !live.contains(use)) {
               live.add(use);
               addLiveRange(
-                  use,
-                  block,
-                  getLiveRangeEndOnExceptionalFlow(instruction, use),
-                  liveIntervals,
-                  code);
+                  use, block, getLiveRangeEndOnExceptionalFlow(ins, use), liveIntervals, code);
             }
           }
         }
         if (appView.options().debug || code.context().isReachabilitySensitive()) {
           // In debug mode, or if the method is reachability sensitive, extend the live range
           // to cover the full scope of a local variable (encoded as debug values).
-          int number = instruction.getNumber();
-          List<Value> sortedDebugValues = new ArrayList<>(instruction.getDebugValues());
+          int number = ins.getNumber();
+          List<Value> sortedDebugValues = new ArrayList<>(ins.getDebugValues());
           sortedDebugValues.sort(Value::compareTo);
           for (Value use : sortedDebugValues) {
             assert use.needsRegister();
@@ -3250,121 +3468,30 @@
     newArgument.addUser(invoke);
   }
 
-  private void generateArgumentMoves(Invoke invoke, InstructionListIterator insertAt) {
-    // If the invoke instruction require more than 5 registers we link the inputs because they
-    // need to be in consecutive registers.
-    if (invoke.requiredArgumentRegisters() > 5 && !argumentsAreAlreadyLinked(invoke)) {
-      List<Value> arguments = invoke.arguments();
-      Value previous = null;
-
-      PriorityQueue<Move> insertAtDefinition = null;
-      if (invoke.requiredArgumentRegisters() > 16) {
-        insertAtDefinition =
-            new PriorityQueue<>(
-                (x, y) -> x.src().definition.getNumber() - y.src().definition.getNumber());
-
-        // Number the instructions in this basic block such that we can order the moves according
-        // to the positions of the instructions that define the srcs of the moves. Note that this
-        // is a local numbering of the instructions. These instruction numbers will be recomputed
-        // just before the liveness analysis.
-        BasicBlock block = invoke.getBlock();
-        if (block.entry().getNumber() == -1) {
-          block.numberInstructions(0);
-        }
+  private void ensureUniqueArgumentsToInvokeRangeInstructions(
+      Invoke invoke, InstructionListIterator instructionIterator) {
+    Set<Value> seen = Sets.newIdentityHashSet();
+    for (int argumentIndex = 0; argumentIndex < invoke.arguments().size(); argumentIndex++) {
+      Value argument = invoke.getArgument(argumentIndex);
+      if (seen.add(argument)) {
+        continue;
       }
-
-      for (int i = 0; i < arguments.size(); i++) {
-        Value argument = arguments.get(i);
-        Value newArgument = argument;
-        // In debug mode, we have debug instructions that are also moves. Do not generate another
-        // move if there already is a move instruction that we can use. We generate moves if:
-        //
-        // 1. the argument is not defined by a move,
-        //
-        // 2. the argument is already linked or would cause a cycle if linked, or
-        //
-        // 3. the argument has a register constraint (the argument moves are there to make the
-        //    input value to a ranged invoke unconstrained.)
-        if (argument.definition == null ||
-            !argument.definition.isMove() ||
-            argument.isLinked() ||
-            argument == previous ||
-            argument.hasRegisterConstraint()) {
-          newArgument = createValue(argument.getType());
-          Move move = new Move(newArgument, argument);
-          move.setBlock(invoke.getBlock());
-          replaceArgument(invoke, i, newArgument);
-
-          boolean argumentIsDefinedInSameBlock =
-              argument.definition != null && argument.definition.getBlock() == invoke.getBlock();
-          if (invoke.requiredArgumentRegisters() > 16 && argumentIsDefinedInSameBlock) {
-            // Heuristic: Insert the move immediately after the argument. This increases the
-            // likelyhood that we will be able to move the argument directly into the register it
-            // needs to be in for the ranged invoke.
-            //
-            // If we instead were to insert the moves immediately before the ranged invoke when
-            // there are many arguments, then there is a high risk that we will need to spill the
-            // arguments before they get moved to the correct register right before the invoke.
-            assert move.src().definition.getNumber() >= 0;
-            insertAtDefinition.add(move);
-            move.setPosition(argument.definition.getPosition());
-          } else {
-            insertAt.add(move);
-            move.setPosition(invoke.getPosition());
-          }
-        }
-        if (previous != null) {
-          previous.linkTo(newArgument);
-        }
-        previous = newArgument;
-      }
-
-      if (insertAtDefinition != null && !insertAtDefinition.isEmpty()) {
-        generateArgumentMovesAtDefinitions(invoke, insertAtDefinition, insertAt);
-      }
+      Value newArgument = createValue(argument.getType());
+      Move move = new Move(newArgument, argument);
+      move.setPosition(invoke.getPosition());
+      replaceArgument(invoke, argumentIndex, newArgument);
+      instructionIterator.add(move);
     }
   }
 
-  private void generateArgumentMovesAtDefinitions(
-      Invoke invoke, PriorityQueue<Move> insertAtDefinition, InstructionListIterator insertAt) {
-    Move move = insertAtDefinition.poll();
-    // Rewind instruction iterator to the position where the first move needs to be inserted.
-    Instruction previousDefinition =
-        move.src().isArgument() ? lastArgumentValue.definition : move.src().definition;
-    while (insertAt.peekPrevious() != previousDefinition) {
-      insertAt.previous();
-    }
-    // Insert the instructions one by one after their definition.
-    insertAt.add(move);
-    while (!insertAtDefinition.isEmpty()) {
-      move = insertAtDefinition.poll();
-      Instruction currentDefinition =
-          move.src().isArgument() ? lastArgumentValue.definition : move.src().definition;
-      assert currentDefinition.getNumber() >= previousDefinition.getNumber();
-      if (currentDefinition.getNumber() > previousDefinition.getNumber()) {
-        // Move the instruction iterator forward to where this move needs to be inserted.
-        while (insertAt.peekPrevious() != currentDefinition) {
-          insertAt.next();
-        }
-      }
-      insertAt.add(move);
-      // Update state.
-      previousDefinition = currentDefinition;
-    }
-    // Move the instruction iterator forward to its old position.
-    while (insertAt.peekNext() != invoke) {
-      insertAt.next();
-    }
-  }
-
-  private boolean isInvokeRange(Instruction instruction) {
+  private static boolean isInvokeRange(Instruction instruction) {
     Invoke invoke = instruction.asInvoke();
     return invoke != null
         && invoke.requiredArgumentRegisters() > 5
         && !argumentsAreAlreadyLinked(invoke);
   }
 
-  private boolean argumentsAreAlreadyLinked(Invoke invoke) {
+  private static boolean argumentsAreAlreadyLinked(Invoke invoke) {
     Iterator<Value> it = invoke.arguments().iterator();
     Value current = it.next();
     while (it.hasNext()) {
@@ -3399,7 +3526,6 @@
         last.getLiveIntervals().link(next.getLiveIntervals());
         last = next;
       }
-      lastArgumentValue = last;
     }
   }
 
@@ -3411,18 +3537,15 @@
   }
 
   private void insertRangeInvokeMoves() {
-    if (options().getTestingOptions().enableLiveIntervalsSplittingForInvokeRange) {
-      return;
-    }
     for (BasicBlock block : code.blocks) {
       InstructionListIterator it = block.listIterator(code);
       while (it.hasNext()) {
         Instruction instruction = it.next();
-        if (instruction.isInvoke()) {
+        if (isInvokeRange(instruction)) {
           // Rewind so moves are inserted before the invoke.
           it.previous();
           // Generate the argument moves.
-          generateArgumentMoves(instruction.asInvoke(), it);
+          ensureUniqueArgumentsToInvokeRangeInstructions(instruction.asInvoke(), it);
           // Move past the move again.
           it.next();
         }
@@ -3480,10 +3603,10 @@
 
   private int getFreeConsecutiveRegisters(int numberOfRegisters, boolean prioritizeSmallRegisters) {
     int oldMaxRegisterNumber = maxRegisterNumber;
-    TreeSet<Integer> freeRegistersWithDesiredOrdering = this.freeRegisters;
+    IntSortedSet freeRegistersWithDesiredOrdering = freeRegisters;
     if (prioritizeSmallRegisters) {
       freeRegistersWithDesiredOrdering =
-          new TreeSet<>(
+          new IntRBTreeSet(
               (Integer x, Integer y) -> {
                 boolean xIsArgument = x < numberOfArgumentRegisters;
                 boolean yIsArgument = y < numberOfArgumentRegisters;
@@ -3498,10 +3621,10 @@
                 // Otherwise use their normal ordering.
                 return x - y;
               });
-      freeRegistersWithDesiredOrdering.addAll(this.freeRegisters);
+      freeRegistersWithDesiredOrdering.addAll(freeRegisters);
     }
 
-    Iterator<Integer> freeRegistersIterator = freeRegistersWithDesiredOrdering.iterator();
+    IntIterator freeRegistersIterator = freeRegistersWithDesiredOrdering.iterator();
     int first = getNextFreeRegister(freeRegistersIterator);
     int current = first;
     while (current - first + 1 != numberOfRegisters) {
@@ -3546,27 +3669,21 @@
     return true;
   }
 
-  private int getNextFreeRegister(Iterator<Integer> freeRegistersIterator) {
+  private int getNextFreeRegister(IntIterator freeRegistersIterator) {
     if (freeRegistersIterator.hasNext()) {
-      return freeRegistersIterator.next();
+      return freeRegistersIterator.nextInt();
     }
     return ++maxRegisterNumber;
   }
 
-  private void excludeRegistersForInterval(LiveIntervals intervals, IntSet excluded) {
-    int register = intervals.getRegister();
-    assert register != NO_REGISTER;
-
-    for (int i = 0; i < intervals.requiredRegisters(); i++) {
-      if (freeRegisters.remove(register + i)) {
-        excluded.add(register + i);
-      }
-    }
-
-    if (intervals.isArgumentInterval() && intervals != intervals.getSplitParent()) {
+  private void excludeRegistersForInterval(LiveIntervals intervals) {
+    assert intervals.hasRegister();
+    intervals.forEachRegister(freeRegisters::remove);
+    if (isPinnedArgumentRegister(intervals) && !intervals.isSplitParent()) {
       LiveIntervals parent = intervals.getSplitParent();
-      if (parent.getRegister() != register) {
-        excludeRegistersForInterval(parent, excluded);
+      assert parent.hasRegister();
+      if (parent.getRegister() != intervals.getRegister()) {
+        parent.forEachRegister(freeRegisters::remove);
       }
     }
   }
@@ -3580,7 +3697,7 @@
       freeRegisters.add(register + 1);
     }
 
-    if (intervals.isArgumentInterval() && intervals != intervals.getSplitParent()) {
+    if (isPinnedArgumentRegister(intervals) && !intervals.isSplitParent()) {
       LiveIntervals parent = intervals.getSplitParent();
       if (parent.getRegister() != intervals.getRegister()) {
         freeOccupiedRegistersForIntervals(intervals.getSplitParent());
@@ -3599,7 +3716,7 @@
   private void takeFreeRegistersForIntervals(LiveIntervals intervals) {
     takeFreeRegisters(intervals.getRegister(), intervals.getType().isWide());
 
-    if (intervals.isArgumentInterval() && intervals != intervals.getSplitParent()) {
+    if (isPinnedArgumentRegister(intervals) && !intervals.isSplitParent()) {
       LiveIntervals parent = intervals.getSplitParent();
       if (parent.getRegister() != intervals.getRegister()) {
         takeFreeRegistersForIntervals(parent);
@@ -3608,8 +3725,17 @@
   }
 
   private boolean registerIsFree(int register) {
-    return freeRegisters.contains(register)
-        || (hasDedicatedMoveExceptionRegister() && register == getMoveExceptionRegister());
+    return freeRegisters.contains(register) || isDedicatedMoveExceptionRegister(register);
+  }
+
+  private boolean registerRangeIsFree(int register, int requiredRegisters) {
+    for (int i = 0; i < requiredRegisters; i++) {
+      assert !isDedicatedMoveExceptionRegister(register + i);
+      if (!freeRegisters.contains(register + i)) {
+        return false;
+      }
+    }
+    return true;
   }
 
   // Note: treats a register as free if it is in the set of free registers, or it is the dedicated
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervals.java b/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervals.java
index 1b6c53d..64d42b0 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervals.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/LiveIntervals.java
@@ -38,11 +38,12 @@
   private boolean sortedChildren = false;
   private List<LiveRange> ranges = new ArrayList<>();
   private final TreeSet<LiveIntervalsUse> uses = new TreeSet<>();
-  private int numberOfConsecutiveRegisters = -1;
   private int register = NO_REGISTER;
-  private Integer hint;
+  private int hint = NO_REGISTER;
   private boolean spilled = false;
+  private boolean isInvokeRangeIntervals = false;
   private boolean usedInMonitorOperations = false;
+  private boolean liveAtMoveExceptionEntry = false;
 
   // Only registers up to and including the registerLimit are allowed for this interval.
   private int registerLimit = U16BIT_MAX;
@@ -87,6 +88,7 @@
   }
 
   public void setHint(LiveIntervals intervals, PriorityQueue<LiveIntervals> unhandled) {
+    assert intervals.hasRegister();
     // Do not set hints if they cannot be used anyway.
     if (!overlaps(intervals)) {
       // The hint is used in sorting the unhandled intervals. Therefore, if the hint changes
@@ -99,7 +101,12 @@
     }
   }
 
-  public Integer getHint() {
+  public boolean hasHint() {
+    return hint != NO_REGISTER;
+  }
+
+  public int getHint() {
+    assert hasHint();
     return hint;
   }
 
@@ -132,7 +139,6 @@
   }
 
   public void link(LiveIntervals next) {
-    assert numberOfConsecutiveRegisters == -1;
     nextConsecutive = next;
     next.previousConsecutive = this;
   }
@@ -146,23 +152,29 @@
     return definition != null && definition.isArgument();
   }
 
-  public LiveIntervals getStartOfConsecutive() {
-    LiveIntervals current = this;
-    while (current.previousConsecutive != null) {
-      current = current.previousConsecutive;
-    }
-    return current;
+  public boolean isSplitParent() {
+    return this == splitParent;
   }
 
   public LiveIntervals getNextConsecutive() {
     return nextConsecutive;
   }
 
+  public LiveIntervals getPreviousSplit() {
+    if (this == splitParent) {
+      return null;
+    }
+    splitParent.sortSplitChildrenIfNeeded();
+    int i = splitParent.getSplitChildren().indexOf(this) - 1;
+    return i >= 0 ? splitParent.getSplitChildren().get(i) : splitParent;
+  }
+
   public LiveIntervals getNextSplit() {
+    splitParent.sortSplitChildrenIfNeeded();
     if (this == splitParent) {
       return Iterables.getFirst(splitChildren, null);
     }
-    int i = splitParent.getSplitChildren().indexOf(this);
+    int i = splitParent.getSplitChildren().indexOf(this) + 1;
     return i < splitParent.getSplitChildren().size() ? splitParent.getSplitChildren().get(i) : null;
   }
 
@@ -170,32 +182,12 @@
     return previousConsecutive;
   }
 
-  public int numberOfConsecutiveRegisters() {
-    LiveIntervals start = getStartOfConsecutive();
-    if (start.numberOfConsecutiveRegisters != -1) {
-      assert start.numberOfConsecutiveRegisters == computeNumberOfConsecutiveRegisters();
-      return start.numberOfConsecutiveRegisters;
-    }
-    return computeNumberOfConsecutiveRegisters();
-  }
-
-  private int computeNumberOfConsecutiveRegisters() {
-    LiveIntervals start = getStartOfConsecutive();
-    int result = 0;
-    for (LiveIntervals current = start;
-        current != null;
-        current = current.nextConsecutive) {
-      result += current.requiredRegisters();
-    }
-    start.numberOfConsecutiveRegisters = result;
-    return result;
-  }
-
   public boolean hasSplits() {
     return splitChildren.size() != 0;
   }
 
   private void sortSplitChildrenIfNeeded() {
+    assert isSplitParent();
     if (!sortedChildren) {
       splitChildren.sort(Comparator.comparingInt(LiveIntervals::getEnd));
       sortedSplitChildrenEnds.clear();
@@ -304,6 +296,29 @@
     register = n;
   }
 
+  public boolean isInvokeRangeIntervals() {
+    return isInvokeRangeIntervals;
+  }
+
+  public void setIsInvokeRangeIntervals() {
+    assert !isInvokeRangeIntervals;
+    isInvokeRangeIntervals = true;
+  }
+
+  public void unsetIsInvokeRangeIntervals() {
+    assert isSplitParent();
+    isInvokeRangeIntervals = false;
+  }
+
+  public boolean isLiveAtMoveExceptionEntry() {
+    return splitParent.liveAtMoveExceptionEntry;
+  }
+
+  public void setIsLiveAtMoveExceptionEntry() {
+    assert isSplitParent();
+    liveAtMoveExceptionEntry = true;
+  }
+
   private int computeMaxNonSpilledRegister() {
     assert splitParent == this;
     assert maxNonSpilledRegister == NO_REGISTER;
@@ -353,7 +368,7 @@
 
   public void clearRegisterAssignment() {
     register = NO_REGISTER;
-    hint = null;
+    hint = NO_REGISTER;
   }
 
   public boolean overlapsPosition(int position) {
@@ -456,7 +471,7 @@
     start = toGapPosition(start);
     LiveIntervals splitChild = new LiveIntervals(splitParent);
     splitParent.splitChildren.add(splitChild);
-    splitParent.sortedChildren = false;
+    splitParent.sortedChildren = splitParent.splitChildren.size() == 1;
     List<LiveRange> beforeSplit = new ArrayList<>();
     List<LiveRange> afterSplit = new ArrayList<>();
     if (start == getEnd()) {
@@ -593,14 +608,14 @@
     if (startDiff != 0) return startDiff;
     // Then sort by register number of hints to make sure that a phi
     // does not take a low register that is the hint for another phi.
-    if (hint != null && other.hint != null) {
-      int registerDiff = hint - other.hint;
+    if (hasHint() && other.hasHint()) {
+      int registerDiff = getHint() - other.getHint();
       if (registerDiff != 0) return registerDiff;
     }
     // Intervals with hints go first so intervals without hints
     // do not take registers from intervals with hints.
-    if (hint != null && other.hint == null) return -1;
-    if (hint == null && other.hint != null) return 1;
+    if (hasHint() && !other.hasHint()) return -1;
+    if (!hasHint() && other.hasHint()) return 1;
     // Tie-breaker: no values have equal numbers.
     int result = value.getNumber() - other.value.getNumber();
     assert result != 0;
@@ -610,10 +625,6 @@
   @Override
   public String toString() {
     StringBuilder builder = new StringBuilder();
-    builder.append("(cons ");
-    // Use the field here to avoid toString to have side effects.
-    builder.append(numberOfConsecutiveRegisters);
-    builder.append("): ");
     for (LiveRange range : getRanges()) {
       builder.append(range);
       builder.append(" ");
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/MoveSorter.java b/src/main/java/com/android/tools/r8/ir/regalloc/MoveSorter.java
new file mode 100644
index 0000000..dacade5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/MoveSorter.java
@@ -0,0 +1,275 @@
+// Copyright (c) 2024, 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.regalloc;
+
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.ConstNumber;
+import com.android.tools.r8.ir.code.FixedRegisterValue;
+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.Move;
+import com.android.tools.r8.utils.ListUtils;
+import com.google.common.base.Equivalence;
+import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.collect.Iterables;
+import java.util.ArrayDeque;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+public class MoveSorter {
+
+  private static final Equivalence<Instruction> equivalence = new MoveEquivalence();
+
+  private final IRCode code;
+
+  public MoveSorter(IRCode code) {
+    this.code = code;
+  }
+
+  public void sortMovesForSuffixSharing() {
+    for (BasicBlock block : code.blocks(this::hasTwoPredecessorsWithUniqueSuccessor)) {
+      // First compute the set of moves that are in the suffix of both predecessor blocks. These are
+      // the candidates for suffix sharing.
+      Set<Wrapper<Instruction>> suffixSharingCandidates = getSuffixSharingCandidates(block);
+      if (suffixSharingCandidates.isEmpty()) {
+        continue;
+      }
+      // Compute a one-to-many map where an edge x -> y means that the instruction x cannot be moved
+      // past instruction y.
+      Map<Wrapper<Instruction>, Set<Instruction>> blockedBy =
+          computeBlockedBy(block, suffixSharingCandidates);
+      // Compute which instructions can be shared in the successor.
+      Set<Wrapper<Instruction>> sharedSuffix =
+          blockedBy.isEmpty()
+              ? suffixSharingCandidates
+              : computeSharedSuffix(suffixSharingCandidates, blockedBy);
+      if (!sharedSuffix.isEmpty()) {
+        for (BasicBlock predecessor : block.getPredecessors()) {
+          sortSuffix(predecessor, sharedSuffix);
+        }
+      }
+    }
+  }
+
+  Set<Wrapper<Instruction>> getSuffixSharingCandidates(BasicBlock block) {
+    Set<Wrapper<Instruction>> firstSuffix = new HashSet<>();
+    for (Instruction instruction = block.getPredecessor(0).exit().getPrev();
+        instruction != null && isMove(instruction);
+        instruction = instruction.getPrev()) {
+      firstSuffix.add(equivalence.wrap(instruction));
+    }
+    if (firstSuffix.isEmpty()) {
+      return firstSuffix;
+    }
+    Set<Wrapper<Instruction>> suffixSharingCandidates = new HashSet<>();
+    for (Instruction instruction = block.getPredecessor(1).exit().getPrev();
+        instruction != null && isMove(instruction);
+        instruction = instruction.getPrev()) {
+      Wrapper<Instruction> wrapper = equivalence.wrap(instruction);
+      if (firstSuffix.contains(wrapper)) {
+        suffixSharingCandidates.add(wrapper);
+      }
+    }
+    return suffixSharingCandidates;
+  }
+
+  Map<Wrapper<Instruction>, Set<Instruction>> computeBlockedBy(
+      BasicBlock block, Set<Wrapper<Instruction>> suffixSharingCandidates) {
+    Map<Wrapper<Instruction>, Set<Instruction>> blockedBy = new HashMap<>();
+    for (BasicBlock predecessor : block.getPredecessors()) {
+      addBlockedBy(predecessor, suffixSharingCandidates, blockedBy);
+    }
+    return blockedBy;
+  }
+
+  void addBlockedBy(
+      BasicBlock block,
+      Set<Wrapper<Instruction>> suffixSharingCandidates,
+      Map<Wrapper<Instruction>, Set<Instruction>> blockedBy) {
+    // Find the first move.
+    Instruction instruction = block.exit().getPrev();
+    assert instruction != null;
+    assert isMove(instruction);
+    while (instruction.hasPrev() && isMove(instruction.getPrev())) {
+      instruction = instruction.getPrev();
+    }
+
+    List<Instruction> seen = ListUtils.newArrayList(instruction);
+    for (instruction = instruction.getNext();
+        !instruction.isExit();
+        instruction = instruction.getNext()) {
+      // Record if the current instruction is blocking any of the previously seen instructions.
+      assert isMove(instruction);
+      for (Instruction previous : seen) {
+        if (isBlockedBy(previous, instruction)) {
+          blockedBy
+              .computeIfAbsent(equivalence.wrap(previous), ignoreKey(HashSet::new))
+              .add(instruction);
+        }
+      }
+      // Only add the current instruction to the seen set if it is a candidate for suffix sharing.
+      // Otherwise, we can't move it to the successor so we don't need worry about whether it is
+      // blocked.
+      if (suffixSharingCandidates.contains(equivalence.wrap(instruction))) {
+        seen.add(instruction);
+      }
+    }
+  }
+
+  Set<Wrapper<Instruction>> computeSharedSuffix(
+      Set<Wrapper<Instruction>> suffixSharingCandidates,
+      Map<Wrapper<Instruction>, Set<Instruction>> blockedBy) {
+    Set<Wrapper<Instruction>> sharedSuffix = new HashSet<>();
+    while (true) {
+      Wrapper<Instruction> unblockedMove =
+          removeUnblockedMove(suffixSharingCandidates, blockedBy, sharedSuffix);
+      if (unblockedMove == null) {
+        break;
+      }
+      sharedSuffix.add(unblockedMove);
+    }
+    return sharedSuffix;
+  }
+
+  Wrapper<Instruction> removeUnblockedMove(
+      Set<Wrapper<Instruction>> suffixSharingCandidates,
+      Map<Wrapper<Instruction>, Set<Instruction>> blockedBy,
+      Set<Wrapper<Instruction>> sharedSuffix) {
+    Iterator<Wrapper<Instruction>> iterator = suffixSharingCandidates.iterator();
+    while (iterator.hasNext()) {
+      Wrapper<Instruction> candidate = iterator.next();
+      Set<Instruction> blockingSet = blockedBy.getOrDefault(candidate, Collections.emptySet());
+      // Disregard blocking moves that have already been moved to the successor.
+      blockingSet.removeIf(blockingMove -> sharedSuffix.contains(equivalence.wrap(blockingMove)));
+      if (blockingSet.isEmpty()) {
+        iterator.remove();
+        return candidate;
+      }
+    }
+    return null;
+  }
+
+  void sortSuffix(BasicBlock predecessor, Set<Wrapper<Instruction>> sharedSuffix) {
+    InstructionListIterator iterator =
+        predecessor.listIterator(code, predecessor.getInstructions().size() - 1);
+    Deque<Instruction> removedInstructions = new ArrayDeque<>();
+    while (iterator.hasPrevious()) {
+      Instruction instruction = iterator.previous();
+      if (isMove(instruction)) {
+        if (sharedSuffix.contains(equivalence.wrap(instruction))) {
+          removedInstructions.addFirst(instruction);
+          iterator.removeInstructionIgnoreOutValue();
+        }
+      } else {
+        break;
+      }
+    }
+    assert removedInstructions.size() == sharedSuffix.size();
+    predecessor
+        .listIterator(code, predecessor.getInstructions().size() - 1)
+        .addAll(removedInstructions);
+  }
+
+  private boolean hasTwoPredecessorsWithUniqueSuccessor(BasicBlock block) {
+    return block.getPredecessors().size() == 2
+        && Iterables.all(
+            block.getPredecessors(),
+            predecessor -> predecessor.hasUniqueSuccessor() && predecessor.exit().isGoto());
+  }
+
+  private boolean isBlockedBy(Instruction instruction, Instruction laterInstruction) {
+    // Check if either of the two instructions write the operand of the other instruction.
+    if (instruction.isMove()) {
+      FixedRegisterValue inValue = instruction.getFirstOperand().asFixedRegisterValue();
+      FixedRegisterValue laterOutValue = laterInstruction.outValue().asFixedRegisterValue();
+      if (laterOutValue.usesRegister(inValue)) {
+        return true;
+      }
+    }
+    if (laterInstruction.isMove()) {
+      FixedRegisterValue outValue = instruction.outValue().asFixedRegisterValue();
+      FixedRegisterValue laterInValue = laterInstruction.getFirstOperand().asFixedRegisterValue();
+      if (outValue.usesRegister(laterInValue)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private boolean isMove(Instruction instruction) {
+    if (instruction.isConstNumber()) {
+      return instruction.outValue().isFixedRegisterValue();
+    }
+    if (instruction.isMove() && !instruction.isDebugLocalWrite()) {
+      if (instruction.getFirstOperand().isFixedRegisterValue()) {
+        assert instruction.outValue().isFixedRegisterValue();
+        return true;
+      }
+      // This Move instruction has been inserted for dealing with invoke/range. It is not inserted
+      // by the move scheduler.
+    }
+    return false;
+  }
+
+  private static class MoveEquivalence extends Equivalence<Instruction> {
+
+    @Override
+    protected boolean doEquivalent(Instruction a, Instruction b) {
+      if (a.isConstNumber()) {
+        if (!b.isConstNumber()) {
+          return false;
+        }
+        ConstNumber constNumber = a.asConstNumber();
+        ConstNumber other = b.asConstNumber();
+        return constNumber.getOutType().equals(other.getOutType())
+            && constNumber.getRawValue() == other.getRawValue()
+            && constNumber.outValue().asFixedRegisterValue().getRegister()
+                == other.outValue().asFixedRegisterValue().getRegister();
+      } else {
+        assert a.isMove();
+        if (!b.isMove()) {
+          return false;
+        }
+        Move move = a.asMove();
+        Move other = b.asMove();
+        return move.src()
+                .asFixedRegisterValue()
+                .outType()
+                .equals(other.src().asFixedRegisterValue().outType())
+            && move.src().asFixedRegisterValue().getRegister()
+                == other.src().asFixedRegisterValue().getRegister()
+            && move.outValue().asFixedRegisterValue().getRegister()
+                == other.outValue().asFixedRegisterValue().getRegister();
+      }
+    }
+
+    @Override
+    protected int doHash(Instruction instruction) {
+      if (instruction.isConstNumber()) {
+        ConstNumber constNumber = instruction.asConstNumber();
+        return Objects.hash(
+            constNumber.getClass(),
+            constNumber.outValue().asFixedRegisterValue().getRegister(),
+            constNumber.getRawValue());
+      } else {
+        assert instruction.isMove();
+        Move move = instruction.asMove();
+        return Objects.hash(
+            move.getClass(),
+            move.outValue().asFixedRegisterValue().getRegister(),
+            move.src().asFixedRegisterValue().getRegister());
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterAllocator.java b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterAllocator.java
index 6c2221c..398b633 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterAllocator.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.regalloc;
 
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.code.BasicBlock;
@@ -18,6 +19,10 @@
   int getRegisterForValue(Value value, int instructionNumber);
   int getArgumentOrAllocateRegisterForValue(Value value, int instructionNumber);
 
+  default int getArgumentRegisterForValue(Value value) {
+    throw new Unreachable();
+  }
+
   InternalOptions options();
 
   AppView<?> getAppView();
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMove.java b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMove.java
index be68587..70d2ab8 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMove.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMove.java
@@ -3,10 +3,15 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.regalloc;
 
+import static com.android.tools.r8.ir.regalloc.LiveIntervals.NO_REGISTER;
+
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.Instruction;
-import java.util.Map;
+import com.android.tools.r8.utils.ObjectUtils;
+import it.unimi.dsi.fastutil.ints.Int2IntMap;
+import java.util.Objects;
 import java.util.Set;
+import java.util.function.IntConsumer;
 
 // Register moves used by the spilling register allocator. These are used both for spill and
 // for phi moves and they are moves between actual registers represented by their register number.
@@ -27,51 +32,85 @@
   public RegisterMove(int dst, TypeElement type, Instruction definition) {
     assert definition.isOutConstant();
     this.dst = dst;
-    this.src = LiveIntervals.NO_REGISTER;
+    this.src = NO_REGISTER;
     this.definition = definition;
     this.type = type;
   }
 
-  private boolean writes(int register) {
-    if (type.isWidePrimitive() && (dst + 1) == register) {
-      return true;
+  public void forEachDestinationRegister(IntConsumer consumer) {
+    consumer.accept(dst);
+    if (isWide()) {
+      consumer.accept(dst + 1);
     }
-    return dst == register;
   }
 
-  @SuppressWarnings("ReferenceEquality")
-  public boolean isBlocked(Set<RegisterMove> moveSet, Map<Integer, Integer> valueMap) {
+  public void forEachSourceRegister(IntConsumer consumer) {
+    if (src != NO_REGISTER) {
+      consumer.accept(src);
+      if (isWide()) {
+        consumer.accept(src + 1);
+      }
+    }
+  }
+
+  public boolean writes(int register, boolean otherIsWide) {
+    if (dst == register) {
+      return true;
+    }
+    if (isWide() && dst + 1 == register) {
+      return true;
+    }
+    if (otherIsWide && dst == register + 1) {
+      return true;
+    }
+    return false;
+  }
+
+  public boolean isBlocked(
+      RegisterMoveScheduler scheduler, Set<RegisterMove> moveSet, Int2IntMap valueMap) {
+    if (isDestUsedAsTemporary(scheduler)) {
+      return true;
+    }
     for (RegisterMove move : moveSet) {
-      if (move.src == LiveIntervals.NO_REGISTER) {
+      if (isIdentical(move) || move.src == NO_REGISTER) {
         continue;
       }
-      if (move != this) {
-        if (writes(valueMap.get(move.src))) {
-          return true;
-        }
-        if (move.type.isWidePrimitive()) {
-          if (writes(valueMap.get(move.src) + 1)) {
-            return true;
-          }
-        }
+      if (writes(valueMap.get(move.src), move.isWide())) {
+        return true;
       }
     }
     return false;
   }
 
-  @Override
-  public int hashCode() {
-    return src + dst * 3 + type.hashCode() * 5 + (definition == null ? 0 : definition.hashCode());
+  public boolean isDestUsedAsTemporary(RegisterMoveScheduler scheduler) {
+    return scheduler.activeTempRegisters.contains(dst)
+        || (isWide() && scheduler.activeTempRegisters.contains(dst + 1));
+  }
+
+  public boolean isIdentical(RegisterMove move) {
+    return ObjectUtils.identical(this, move);
+  }
+
+  public boolean isNotIdentical(RegisterMove move) {
+    return !isIdentical(move);
+  }
+
+  public boolean isWide() {
+    return type.isWidePrimitive();
   }
 
   @Override
-  @SuppressWarnings("ReferenceEquality")
+  public int hashCode() {
+    return src + dst * 3 + type.hashCode() * 5 + Objects.hashCode(definition);
+  }
+
+  @Override
   public boolean equals(Object other) {
     if (!(other instanceof RegisterMove)) {
       return false;
     }
     RegisterMove o = (RegisterMove) other;
-    return o.src == src && o.dst == dst && o.type == type && o.definition == definition;
+    return o.src == src && o.dst == dst && type.equals(o.type) && o.definition == definition;
   }
 
   @Override
@@ -104,4 +143,16 @@
     }
     return definition.getNumber() - o.definition.getNumber();
   }
+
+  @Override
+  public String toString() {
+    if (type.isSinglePrimitive()) {
+      return "move " + dst + ", " + src;
+    } else if (type.isWidePrimitive()) {
+      return "move-wide " + dst + ", " + src;
+    } else {
+      assert type.isReferenceType();
+      return "move-object " + dst + ", " + src;
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveCycle.java b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveCycle.java
new file mode 100644
index 0000000..a1e986d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveCycle.java
@@ -0,0 +1,28 @@
+// Copyright (c) 2024, 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.regalloc;
+
+import java.util.TreeSet;
+
+class RegisterMoveCycle {
+
+  private final TreeSet<RegisterMove> moves;
+
+  // Whether the cycle is closed, i.e., the moves in the cycle are only blocked by moves also
+  // present in this cycle.
+  private final boolean closed;
+
+  RegisterMoveCycle(TreeSet<RegisterMove> cycle, boolean closed) {
+    this.moves = cycle;
+    this.closed = closed;
+  }
+
+  public TreeSet<RegisterMove> getMoves() {
+    return moves;
+  }
+
+  public boolean isClosed() {
+    return closed;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveCycleDetector.java b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveCycleDetector.java
new file mode 100644
index 0000000..cdb321d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveCycleDetector.java
@@ -0,0 +1,141 @@
+// Copyright (c) 2024, 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.regalloc;
+
+import com.android.tools.r8.utils.SetUtils;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+public class RegisterMoveCycleDetector {
+
+  @SuppressWarnings("MixedMutabilityReturnType")
+  static List<RegisterMoveCycle> getMoveCycles(TreeSet<RegisterMove> moveSet) {
+    // Although there can be a cycle when there are two moves, we return no cycles, since the
+    // default move scheduling has the same behavior as the cycle-based move scheduling in this
+    // case.
+    if (moveSet.size() <= 2) {
+      return Collections.emptyList();
+    }
+    List<RegisterMoveCycle> moveCycles = new ArrayList<>();
+    Int2ObjectMap<TreeSet<RegisterMove>> readBy = createReadByGraph(moveSet);
+    Set<RegisterMove> finished = new HashSet<>();
+    Deque<RegisterMove> stack = new ArrayDeque<>();
+    TreeSet<RegisterMove> stackSet = new TreeSet<>();
+    for (RegisterMove move : moveSet) {
+      if (finished.contains(move)) {
+        continue;
+      }
+      dfs(move, finished, readBy, stack, stackSet, moveCycles);
+      assert finished.contains(move);
+      assert stack.isEmpty();
+      assert stackSet.isEmpty();
+    }
+    return moveCycles;
+  }
+
+  private static void dfs(
+      RegisterMove move,
+      Set<RegisterMove> finished,
+      Int2ObjectMap<TreeSet<RegisterMove>> readBy,
+      Deque<RegisterMove> stack,
+      TreeSet<RegisterMove> stackSet,
+      List<RegisterMoveCycle> moveCycles) {
+    stack.addLast(move);
+    boolean addedToStack = stackSet.add(move);
+    assert addedToStack;
+    // Explore all outgoing edges. The successors of this move are the moves that read the
+    // destination registers of the current move.
+    for (RegisterMove successor : getSuccessors(move, readBy)) {
+      if (finished.contains(successor)) {
+        // Already fully explored.
+        continue;
+      }
+      if (successor.isIdentical(move)) {
+        // This move is reading/writing overlapping registers (e.g., move-wide 0, 1).
+        continue;
+      }
+      if (stackSet.contains(successor)) {
+        moveCycles.add(extractCycle(stack, successor, readBy));
+      } else {
+        dfs(successor, finished, readBy, stack, stackSet, moveCycles);
+      }
+    }
+    RegisterMove removedFromStack = stack.removeLast();
+    assert removedFromStack.isIdentical(move);
+    boolean removedFromStackSet = stackSet.remove(move);
+    assert removedFromStackSet;
+    boolean markedFinished = finished.add(move);
+    assert markedFinished;
+  }
+
+  // Returns a one-to-many map from registers to the set of moves that read that register.
+  private static Int2ObjectMap<TreeSet<RegisterMove>> createReadByGraph(
+      TreeSet<RegisterMove> moveSet) {
+    Int2ObjectMap<TreeSet<RegisterMove>> readBy = new Int2ObjectOpenHashMap<>();
+    for (RegisterMove move : moveSet) {
+      move.forEachSourceRegister(
+          register -> {
+            if (readBy.containsKey(register)) {
+              readBy.get(register).add(move);
+            } else {
+              readBy.put(register, SetUtils.newTreeSet(move));
+            }
+          });
+    }
+    return readBy;
+  }
+
+  private static Set<RegisterMove> getSuccessors(
+      RegisterMove move, Int2ObjectMap<TreeSet<RegisterMove>> readBy) {
+    TreeSet<RegisterMove> successors = readBy.get(move.dst);
+    if (move.isWide()) {
+      TreeSet<RegisterMove> additionalSuccessors = readBy.get(move.dst + 1);
+      if (successors == null) {
+        successors = additionalSuccessors;
+      } else if (additionalSuccessors != null) {
+        successors = new TreeSet<>(successors);
+        successors.addAll(additionalSuccessors);
+      }
+    }
+    return successors != null ? successors : Collections.emptySet();
+  }
+
+  private static RegisterMoveCycle extractCycle(
+      Deque<RegisterMove> stack,
+      RegisterMove cycleEntry,
+      Int2ObjectMap<TreeSet<RegisterMove>> readBy) {
+    Deque<RegisterMove> cycle = new ArrayDeque<>();
+    while (!cycleEntry.isIdentical(cycle.peekFirst())) {
+      cycle.addFirst(stack.removeLast());
+    }
+    stack.addAll(cycle);
+    TreeSet<RegisterMove> cycleSet = new TreeSet<>(cycle);
+    return new RegisterMoveCycle(cycleSet, isClosedCycle(cycleSet, readBy));
+  }
+
+  private static boolean isClosedCycle(
+      TreeSet<RegisterMove> cycle, Int2ObjectMap<TreeSet<RegisterMove>> readBy) {
+    for (RegisterMove move : cycle) {
+      for (int i = 0; i < move.type.requiredRegisters(); i++) {
+        TreeSet<RegisterMove> successors = readBy.get(move.dst + i);
+        if (successors != null) {
+          for (RegisterMove successor : successors) {
+            if (!cycle.contains(successor)) {
+              return false;
+            }
+          }
+        }
+      }
+    }
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveScheduler.java b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveScheduler.java
index 5251d17..43c2ed3 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveScheduler.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveScheduler.java
@@ -3,6 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.regalloc;
 
+import static com.android.tools.r8.ir.regalloc.LiveIntervals.NO_REGISTER;
+import static com.android.tools.r8.utils.IntConsumerUtils.emptyIntConsumer;
+import static com.google.common.base.Predicates.not;
+
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.Argument;
@@ -14,119 +18,152 @@
 import com.android.tools.r8.ir.code.Move;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.utils.ObjectUtils;
+import com.google.common.collect.Iterables;
+import it.unimi.dsi.fastutil.ints.Int2IntMap;
+import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
 import it.unimi.dsi.fastutil.ints.IntArraySet;
+import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
+import it.unimi.dsi.fastutil.ints.IntRBTreeSet;
 import it.unimi.dsi.fastutil.ints.IntSet;
+import it.unimi.dsi.fastutil.ints.IntSortedSet;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Deque;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedList;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
 import java.util.TreeSet;
+import java.util.function.IntConsumer;
 
 public class RegisterMoveScheduler {
   // The set of moves to schedule.
-  private final Set<RegisterMove> moveSet = new TreeSet<>();
+  private final TreeSet<RegisterMove> moveSet = new TreeSet<>();
   // Mapping to keep track of which values currently corresponds to each other.
   // This is initially an identity map but changes as we insert moves.
-  private final Map<Integer, Integer> valueMap = new HashMap<>();
-  // Number of temp registers used to schedule the moves.
-  private int usedTempRegisters = 0;
+  private final Int2IntMap valueMap = new Int2IntOpenHashMap();
   // Location at which to insert the scheduled moves.
   private final InstructionListIterator insertAt;
   // Debug position associated with insertion point.
   private final Position position;
   // The first available temporary register.
-  private final int tempRegister;
+  private final int firstTempRegister;
+  private int nextTempRegister;
+  // Free temporary registers that have been allocated by move scheduling.
+  private final IntSortedSet freeRegisters = new IntRBTreeSet();
+  // Registers that can be used as temporary registers until they are assigned by a move in the
+  // move set.
+  private final IntSortedSet freeRegistersUntilAssigned = new IntRBTreeSet();
+  // Registers that are the destination register of some move in the move set, but which are
+  // currently being used as a temporary register for another value. Moves in the move set that
+  // write a register in this set are blocked until the temporary registers are released.
+  public final IntSet activeTempRegisters = new IntOpenHashSet();
 
   public RegisterMoveScheduler(
-      InstructionListIterator insertAt, int tempRegister, Position position) {
+      InstructionListIterator insertAt, int firstTempRegister, Position position) {
     this.insertAt = insertAt;
-    this.tempRegister = tempRegister;
+    this.firstTempRegister = firstTempRegister;
+    this.nextTempRegister = firstTempRegister;
     this.position = position;
+    this.valueMap.defaultReturnValue(NO_REGISTER);
   }
 
-  public RegisterMoveScheduler(InstructionListIterator insertAt, int tempRegister) {
-    this(insertAt, tempRegister, Position.none());
+  public RegisterMoveScheduler(InstructionListIterator insertAt, int firstTempRegister) {
+    this(insertAt, firstTempRegister, Position.none());
+  }
+
+  private void initializeFreeRegistersUntilAssigned() {
+    // All registers that are assigned by the move set but not read can be used as temporary
+    // registers until they are assigned.
+    assert activeTempRegisters.isEmpty();
+    assert freeRegistersUntilAssigned.isEmpty();
+    for (RegisterMove move : moveSet) {
+      move.forEachDestinationRegister(freeRegistersUntilAssigned::add);
+    }
+    for (RegisterMove move : moveSet) {
+      move.forEachSourceRegister(freeRegistersUntilAssigned::remove);
+    }
   }
 
   public void addMove(RegisterMove move) {
     moveSet.add(move);
-    if (move.src != LiveIntervals.NO_REGISTER) {
+    if (move.src != NO_REGISTER) {
       valueMap.put(move.src, move.src);
     }
     valueMap.put(move.dst, move.dst);
   }
 
-  // TODO(b/270398965): Replace LinkedList.
-  @SuppressWarnings("JdkObsolete")
   public void schedule() {
     assert everyDestinationOnlyWrittenOnce();
+    initializeFreeRegistersUntilAssigned();
 
     // Worklist of moves that are ready to be inserted.
-    Deque<RegisterMove> worklist = new LinkedList<>();
+    Deque<RegisterMove> worklist = new ArrayDeque<>();
 
-    // Initialize worklist with the moves that do not interfere with other moves.
-    Iterator<RegisterMove> iterator = moveSet.iterator();
-    while (iterator.hasNext()) {
-      RegisterMove move = iterator.next();
-      if (!move.isBlocked(moveSet, valueMap)) {
-        worklist.addLast(move);
-        iterator.remove();
+    // If there are destination registers we can use until they are assigned, then instead of
+    // emitting these unblocked moves, we use them as temporary registers to unblock move cycles.
+    List<RegisterMoveCycle> moveCycles = RegisterMoveCycleDetector.getMoveCycles(moveSet);
+    for (RegisterMoveCycle moveCycle : moveCycles) {
+      for (RegisterMove move : moveCycle.getMoves()) {
+        assert move.isBlocked(this, moveSet, valueMap) || !moveSet.contains(move);
+        removeFromFreeRegistersUntilAssigned(move.dst, move.isWide(), emptyIntConsumer());
       }
+      // If the cycle is not closed then some moves in the cycle are blocked by other moves.
+      // TODO(b/375147902): Try to schedule these other moves before scheduling the cycle to
+      //  unblock the cycle. In JetNews 15% of move cycles are open.
+      if (moveCycle.isClosed()) {
+        assert moveSet.containsAll(moveCycle.getMoves());
+        assert worklist.isEmpty();
+        schedulePartial(moveCycle.getMoves(), worklist);
+      }
+      assert activeTempRegisters.isEmpty();
     }
 
+    // Initialize worklist with the moves that do not interfere with other moves.
+    enqueueUnblockedMoves(worklist);
+    schedulePartial(moveSet, worklist);
+    assert freeRegistersUntilAssigned.isEmpty();
+  }
+
+  private void schedulePartial(
+      TreeSet<RegisterMove> movesToSchedule, Deque<RegisterMove> worklist) {
     // Process the worklist generating moves. If the worklist becomes empty while the move set
     // still contains elements we need to use a temporary to break cycles.
-    while (!worklist.isEmpty() || !moveSet.isEmpty()) {
+    while (!worklist.isEmpty() || !movesToSchedule.isEmpty()) {
       while (!worklist.isEmpty()) {
-        RegisterMove move = worklist.removeFirst();
-        assert !move.isBlocked(moveSet, valueMap);
-        // Insert the move.
-        Integer generatedDest = createMove(move);
-        // Update the value map with the information that dest can be used instead of
-        // src starting now.
-        if (move.src != LiveIntervals.NO_REGISTER) {
-          valueMap.put(move.src, generatedDest);
+        while (!worklist.isEmpty()) {
+          RegisterMove move = worklist.removeFirst();
+          assert !move.isBlocked(this, moveSet, valueMap);
+          createMove(move);
         }
-        // Iterate and find the moves that were blocked because they need to write to
-        // one of the move src. That is now valid because the move src is preserved in dest.
-        iterator = moveSet.iterator();
-        while (iterator.hasNext()) {
-          RegisterMove other = iterator.next();
-          if (!other.isBlocked(moveSet, valueMap)) {
-            worklist.addLast(other);
-            iterator.remove();
-          }
-        }
+        enqueueUnblockedMoves(worklist, movesToSchedule);
       }
-      if (!moveSet.isEmpty()) {
+      if (!movesToSchedule.isEmpty()) {
         // The remaining moves are conflicting. Chose a move and unblock it by generating moves to
         // temporary registers for its destination value(s).
-        RegisterMove move = pickMoveToUnblock();
+        RegisterMove move = pickMoveToUnblock(movesToSchedule);
         createMoveDestToTemp(move);
-        worklist.addLast(move);
+        // TODO(b/375147902): After emitting the newly unblocked move, try to prioritize the moves
+        //  that blocked it, so that we free up the temp register, rather than getting overlapping
+        //  temporary registers.
+        worklist.add(move);
       }
     }
   }
 
   public int getUsedTempRegisters() {
-    return usedTempRegisters;
+    return nextTempRegister - firstTempRegister;
   }
 
   private List<RegisterMove> findMovesWithSrc(int src, TypeElement type) {
     List<RegisterMove> result = new ArrayList<>();
-    assert src != LiveIntervals.NO_REGISTER;
+    assert src != NO_REGISTER;
     for (RegisterMove move : moveSet) {
-      if (move.src == LiveIntervals.NO_REGISTER) {
+      if (move.src == NO_REGISTER) {
         continue;
       }
       int moveSrc = valueMap.get(move.src);
       if (moveSrc == src) {
         result.add(move);
-      } else if (move.type.isWidePrimitive() && (moveSrc + 1) == src) {
+      } else if (move.isWide() && (moveSrc + 1) == src) {
         result.add(move);
       } else if (type.isWidePrimitive() && (moveSrc - 1) == src) {
         result.add(move);
@@ -135,7 +172,7 @@
     return result;
   }
 
-  private Integer createMove(RegisterMove move) {
+  private void createMove(RegisterMove move) {
     Instruction instruction;
     if (move.definition != null) {
       if (move.definition.isArgument()) {
@@ -155,14 +192,51 @@
         }
       }
     } else {
+      int mappedSrc = valueMap.get(move.src);
       Value to = new FixedRegisterValue(move.type, move.dst);
-      Value from = new FixedRegisterValue(move.type, valueMap.get(move.src));
+      Value from = new FixedRegisterValue(move.type, mappedSrc);
       instruction = new Move(to, from);
+      returnTemporaryRegister(mappedSrc, move.isWide());
     }
     instruction.setPosition(position);
     insertAt.add(instruction);
-    return move.dst;
 
+    // Update the value map with the information that dest can be used instead of
+    // src starting now.
+    if (move.src != NO_REGISTER) {
+      valueMap.put(move.src, move.dst);
+    }
+    removeFromFreeRegistersUntilAssigned(move.dst, move.isWide(), emptyIntConsumer());
+  }
+
+  private void enqueueUnblockedMoves(Deque<RegisterMove> worklist) {
+    // Iterate and find the moves that were blocked because they need to write to one of the move
+    // src. That is now valid because the move src is preserved in dest.
+    moveSet.removeIf(
+        move -> {
+          if (move.isBlocked(this, moveSet, valueMap)) {
+            return false;
+          }
+          worklist.addLast(move);
+          return true;
+        });
+  }
+
+  private void enqueueUnblockedMoves(
+      Deque<RegisterMove> worklist, TreeSet<RegisterMove> movesToSchedule) {
+    // Iterate and find the moves that were blocked because they need to write to one of the move
+    // src. That is now valid because the move src is preserved in dest.
+    movesToSchedule.removeIf(
+        move -> {
+          if (move.isBlocked(this, moveSet, valueMap)) {
+            return false;
+          }
+          if (ObjectUtils.notIdentical(moveSet, movesToSchedule)) {
+            moveSet.remove(move);
+          }
+          worklist.addLast(move);
+          return true;
+        });
   }
 
   private void createMoveDestToTemp(RegisterMove move) {
@@ -170,34 +244,139 @@
     // registers if we are unlucky with the overlap for values that use two registers.
     List<RegisterMove> movesWithSrc = findMovesWithSrc(move.dst, move.type);
     assert movesWithSrc.size() > 0;
+    assert verifyMovesHaveDifferentSources(movesWithSrc);
     for (RegisterMove moveWithSrc : movesWithSrc) {
-      // TODO(ager): For now we always use a new temporary register whenever we have to unblock
-      // a move. The move scheduler can have multiple unblocking temps live at the same time
-      // and therefore we cannot have just one tempRegister (pair). However, we could check here
-      // if the previously used tempRegisters is still needed by any of the moves in the move set
-      // (taking the value map into account). If not, we can reuse the temp register instead
-      // of generating a new one.
-      Value to = new FixedRegisterValue(moveWithSrc.type, tempRegister + usedTempRegisters);
+      // TODO(b/375147902): Maybe seed the move scheduler with a set of registers known to be free
+      //  at this point.
+      int register = takeFreeRegister(moveWithSrc.isWide());
+      Value to = new FixedRegisterValue(moveWithSrc.type, register);
       Value from = new FixedRegisterValue(moveWithSrc.type, valueMap.get(moveWithSrc.src));
       Move instruction = new Move(to, from);
       instruction.setPosition(position);
       insertAt.add(instruction);
-      valueMap.put(moveWithSrc.src, tempRegister + usedTempRegisters);
-      usedTempRegisters += moveWithSrc.type.requiredRegisters();
+      valueMap.put(moveWithSrc.src, register);
     }
   }
 
-  private RegisterMove pickMoveToUnblock() {
-    Iterator<RegisterMove> iterator = moveSet.iterator();
-    RegisterMove move = null;
-    // Pick a non-wide move to unblock if possible.
-    while (iterator.hasNext()) {
-      move = iterator.next();
-      if (!move.type.isWidePrimitive()) {
-        break;
+  private int takeFreeRegister(boolean wide) {
+    int register = takeFreeRegisterFrom(freeRegistersUntilAssigned, wide);
+    if (register != NO_REGISTER) {
+      addActiveTempRegisters(register, wide);
+      return register;
+    }
+    register = takeFreeRegisterFrom(freeRegisters, wide);
+    if (register != NO_REGISTER) {
+      return register;
+    }
+    // We don't have a free register.
+    register = allocateExtraRegister();
+    if (!wide) {
+      return register;
+    }
+    if (freeRegisters.remove(register - 1)) {
+      return register - 1;
+    }
+    allocateExtraRegister();
+    return register;
+  }
+
+  private static int takeFreeRegisterFrom(IntSortedSet freeRegisters, boolean wide) {
+    for (int freeRegister : freeRegisters) {
+      if (wide && !freeRegisters.remove(freeRegister + 1)) {
+        continue;
+      }
+      freeRegisters.remove(freeRegister);
+      return freeRegister;
+    }
+    return NO_REGISTER;
+  }
+
+  private void addActiveTempRegisters(int register, boolean wide) {
+    boolean addedRegister = activeTempRegisters.add(register);
+    assert addedRegister;
+    if (wide) {
+      boolean addedHighRegister = activeTempRegisters.add(register + 1);
+      assert addedHighRegister;
+    }
+  }
+
+  private void returnTemporaryRegister(int register, boolean wide) {
+    if (returnActiveTempRegister(register, wide)) {
+      addFreeRegistersUntilAssigned(register, wide);
+    } else if (isExtraTemporaryRegister(register)) {
+      returnExtraTemporaryRegister(register, wide);
+    }
+  }
+
+  private boolean returnActiveTempRegister(int register, boolean wide) {
+    boolean removedRegister = activeTempRegisters.remove(register);
+    if (wide) {
+      if (removedRegister) {
+        boolean removedHighRegister = activeTempRegisters.remove(register + 1);
+        assert removedHighRegister;
+      } else {
+        assert !activeTempRegisters.contains(register + 1);
       }
     }
-    iterator.remove();
+    return removedRegister;
+  }
+
+  private void addFreeRegistersUntilAssigned(int register, boolean wide) {
+    boolean addedRegister = freeRegistersUntilAssigned.add(register);
+    assert addedRegister;
+    if (wide) {
+      boolean addedHighRegister = freeRegistersUntilAssigned.add(register + 1);
+      assert addedHighRegister;
+    }
+  }
+
+  private void returnExtraTemporaryRegister(int register, boolean wide) {
+    assert isExtraTemporaryRegister(register);
+    freeRegisters.add(register);
+    if (wide) {
+      assert isExtraTemporaryRegister(register + 1);
+      freeRegisters.add(register + 1);
+    }
+  }
+
+  private void removeFromFreeRegistersUntilAssigned(
+      int register, boolean wide, IntConsumer changedConsumer) {
+    if (freeRegistersUntilAssigned.remove(register)) {
+      changedConsumer.accept(register);
+    }
+    if (wide) {
+      if (freeRegistersUntilAssigned.remove(register + 1)) {
+        changedConsumer.accept(register + 1);
+      }
+    }
+  }
+
+  private boolean isExtraTemporaryRegister(int register) {
+    return register >= firstTempRegister;
+  }
+
+  private int allocateExtraRegister() {
+    return nextTempRegister++;
+  }
+
+  private boolean verifyMovesHaveDifferentSources(List<RegisterMove> movesWithSrc) {
+    IntSet seen = new IntOpenHashSet();
+    for (RegisterMove move : movesWithSrc) {
+      assert seen.add(move.src);
+    }
+    return true;
+  }
+
+  private RegisterMove pickMoveToUnblock(TreeSet<RegisterMove> moves) {
+    // Pick a non-wide move to unblock if possible.
+    Iterable<RegisterMove> eligible =
+        Iterables.filter(moves, move -> !move.isDestUsedAsTemporary(this));
+    RegisterMove move =
+        Iterables.find(eligible, not(RegisterMove::isWide), eligible.iterator().next());
+    moves.remove(move);
+    if (ObjectUtils.notIdentical(moves, moveSet)) {
+      moveSet.remove(move);
+    }
     return move;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/SpillMoveSet.java b/src/main/java/com/android/tools/r8/ir/regalloc/SpillMoveSet.java
index 87c059a..0bd5cf8 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/SpillMoveSet.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/SpillMoveSet.java
@@ -300,7 +300,7 @@
     // the arguments are not live, so it is insufficient to check that the destination register
     // is in the argument register range.
     for (SpillMove move : moves) {
-      assert !allocator.isPinnedArgumentRegister(move.to);
+      assert !allocator.canSkipArgumentMove(move.to);
     }
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/UnsplitArgumentsResult.java b/src/main/java/com/android/tools/r8/ir/regalloc/UnsplitArgumentsResult.java
new file mode 100644
index 0000000..5b0810d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/UnsplitArgumentsResult.java
@@ -0,0 +1,58 @@
+// Copyright (c) 2024, 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.regalloc;
+
+import static com.android.tools.r8.ir.regalloc.LiveIntervals.NO_REGISTER;
+
+import com.android.tools.r8.ir.code.Value;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+
+public class UnsplitArgumentsResult {
+
+  private final LinearScanRegisterAllocator allocator;
+  private final Reference2IntMap<LiveIntervals> originalRegisterAssignment;
+
+  public UnsplitArgumentsResult(
+      LinearScanRegisterAllocator allocator,
+      Reference2IntMap<LiveIntervals> originalRegisterAssignment) {
+    assert originalRegisterAssignment.defaultReturnValue() == NO_REGISTER;
+    this.allocator = allocator;
+    this.originalRegisterAssignment = originalRegisterAssignment;
+  }
+
+  public boolean isFullyReverted() {
+    return originalRegisterAssignment.isEmpty();
+  }
+
+  // Returns true if any changes were made.
+  public boolean revertPartial() {
+    boolean changed = false;
+    for (Value argument = allocator.firstArgumentValue;
+        argument != null;
+        argument = argument.getNextConsecutive()) {
+      for (LiveIntervals child : argument.getLiveIntervals().getSplitChildren()) {
+        changed |= revertPartial(child);
+      }
+    }
+    return changed;
+  }
+
+  private boolean revertPartial(LiveIntervals intervals) {
+    int originalRegister = originalRegisterAssignment.getInt(intervals);
+    if (originalRegister == NO_REGISTER) {
+      // This live intervals was not affected by the unsplit arguments optimization.
+      return false;
+    }
+    int conservativeRealRegisterEnd =
+        allocator.realRegisterNumberFromAllocated(intervals.getRegisterEnd());
+    if (conservativeRealRegisterEnd <= intervals.getRegister()) {
+      return false;
+    }
+    // Apply revert.
+    intervals.clearRegisterAssignment();
+    intervals.setRegister(originalRegister);
+    originalRegisterAssignment.removeInt(intervals);
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
index 27f4575..fd08861 100644
--- a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
@@ -356,7 +356,7 @@
       return blocks.computeIfAbsent(
           instructionIndex,
           k -> {
-            BasicBlock block = new BasicBlock();
+            BasicBlock block = new BasicBlock(irMetadata);
             block.setNumber(basicBlockNumberGenerator.next());
             return block;
           });
@@ -424,9 +424,7 @@
       int index = toInstructionIndexInIR(peekNextInstructionIndex());
       advanceInstructionState();
       instruction.setPosition(currentPosition);
-      currentBlock.getInstructions().add(instruction);
-      irMetadata.record(instruction);
-      instruction.setBlock(currentBlock);
+      currentBlock.getInstructions().addLast(instruction);
       int[] debugEndIndices = code.getDebugLocalEnds(index);
       if (debugEndIndices != null) {
         for (int encodedDebugEndIndex : debugEndIndices) {
@@ -471,9 +469,7 @@
       // which would otherwise advance the state.
       Argument argument = new Argument(dest, currentBlock.size(), isBooleanType);
       argument.setPosition(currentPosition);
-      currentBlock.getInstructions().add(argument);
-      irMetadata.record(argument);
-      argument.setBlock(currentBlock);
+      currentBlock.getInstructions().addLast(argument);
       return argument;
     }
 
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FieldReadBeforeWriteDfsAnalysis.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FieldReadBeforeWriteDfsAnalysis.java
index 1fdfb27..36ca4f0 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FieldReadBeforeWriteDfsAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FieldReadBeforeWriteDfsAnalysis.java
@@ -23,7 +23,6 @@
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.ir.code.Assume;
 import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.BasicBlockInstructionIterator;
 import com.android.tools.r8.ir.code.CheckCast;
 import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.ConstString;
@@ -142,9 +141,8 @@
       }
     }
 
-    AnalysisContinuation applyInstructions(BasicBlockInstructionIterator instructionIterator) {
-      while (instructionIterator.hasNext()) {
-        Instruction instruction = instructionIterator.next();
+    AnalysisContinuation applyInstructions(Instruction instruction) {
+      for (; instruction != null; instruction = instruction.getNext()) {
         assert !instruction.hasOutValue() || !isMaybeInstance(instruction.outValue());
         AnalysisContinuation continuation;
         // TODO(b/339210038): Extend this to many other instructions, such as ConstClass,
@@ -354,11 +352,9 @@
       }
       addBlockToStack(block);
       addInstanceAlias(getNewInstance().outValue());
-      BasicBlockInstructionIterator instructionIterator = block.iterator(uniqueConstructorInvoke);
       // Start the analysis from the invoke-direct instruction. This is important if we can tell
       // that the constructor definitely writes some fields.
-      instructionIterator.previous();
-      return applyInstructions(instructionIterator).toTraversalContinuation();
+      return applyInstructions(uniqueConstructorInvoke).toTraversalContinuation();
     }
   }
 
@@ -379,7 +375,7 @@
       }
       addBlockToStack(block);
       applyPhis(block);
-      AnalysisContinuation continuation = applyInstructions(block.iterator());
+      AnalysisContinuation continuation = applyInstructions(block.entry());
       assert continuation.isAbortOrContinue();
       return TraversalContinuation.breakIf(continuation.isAbort());
     }
diff --git a/src/main/java/com/android/tools/r8/shaking/EmptyEnqueuerDeferredTracing.java b/src/main/java/com/android/tools/r8/shaking/EmptyEnqueuerDeferredTracing.java
index 2faf20b..8872ed2 100644
--- a/src/main/java/com/android/tools/r8/shaking/EmptyEnqueuerDeferredTracing.java
+++ b/src/main/java/com/android/tools/r8/shaking/EmptyEnqueuerDeferredTracing.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.shaking.Enqueuer.FieldAccessKind;
 import com.android.tools.r8.shaking.Enqueuer.FieldAccessMetadata;
+import com.android.tools.r8.utils.Timing;
 import java.util.concurrent.ExecutorService;
 
 public class EmptyEnqueuerDeferredTracing extends EnqueuerDeferredTracing {
@@ -25,7 +26,7 @@
   }
 
   @Override
-  public boolean enqueueWorklistActions(EnqueuerWorklist worklist) {
+  public boolean enqueueWorklistActions(EnqueuerWorklist worklist, Timing timing) {
     return false;
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 30d420d..cb5e723 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -97,7 +97,9 @@
 import com.android.tools.r8.ir.analysis.proto.GeneratedMessageLiteBuilderShrinker;
 import com.android.tools.r8.ir.analysis.proto.ProtoEnqueuerUseRegistry;
 import com.android.tools.r8.ir.analysis.proto.schema.ProtoEnqueuerExtension;
+import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.code.ArrayPut;
+import com.android.tools.r8.ir.code.ConstClass;
 import com.android.tools.r8.ir.code.ConstantValueUtils;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
@@ -1127,7 +1129,7 @@
   }
 
   public void traceResourceValue(int value) {
-    appView.getResourceShrinkerState().trace(value);
+    appView.getResourceShrinkerState().trace(value, "from dex");
   }
 
   public void traceReflectiveFieldWrite(ProgramField field, ProgramMethod context) {
@@ -1516,6 +1518,11 @@
     MethodResolutionResult resolutionResult =
         handleInvokeOfDirectTarget(invokedMethod, context, reason);
     analyses.traceInvokeDirect(invokedMethod, resolutionResult, context);
+
+    if (invokedMethod.equals(appView.dexItemFactory().javaUtilEnumMapMembers.constructor)) {
+      // EnumMap uses reflection.
+      pendingReflectiveUses.add(context);
+    }
   }
 
   void traceInvokeInterface(
@@ -1570,16 +1577,14 @@
       identifierNameStrings.add(invokedMethod);
       // Revisit the current method to implicitly add -keep rule for items with reflective access.
       pendingReflectiveUses.add(context);
-    }
-    // See comment in handleJavaLangEnumValueOf.
-    if (invokedMethod == dexItemFactory.enumMembers.valueOf) {
+    } else if (invokedMethod == dexItemFactory.enumMembers.valueOf
+        || dexItemFactory.javaUtilEnumSetMembers.isFactoryMethod(invokedMethod)) {
+      // See comment in handleEnumValueOfOrCollectionInstantiation.
       pendingReflectiveUses.add(context);
-    }
-    // Handling of application services.
-    if (dexItemFactory.serviceLoaderMethods.isLoadMethod(invokedMethod)) {
+    } else if (invokedMethod == dexItemFactory.proxyMethods.newProxyInstance) {
       pendingReflectiveUses.add(context);
-    }
-    if (invokedMethod == dexItemFactory.proxyMethods.newProxyInstance) {
+    } else if (dexItemFactory.serviceLoaderMethods.isLoadMethod(invokedMethod)) {
+      // Handling of application services.
       pendingReflectiveUses.add(context);
     }
     markTypeAsLive(invokedMethod.getHolderType(), context);
@@ -4187,7 +4192,7 @@
     }
   }
 
-  private void synthesize() throws ExecutionException {
+  private void synthesize(Timing timing) throws ExecutionException {
     if (!mode.isInitialTreeShaking()) {
       return;
     }
@@ -4195,20 +4200,20 @@
     // In particular these additions are order independent, i.e., it does not matter which are
     // registered first and no dependencies may exist among them.
     SyntheticAdditions additions = new SyntheticAdditions(appView.createProcessorContext());
-    desugar(additions);
-    synthesizeInterfaceMethodBridges();
+    timing.time("Desugar", () -> desugar(additions));
+    timing.time("Synthesize interface method bridges", this::synthesizeInterfaceMethodBridges);
     if (additions.isEmpty()) {
       return;
     }
 
     // Commit the pending synthetics and recompute subtypes.
-    appInfo = appInfo.rebuildWithClassHierarchy(app -> app);
+    appInfo = timing.time("Rebuild AppInfo", () -> appInfo.rebuildWithClassHierarchy(app -> app));
     appView.setAppInfo(appInfo);
-    subtypingInfo = SubtypingInfo.create(appView);
+    subtypingInfo = timing.time("Create SubtypingInfo", () -> SubtypingInfo.create(appView));
 
     // Finally once all synthesized items "exist" it is now safe to continue tracing. The new work
     // items are enqueued and the fixed point will continue once this subroutine returns.
-    additions.enqueueWorkItems(this);
+    timing.time("Enqueue work items", () -> additions.enqueueWorkItems(this));
   }
 
   private boolean mustMoveToInterfaceCompanionMethod(ProgramMethod method) {
@@ -4638,7 +4643,9 @@
   private void trace(ExecutorService executorService, Timing timing) throws ExecutionException {
     timing.begin("Grow the tree.");
     try {
+      int round = 1;
       while (true) {
+        timing.begin("Compute fixpoint #" + round++);
         long numberOfLiveItems = getNumberOfLiveItems();
         while (worklist.hasNext()) {
           EnqueuerAction action = worklist.poll();
@@ -4651,38 +4658,49 @@
           timing.time("Conditional rules", () -> applicableRules.evaluateConditionalRules(this));
           assert getNumberOfLiveItems() == numberOfLiveItemsAfterProcessing;
           if (worklist.hasNext()) {
+            timing.end();
             continue;
           }
         }
 
         // Process all deferred annotations.
+        timing.begin("Process deferred annotations");
         processDeferredAnnotations(deferredAnnotations, AnnotatedKind::from);
         processDeferredAnnotations(
             deferredParameterAnnotations, annotatedItem -> AnnotatedKind.PARAMETER);
+        timing.end();
 
         // Continue fix-point processing while there are additional work items to ensure items that
         // are passed to Java reflections are traced.
         if (!pendingReflectiveUses.isEmpty()) {
+          timing.begin("Handle reflective behavior");
           pendingReflectiveUses.forEach(this::handleReflectiveBehavior);
           pendingReflectiveUses.clear();
+          timing.end();
         }
         if (worklist.hasNext()) {
+          timing.end();
           continue;
         }
 
         // Allow deferred tracing to enqueue worklist items.
-        if (deferredTracing.enqueueWorklistActions(worklist)) {
+        if (deferredTracing.enqueueWorklistActions(worklist, timing)) {
           assert worklist.hasNext();
+          timing.end();
           continue;
         }
 
         // Notify each analysis that a fixpoint has been reached, and give each analysis an
         // opportunity to add items to the worklist.
-        analyses.notifyFixpoint(this, worklist, executorService, timing);
+        timing.time(
+            "Notify analyses",
+            () -> analyses.notifyFixpoint(this, worklist, executorService, timing));
         if (worklist.hasNext()) {
+          timing.end();
           continue;
         }
 
+        timing.begin("Process delayed root set items");
         for (DelayedRootSetActionItem delayedRootSetActionItem :
             rootSet.delayedRootSetActionItems) {
           if (delayedRootSetActionItem.isInterfaceMethodSyntheticBridgeAction()) {
@@ -4690,26 +4708,31 @@
                 delayedRootSetActionItem.asInterfaceMethodSyntheticBridgeAction());
           }
         }
+        timing.end();
 
-        synthesize();
+        timing.time("Synthesize", () -> synthesize(timing));
 
+        timing.begin("Delayed interface method synthetic bridges");
         ConsequentRootSet consequentRootSet = computeDelayedInterfaceMethodSyntheticBridges();
         addConsequentRootSet(consequentRootSet);
         rootSet
             .getDependentMinimumKeepInfo()
             .merge(consequentRootSet.getDependentMinimumKeepInfo());
         rootSet.delayedRootSetActionItems.clear();
+        timing.end();
 
         if (worklist.hasNext()) {
+          timing.end();
           continue;
         }
 
         // Reached the fixpoint.
+        timing.end();
         break;
       }
 
       if (mode.isInitialTreeShaking()) {
-        postProcessingDesugaring();
+        timing.time("Post processing desugaring", this::postProcessingDesugaring);
       }
     } finally {
       timing.end();
@@ -5129,8 +5152,10 @@
       handleJavaLangReflectConstructorNewInstance(method, invoke);
       return;
     }
-    if (invokedMethod == dexItemFactory.enumMembers.valueOf) {
-      handleJavaLangEnumValueOf(method, invoke);
+    if (invokedMethod == dexItemFactory.enumMembers.valueOf
+        || invokedMethod == dexItemFactory.javaUtilEnumMapMembers.constructor
+        || dexItemFactory.javaUtilEnumSetMembers.isFactoryMethod(invokedMethod)) {
+      handleEnumValueOfOrCollectionInstantiation(method, invoke);
       return;
     }
     if (invokedMethod == dexItemFactory.proxyMethods.newProxyInstance) {
@@ -5462,17 +5487,44 @@
     }
   }
 
-  private void handleJavaLangEnumValueOf(ProgramMethod method, InvokeMethod invoke) {
+  private void handleEnumValueOfOrCollectionInstantiation(
+      ProgramMethod context, InvokeMethod invoke) {
+    if (invoke.inValues().isEmpty()) {
+      // Should never happen.
+      return;
+    }
+
     // The use of java.lang.Enum.valueOf(java.lang.Class, java.lang.String) will indirectly
     // access the values() method of the enum class passed as the first argument. The method
     // SomeEnumClass.valueOf(java.lang.String) which is generated by javac for all enums will
     // call this method.
-    if (invoke.inValues().get(0).isConstClass()) {
-      DexType type = invoke.inValues().get(0).definition.asConstClass().getType();
-      DexProgramClass clazz = getProgramClassOrNull(type, method);
-      if (clazz != null && clazz.isEnum()) {
-        markEnumValuesAsReachable(clazz, KeepReason.invokedFrom(method));
+    // Likewise, EnumSet and EnumMap call values() on the passed in Class.
+    Value firstArg = invoke.getFirstNonReceiverArgument();
+    if (firstArg.isPhi()) {
+      return;
+    }
+    DexType type;
+    if (invoke
+        .getInvokedMethod()
+        .getParameter(0)
+        .isIdenticalTo(appView.dexItemFactory().classType)) {
+      // EnumMap.<init>(), EnumSet.noneOf(), EnumSet.allOf(), Enum.valueOf().
+      ConstClass constClass = firstArg.definition.asConstClass();
+      if (constClass == null || !constClass.getType().isClassType()) {
+        return;
       }
+      type = constClass.getType();
+    } else {
+      // EnumSet.of(), EnumSet.range()
+      ClassTypeElement typeElement = firstArg.getType().asClassType();
+      if (typeElement == null) {
+        return;
+      }
+      type = typeElement.getClassType();
+    }
+    DexProgramClass clazz = getProgramClassOrNull(type, context);
+    if (clazz != null && clazz.isEnum()) {
+      markEnumValuesAsReachable(clazz, KeepReason.invokedFrom(context));
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracing.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracing.java
index d7369fbd..1955dfe 100644
--- a/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracing.java
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracing.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.shaking.Enqueuer.FieldAccessMetadata;
 import com.android.tools.r8.shaking.Enqueuer.Mode;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Timing;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 
@@ -56,7 +57,7 @@
    *
    * @return true if any worklist items were enqueued.
    */
-  public abstract boolean enqueueWorklistActions(EnqueuerWorklist worklist);
+  public abstract boolean enqueueWorklistActions(EnqueuerWorklist worklist, Timing timing);
 
   /**
    * Called when tree shaking has ended, to allow rewriting the application according to the tracing
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingImpl.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingImpl.java
index 1c3889d..bf3e76d 100644
--- a/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingImpl.java
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerDeferredTracingImpl.java
@@ -229,15 +229,19 @@
   }
 
   @Override
-  public boolean enqueueWorklistActions(EnqueuerWorklist worklist) {
-    return deferredEnqueuerActions.removeIf(
-        (field, worklistActions) -> {
-          if (isEligibleForPruning(field)) {
-            return false;
-          }
-          worklist.enqueueAll(worklistActions);
-          return true;
-        });
+  public boolean enqueueWorklistActions(EnqueuerWorklist worklist, Timing timing) {
+    timing.begin("Process deferred tracing");
+    boolean changed =
+        deferredEnqueuerActions.removeIf(
+            (field, worklistActions) -> {
+              if (isEligibleForPruning(field)) {
+                return false;
+              }
+              worklist.enqueueAll(worklistActions);
+              return true;
+            });
+    timing.end();
+    return changed;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/utils/CompileDumpBase.java b/src/main/java/com/android/tools/r8/utils/CompileDumpBase.java
index a5aadef..0cf82dc 100644
--- a/src/main/java/com/android/tools/r8/utils/CompileDumpBase.java
+++ b/src/main/java/com/android/tools/r8/utils/CompileDumpBase.java
@@ -4,183 +4,27 @@
 
 package com.android.tools.r8.utils;
 
+import com.android.tools.r8.BaseCompilerCommand;
+import com.android.tools.r8.utils.compiledump.ArtProfileDumpUtils;
 import java.io.IOException;
-import java.lang.reflect.Method;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
 import java.util.Map;
-import java.util.function.Consumer;
-import java.util.function.Function;
+import java.util.Map.Entry;
 
 public class CompileDumpBase {
 
-  static void setEnableExperimentalMissingLibraryApiModeling(
-      Object builder, boolean enableMissingLibraryApiModeling) {
-    getReflectiveBuilderMethod(
-            builder, "setEnableExperimentalMissingLibraryApiModeling", boolean.class)
-        .accept(new Object[] {enableMissingLibraryApiModeling});
-  }
-
-  static void setAndroidPlatformBuild(Object builder, boolean androidPlatformBuild) {
-    getReflectiveBuilderMethod(builder, "setAndroidPlatformBuild", boolean.class)
-        .accept(new Object[] {androidPlatformBuild});
-  }
-
-  static void setIsolatedSplits(Object builder, boolean isolatedSplits) {
-    getReflectiveBuilderMethod(builder, "setEnableIsolatedSplits", boolean.class)
-        .accept(new Object[] {isolatedSplits});
-  }
-
-  static void setupResourceShrinking(
-      Path androidResourcesInput, Path androidResourcesOutput, Object builder) {
-    try {
-      Class<?> androidResourceProvider =
-          Class.forName("com.android.tools.r8.AndroidResourceProvider");
-      Class<?> androidResourceConsumer =
-          Class.forName("com.android.tools.r8.AndroidResourceConsumer");
-      getReflectiveBuilderMethod(builder, "setAndroidResourceProvider", androidResourceProvider)
-          .accept(new Object[] {createAndroidResourceProvider(androidResourcesInput)});
-      getReflectiveBuilderMethod(builder, "setAndroidResourceConsumer", androidResourceConsumer)
-          .accept(new Object[] {createAndroidResourceConsumer(androidResourcesOutput)});
-    } catch (ClassNotFoundException e) {
-      // Ignore
+  static void addArtProfilesForRewriting(
+      BaseCompilerCommand.Builder<?, ?> builder, Map<Path, Path> artProfileFiles) {
+    for (Entry<Path, Path> inputOutput : artProfileFiles.entrySet()) {
+      runIgnoreMissing(
+          () ->
+              ArtProfileDumpUtils.addArtProfileForRewriting(
+                  inputOutput.getKey(), inputOutput.getValue(), builder),
+          "Unable to setup art profile rewriting for " + inputOutput.getKey());
     }
   }
 
-  static void addArtProfilesForRewriting(Object builder, Map<Path, Path> artProfileFiles) {
-    try {
-      Class<?> artProfileProviderClass =
-          Class.forName("com.android.tools.r8.profile.art.ArtProfileProvider");
-      Class<?> artProfileConsumerClass =
-          Class.forName("com.android.tools.r8.profile.art.ArtProfileConsumer");
-      artProfileFiles.forEach(
-          (artProfile, residualArtProfile) ->
-              getReflectiveBuilderMethod(
-                      builder,
-                      "addArtProfileForRewriting",
-                      artProfileProviderClass,
-                      artProfileConsumerClass)
-                  .accept(
-                      new Object[] {
-                        createArtProfileProvider(artProfile),
-                        createResidualArtProfileConsumer(residualArtProfile)
-                      }));
-    } catch (ClassNotFoundException e) {
-      // Ignore.
-    }
-  }
-
-  static void addStartupProfileProviders(Object builder, List<Path> startupProfileFiles) {
-    getReflectiveBuilderMethod(builder, "addStartupProfileProviders", Collection.class)
-        .accept(new Object[] {createStartupProfileProviders(startupProfileFiles)});
-  }
-
-  static Object callReflectiveDumpUtilsMethodWithPath(Path path, String method) {
-    Object[] returnObject = new Object[1];
-    boolean found =
-        callReflectiveUtilsMethod(
-            method,
-            new Class<?>[] {Path.class},
-            fn -> returnObject[0] = fn.apply(new Object[] {path}));
-    if (!found) {
-      System.out.println(
-          "Unable to call invoke method on path "
-              + path
-              + ". "
-              + "Method "
-              + method
-              + "() was not found.");
-      return null;
-    }
-    return returnObject[0];
-  }
-
-  static Object createAndroidResourceProvider(Path resourceInput) {
-    return callReflectiveDumpUtilsMethodWithPath(
-        resourceInput, "createAndroidResourceProviderFromDumpFile");
-  }
-
-  static Object createAndroidResourceConsumer(Path resourceOutput) {
-    return callReflectiveDumpUtilsMethodWithPath(
-        resourceOutput, "createAndroidResourceConsumerFromDumpFile");
-  }
-
-  static Object createArtProfileProvider(Path artProfile) {
-    return callReflectiveDumpUtilsMethodWithPath(
-        artProfile, "createArtProfileProviderFromDumpFile");
-  }
-
-  static Object createResidualArtProfileConsumer(Path residualArtProfile) {
-    return callReflectiveDumpUtilsMethodWithPath(
-        residualArtProfile, "createResidualArtProfileConsumerFromDumpFile");
-  }
-
-  static Collection<Object> createStartupProfileProviders(List<Path> startupProfileFiles) {
-    List<Object> startupProfileProviders = new ArrayList<>();
-    for (Path startupProfileFile : startupProfileFiles) {
-      boolean found =
-          callReflectiveUtilsMethod(
-              "createStartupProfileProviderFromDumpFile",
-              new Class<?>[] {Path.class},
-              fn -> startupProfileProviders.add(fn.apply(new Object[] {startupProfileFile})));
-      if (!found) {
-        System.out.println(
-            "Unable to add startup profiles as input. "
-                + "Method createStartupProfileProviderFromDumpFile() was not found.");
-        break;
-      }
-    }
-    return startupProfileProviders;
-  }
-
-  static Consumer<Object[]> getReflectiveBuilderMethod(
-      Object builder, String setter, Class<?>... parameters) {
-    try {
-      Method declaredMethod = builder.getClass().getMethod(setter, parameters);
-      return args -> {
-        try {
-          declaredMethod.invoke(builder, args);
-        } catch (Exception e) {
-          throw new RuntimeException(e);
-        }
-      };
-    } catch (NoSuchMethodException e) {
-      System.out.println(setter + " is not available on the compiledump version.");
-      // The option is not available so we just return an empty consumer
-      return args -> {};
-    }
-  }
-
-  static boolean callReflectiveUtilsMethod(
-      String methodName, Class<?>[] parameters, Consumer<Function<Object[], Object>> fnConsumer) {
-    Class<?> utilsClass;
-    try {
-      utilsClass = Class.forName("com.android.tools.r8.utils.CompileDumpUtils");
-    } catch (ClassNotFoundException e) {
-      return false;
-    }
-
-    Method declaredMethod;
-    try {
-      declaredMethod = utilsClass.getDeclaredMethod(methodName, parameters);
-    } catch (NoSuchMethodException e) {
-      return false;
-    }
-
-    fnConsumer.accept(
-        args -> {
-          try {
-            return declaredMethod.invoke(null, args);
-          } catch (Exception e) {
-            throw new RuntimeException(e);
-          }
-        });
-    return true;
-  }
-
   @SuppressWarnings({"CatchAndPrintStackTrace", "DefaultCharset"})
   // We cannot use StringResource since this class is added to the class path and has access only
   // to the public APIs.
@@ -195,4 +39,28 @@
 
     return content;
   }
+
+  protected static void runIgnoreMissing(Runnable runnable, String onMissing) {
+    try {
+      runnable.run();
+    } catch (NoClassDefFoundError | NoSuchMethodError e) {
+      System.out.println(onMissing);
+    }
+  }
+
+  protected static class BooleanBox {
+    public boolean value = false;
+
+    public BooleanBox(boolean value) {
+      this.value = value;
+    }
+
+    public void set(boolean value) {
+      this.value = value;
+    }
+
+    public boolean get() {
+      return value;
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/CompileDumpCompatR8.java b/src/main/java/com/android/tools/r8/utils/CompileDumpCompatR8.java
index 9112b77..ef8e947 100644
--- a/src/main/java/com/android/tools/r8/utils/CompileDumpCompatR8.java
+++ b/src/main/java/com/android/tools/r8/utils/CompileDumpCompatR8.java
@@ -10,8 +10,10 @@
 import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.R8;
 import com.android.tools.r8.R8Command;
-import com.android.tools.r8.R8Command.Builder;
 import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.utils.compiledump.CompilerCommandDumpUtils;
+import com.android.tools.r8.utils.compiledump.ResourceShrinkerDumpUtils;
+import com.android.tools.r8.utils.compiledump.StartupProfileDumpUtils;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
@@ -95,9 +97,9 @@
     Path androidResourcesOutput = null;
     int minApi = 1;
     int threads = -1;
-    boolean enableMissingLibraryApiModeling = false;
-    boolean androidPlatformBuild = false;
-    boolean isolatedSplits = false;
+    BooleanBox enableMissingLibraryApiModeling = new BooleanBox(false);
+    BooleanBox androidPlatformBuild = new BooleanBox(false);
+    BooleanBox isolatedSplits = new BooleanBox(false);
     for (int i = 0; i < args.length; i++) {
       String option = args[i];
       if (VALID_OPTIONS.contains(option)) {
@@ -123,13 +125,13 @@
               break;
             }
           case "--enable-missing-library-api-modeling":
-            enableMissingLibraryApiModeling = true;
+            enableMissingLibraryApiModeling.set(true);
             break;
           case "--android-platform-build":
-            androidPlatformBuild = true;
+            androidPlatformBuild.set(true);
             break;
           case ISOLATED_SPLITS_FLAG:
-            isolatedSplits = true;
+            isolatedSplits.set(true);
             break;
           default:
             throw new IllegalArgumentException("Unimplemented option: " + option);
@@ -228,7 +230,7 @@
         program.add(Paths.get(option));
       }
     }
-    Builder commandBuilder =
+    R8Command.Builder commandBuilder =
         new CompatProguardCommandBuilder(isCompatMode)
             .addProgramFiles(program)
             .addLibraryFiles(library)
@@ -238,15 +240,35 @@
             .setOutput(outputPath, outputMode)
             .setMode(compilationMode);
     addArtProfilesForRewriting(commandBuilder, artProfileFiles);
-    addStartupProfileProviders(commandBuilder, startupProfileFiles);
-    setAndroidPlatformBuild(commandBuilder, androidPlatformBuild);
-    setIsolatedSplits(commandBuilder, isolatedSplits);
-    setEnableExperimentalMissingLibraryApiModeling(commandBuilder, enableMissingLibraryApiModeling);
+    if (!startupProfileFiles.isEmpty()) {
+      runIgnoreMissing(
+          () -> StartupProfileDumpUtils.addStartupProfiles(startupProfileFiles, commandBuilder),
+          "Could not add startup profiles.");
+    }
+    runIgnoreMissing(
+        () ->
+            CompilerCommandDumpUtils.setAndroidPlatformBuild(
+                commandBuilder, androidPlatformBuild.get()),
+        "Android platform flag not available.");
+    runIgnoreMissing(
+        () -> CompilerCommandDumpUtils.setIsolatedSplits(commandBuilder, isolatedSplits.get()),
+        "Isolated splits flag not available.");
+    runIgnoreMissing(
+        () ->
+            CompilerCommandDumpUtils.setEnableExperimentalMissingLibraryApiModeling(
+                commandBuilder, enableMissingLibraryApiModeling.get()),
+        "Missing library api modeling not available.");
     if (desugaredLibJson != null) {
       commandBuilder.addDesugaredLibraryConfiguration(readAllBytesJava7(desugaredLibJson));
     }
     if (androidResourcesInput != null) {
-      setupResourceShrinking(androidResourcesInput, androidResourcesOutput, commandBuilder);
+      Path finalAndroidResourcesInput = androidResourcesInput;
+      Path finalAndroidResourcesOutput = androidResourcesOutput;
+      runIgnoreMissing(
+          () ->
+              ResourceShrinkerDumpUtils.setupBaseResourceShrinking(
+                  finalAndroidResourcesInput, finalAndroidResourcesOutput, commandBuilder),
+          "Failed initializing resource shrinker.");
     }
     if (desugaredLibKeepRuleConsumer != null) {
       commandBuilder.setDesugaredLibraryKeepRuleConsumer(desugaredLibKeepRuleConsumer);
diff --git a/src/main/java/com/android/tools/r8/utils/CompileDumpD8.java b/src/main/java/com/android/tools/r8/utils/CompileDumpD8.java
index 137dfe7..e9ce914 100644
--- a/src/main/java/com/android/tools/r8/utils/CompileDumpD8.java
+++ b/src/main/java/com/android/tools/r8/utils/CompileDumpD8.java
@@ -9,6 +9,8 @@
 import com.android.tools.r8.D8Command;
 import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.utils.compiledump.CompilerCommandDumpUtils;
+import com.android.tools.r8.utils.compiledump.StartupProfileDumpUtils;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
@@ -71,8 +73,8 @@
     List<Path> startupProfileFiles = new ArrayList<>();
     int minApi = 1;
     int threads = -1;
-    boolean enableMissingLibraryApiModeling = false;
-    boolean androidPlatformBuild = false;
+    BooleanBox enableMissingLibraryApiModeling = new BooleanBox(false);
+    BooleanBox androidPlatformBuild = new BooleanBox(false);
     for (int i = 0; i < args.length; i++) {
       String option = args[i];
       if (VALID_OPTIONS.contains(option)) {
@@ -93,10 +95,10 @@
               break;
             }
           case "--enable-missing-library-api-modeling":
-            enableMissingLibraryApiModeling = true;
+            enableMissingLibraryApiModeling.set(true);
             break;
           case "--android-platform-build":
-            androidPlatformBuild = true;
+            androidPlatformBuild.set(true);
             break;
           default:
             throw new IllegalArgumentException("Unimplemented option: " + option);
@@ -177,9 +179,21 @@
             .setOutput(outputPath, outputMode)
             .setMode(compilationMode);
     addArtProfilesForRewriting(commandBuilder, artProfileFiles);
-    addStartupProfileProviders(commandBuilder, startupProfileFiles);
-    setAndroidPlatformBuild(commandBuilder, androidPlatformBuild);
-    setEnableExperimentalMissingLibraryApiModeling(commandBuilder, enableMissingLibraryApiModeling);
+    if (!startupProfileFiles.isEmpty()) {
+      runIgnoreMissing(
+          () -> StartupProfileDumpUtils.addStartupProfiles(startupProfileFiles, commandBuilder),
+          "Could not add startup profiles.");
+    }
+    runIgnoreMissing(
+        () ->
+            CompilerCommandDumpUtils.setAndroidPlatformBuild(
+                commandBuilder, androidPlatformBuild.get()),
+        "Android platform flag not available.");
+    runIgnoreMissing(
+        () ->
+            CompilerCommandDumpUtils.setEnableExperimentalMissingLibraryApiModeling(
+                commandBuilder, enableMissingLibraryApiModeling.get()),
+        "Missing library api modeling not available.");
     if (desugaredLibJson != null) {
       commandBuilder.addDesugaredLibraryConfiguration(readAllBytesJava7(desugaredLibJson));
     }
diff --git a/src/main/java/com/android/tools/r8/utils/CompileDumpUtils.java b/src/main/java/com/android/tools/r8/utils/CompileDumpUtils.java
deleted file mode 100644
index ef013f6..0000000
--- a/src/main/java/com/android/tools/r8/utils/CompileDumpUtils.java
+++ /dev/null
@@ -1,84 +0,0 @@
-// 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.utils;
-
-import com.android.tools.r8.AndroidResourceConsumer;
-import com.android.tools.r8.AndroidResourceProvider;
-import com.android.tools.r8.ArchiveProtoAndroidResourceConsumer;
-import com.android.tools.r8.ArchiveProtoAndroidResourceProvider;
-import com.android.tools.r8.KeepMethodForCompileDump;
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.origin.PathOrigin;
-import com.android.tools.r8.profile.art.ArtProfileConsumer;
-import com.android.tools.r8.profile.art.ArtProfileConsumerUtils;
-import com.android.tools.r8.profile.art.ArtProfileProvider;
-import com.android.tools.r8.profile.art.ArtProfileProviderUtils;
-import com.android.tools.r8.references.MethodReference;
-import com.android.tools.r8.references.Reference;
-import com.android.tools.r8.startup.StartupProfileBuilder;
-import com.android.tools.r8.startup.StartupProfileProvider;
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-
-class CompileDumpUtils {
-
-  @KeepMethodForCompileDump
-  static ArtProfileProvider createArtProfileProviderFromDumpFile(Path artProfile) {
-    return ArtProfileProviderUtils.createFromHumanReadableArtProfile(artProfile);
-  }
-
-  @KeepMethodForCompileDump
-  static ArtProfileConsumer createResidualArtProfileConsumerFromDumpFile(Path residualArtProfile) {
-    return ArtProfileConsumerUtils.create(residualArtProfile);
-  }
-
-  @KeepMethodForCompileDump
-  static AndroidResourceProvider createAndroidResourceProviderFromDumpFile(Path resourceInput) {
-    return new ArchiveProtoAndroidResourceProvider(resourceInput);
-  }
-
-  @KeepMethodForCompileDump
-  static AndroidResourceConsumer createAndroidResourceConsumerFromDumpFile(Path resourceOutput) {
-    return new ArchiveProtoAndroidResourceConsumer(resourceOutput);
-  }
-
-  @KeepMethodForCompileDump
-  static StartupProfileProvider createStartupProfileProviderFromDumpFile(Path path) {
-    return new StartupProfileProvider() {
-
-      @Override
-      public void getStartupProfile(StartupProfileBuilder startupProfileBuilder) {
-        try {
-          try (BufferedReader bufferedReader = Files.newBufferedReader(path)) {
-            while (bufferedReader.ready()) {
-              String rule = bufferedReader.readLine();
-              MethodReference methodReference = MethodReferenceUtils.parseSmaliString(rule);
-              if (methodReference != null) {
-                startupProfileBuilder.addStartupMethod(
-                    startupMethodBuilder ->
-                        startupMethodBuilder.setMethodReference(methodReference));
-              } else {
-                assert DescriptorUtils.isClassDescriptor(rule);
-                startupProfileBuilder.addStartupClass(
-                    startupClassBuilder ->
-                        startupClassBuilder.setClassReference(Reference.classFromDescriptor(rule)));
-              }
-            }
-          }
-        } catch (IOException e) {
-          throw new UncheckedIOException(e);
-        }
-      }
-
-      @Override
-      public Origin getOrigin() {
-        return new PathOrigin(path);
-      }
-    };
-  }
-}
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 f35b8b4..b0757f4 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -2481,7 +2481,6 @@
     public boolean allowUnusedDontWarnRules = true;
     public boolean alwaysUseExistingAccessInfoCollectionsInMemberRebinding = true;
     public boolean alwaysUsePessimisticRegisterAllocation = false;
-    public boolean enableLiveIntervalsSplittingForInvokeRange = false;
     public boolean enableRegisterHintsForBlockedRegisters = false;
     // TODO(b/374266460): Investigate why enabling this leads to more moves, for example, in
     //  JetNews. Also investigate the impact on performance and how often the refinement pass is
diff --git a/src/main/java/com/android/tools/r8/utils/IterableUtils.java b/src/main/java/com/android/tools/r8/utils/IterableUtils.java
index 5df6add..acd31a4 100644
--- a/src/main/java/com/android/tools/r8/utils/IterableUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/IterableUtils.java
@@ -38,6 +38,18 @@
     return true;
   }
 
+  public static <T> boolean allWithPrevious(
+      Iterable<? extends T> iterable, BiPredicate<? super T, ? super T> predicate) {
+    T previous = null;
+    for (T element : iterable) {
+      if (!predicate.test(element, previous)) {
+        return false;
+      }
+      previous = element;
+    }
+    return true;
+  }
+
   public static <S, T> boolean any(
       Iterable<S> iterable, Function<S, T> transform, Predicate<T> predicate) {
     for (S element : iterable) {
diff --git a/src/main/java/com/android/tools/r8/utils/SetUtils.java b/src/main/java/com/android/tools/r8/utils/SetUtils.java
index ea46d64..7ec47cb 100644
--- a/src/main/java/com/android/tools/r8/utils/SetUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/SetUtils.java
@@ -13,6 +13,7 @@
 import java.util.IdentityHashMap;
 import java.util.LinkedHashSet;
 import java.util.Set;
+import java.util.TreeSet;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Function;
 
@@ -118,6 +119,12 @@
     return builder.build();
   }
 
+  public static <T extends Comparable<T>> TreeSet<T> newTreeSet(T element) {
+    TreeSet<T> result = new TreeSet<>();
+    result.add(element);
+    return result;
+  }
+
   public static <T, S> Set<T> mapIdentityHashSet(Collection<S> set, Function<S, T> fn) {
     Set<T> out = newIdentityHashSet(set.size());
     for (S element : set) {
diff --git a/src/main/java/com/android/tools/r8/utils/compiledump/ArtProfileDumpUtils.java b/src/main/java/com/android/tools/r8/utils/compiledump/ArtProfileDumpUtils.java
new file mode 100644
index 0000000..51935bb
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/compiledump/ArtProfileDumpUtils.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2024, 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.utils.compiledump;
+
+import com.android.tools.r8.BaseCompilerCommand;
+import com.android.tools.r8.profile.art.ArtProfileConsumerUtils;
+import com.android.tools.r8.profile.art.ArtProfileProviderUtils;
+import java.nio.file.Path;
+
+public class ArtProfileDumpUtils {
+  public static void addArtProfileForRewriting(
+      Path input, Path output, BaseCompilerCommand.Builder<?, ?> builder) {
+    builder.addArtProfileForRewriting(
+        ArtProfileProviderUtils.createFromHumanReadableArtProfile(input),
+        ArtProfileConsumerUtils.create(output));
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/compiledump/CompilerCommandDumpUtils.java b/src/main/java/com/android/tools/r8/utils/compiledump/CompilerCommandDumpUtils.java
new file mode 100644
index 0000000..3a7f051
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/compiledump/CompilerCommandDumpUtils.java
@@ -0,0 +1,35 @@
+// Copyright (c) 2024, 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.utils.compiledump;
+
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.R8Command;
+
+// Simple boolean commands directly on compiler command builders
+public class CompilerCommandDumpUtils {
+  public static void setEnableExperimentalMissingLibraryApiModeling(
+      R8Command.Builder builder, boolean enable) {
+    builder.setEnableExperimentalMissingLibraryApiModeling(enable);
+  }
+
+  public static void setEnableExperimentalMissingLibraryApiModeling(
+      D8Command.Builder builder, boolean enable) {
+    builder.setEnableExperimentalMissingLibraryApiModeling(enable);
+  }
+
+  public static void setAndroidPlatformBuild(
+      R8Command.Builder builder, boolean androidPlatformBuild) {
+    builder.setAndroidPlatformBuild(androidPlatformBuild);
+  }
+
+  public static void setAndroidPlatformBuild(
+      D8Command.Builder builder, boolean androidPlatformBuild) {
+    builder.setAndroidPlatformBuild(androidPlatformBuild);
+  }
+
+  public static void setIsolatedSplits(R8Command.Builder builder, boolean isolatedSplits) {
+    builder.setEnableIsolatedSplits(isolatedSplits);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/compiledump/FeatureSplitResourceShrinkerDumpUtils.java b/src/main/java/com/android/tools/r8/utils/compiledump/FeatureSplitResourceShrinkerDumpUtils.java
new file mode 100644
index 0000000..e58d34e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/compiledump/FeatureSplitResourceShrinkerDumpUtils.java
@@ -0,0 +1,18 @@
+// Copyright (c) 2024, 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.utils.compiledump;
+
+import com.android.tools.r8.ArchiveProtoAndroidResourceConsumer;
+import com.android.tools.r8.ArchiveProtoAndroidResourceProvider;
+import com.android.tools.r8.R8Command;
+import java.nio.file.Path;
+
+public class FeatureSplitResourceShrinkerDumpUtils {
+  public static void setupBaseResourceShrinking(
+      Path input, Path output, R8Command.Builder builder) {
+    builder.setAndroidResourceProvider(new ArchiveProtoAndroidResourceProvider(input));
+    builder.setAndroidResourceConsumer(new ArchiveProtoAndroidResourceConsumer(output));
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/compiledump/ResourceShrinkerDumpUtils.java b/src/main/java/com/android/tools/r8/utils/compiledump/ResourceShrinkerDumpUtils.java
new file mode 100644
index 0000000..7be55c1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/compiledump/ResourceShrinkerDumpUtils.java
@@ -0,0 +1,18 @@
+// Copyright (c) 2024, 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.utils.compiledump;
+
+import com.android.tools.r8.ArchiveProtoAndroidResourceConsumer;
+import com.android.tools.r8.ArchiveProtoAndroidResourceProvider;
+import com.android.tools.r8.R8Command;
+import java.nio.file.Path;
+
+public class ResourceShrinkerDumpUtils {
+  public static void setupBaseResourceShrinking(
+      Path input, Path output, R8Command.Builder builder) {
+    builder.setAndroidResourceProvider(new ArchiveProtoAndroidResourceProvider(input));
+    builder.setAndroidResourceConsumer(new ArchiveProtoAndroidResourceConsumer(output));
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/compiledump/StartupProfileDumpUtils.java b/src/main/java/com/android/tools/r8/utils/compiledump/StartupProfileDumpUtils.java
new file mode 100644
index 0000000..c78a082
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/compiledump/StartupProfileDumpUtils.java
@@ -0,0 +1,53 @@
+// Copyright (c) 2024, 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.utils.compiledump;
+
+import com.android.tools.r8.D8Command;
+import com.android.tools.r8.R8Command;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.origin.PathOrigin;
+import com.android.tools.r8.startup.StartupProfileBuilder;
+import com.android.tools.r8.startup.StartupProfileProvider;
+import com.android.tools.r8.utils.UTF8TextInputStream;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class StartupProfileDumpUtils {
+  public static void addStartupProfiles(List<Path> inputs, R8Command.Builder builder) {
+    builder.addStartupProfileProviders(createProviders(inputs));
+  }
+
+  public static void addStartupProfiles(List<Path> inputs, D8Command.Builder builder) {
+    builder.addStartupProfileProviders(createProviders(inputs));
+  }
+
+  private static List<StartupProfileProvider> createProviders(List<Path> inputs) {
+    return inputs.stream()
+        .map(StartupProfileDumpUtils::createStartupProfileProvider)
+        .collect(Collectors.toList());
+  }
+
+  private static StartupProfileProvider createStartupProfileProvider(Path path) {
+    return new StartupProfileProvider() {
+      @Override
+      public void getStartupProfile(StartupProfileBuilder startupProfileBuilder) {
+        try {
+          startupProfileBuilder.addHumanReadableArtProfile(
+              new UTF8TextInputStream(path), builder -> {});
+        } catch (IOException e) {
+          throw new UncheckedIOException(e);
+        }
+      }
+
+      @Override
+      public Origin getOrigin() {
+        return new PathOrigin(path);
+      }
+    };
+  }
+}
diff --git a/src/main/keep.txt b/src/main/keep.txt
index ddb667f..6034f07 100644
--- a/src/main/keep.txt
+++ b/src/main/keep.txt
@@ -2,8 +2,6 @@
 # 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.
 
--keepclasseswithmembers class * { @com.android.tools.r8.KeepMethodForCompileDump <methods>; }
-
 -keep public class com.android.tools.r8.D8 { public static void main(java.lang.String[]); }
 -keep public class com.android.tools.r8.R8 { public static void main(java.lang.String[]); }
 -keep public class com.android.tools.r8.ExtractMarker { public static void main(java.lang.String[]); }
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/ResourceTableUtil.kt b/src/resourceshrinker/java/com/android/build/shrinker/ResourceTableUtil.kt
index 88bb5e4..e81c951 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/ResourceTableUtil.kt
+++ b/src/resourceshrinker/java/com/android/build/shrinker/ResourceTableUtil.kt
@@ -111,7 +111,7 @@
 private fun packageIdFromIdentifier(
     identifier: Int
 ): Int =
-    identifier shr 24
+    identifier ushr 24
 
 private fun typeIdFromIdentifier(
     identifier: Int
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
index fccc50d..34e581b 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
@@ -44,6 +44,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Function;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
@@ -58,11 +59,13 @@
   private final List<Supplier<InputStream>> manifestProviders = new ArrayList<>();
   private final Map<String, Supplier<InputStream>> resfileProviders = new HashMap<>();
   private final Map<FeatureSplit, ResourceTable> resourceTables = new HashMap<>();
+  private final ShrinkerDebugReporter shrinkerDebugReporter;
   private ClassReferenceCallback enqueuerCallback;
   private Map<Integer, List<String>> resourceIdToXmlFiles;
   private Set<String> packageNames;
   private final Set<String> seenNoneClassValues = new HashSet<>();
   private final Set<Integer> seenResourceIds = new HashSet<>();
+  private final Map<Resource, String> reachabilityMap = new ConcurrentHashMap<>();
 
   private static final Set<String> SPECIAL_MANIFEST_ELEMENTS =
       ImmutableSet.of(
@@ -86,10 +89,11 @@
       Function<Exception, RuntimeException> errorHandler,
       ShrinkerDebugReporter shrinkerDebugReporter) {
     r8ResourceShrinkerModel = new R8ResourceShrinkerModel(shrinkerDebugReporter, true);
+    this.shrinkerDebugReporter = shrinkerDebugReporter;
     this.errorHandler = errorHandler;
   }
 
-  public void trace(int id) {
+  public void trace(int id, String reachableFrom) {
     if (!seenResourceIds.add(id)) {
       return;
     }
@@ -97,12 +101,17 @@
     if (resource == null) {
       return;
     }
+    assert reachableFrom != null;
+
+    // For deterministic output, sort the strings lexicographically.
+    reachabilityMap.compute(
+        resource, (r, v) -> v == null || v.compareTo(reachableFrom) > 0 ? reachableFrom : v);
     ResourceUsageModel.markReachable(resource);
     traceXmlForResourceId(id);
     if (resource.references != null) {
       for (Resource reference : resource.references) {
         if (!reference.isReachable()) {
-          trace(reference.value);
+          trace(reference.value, reference.toString());
         }
       }
     }
@@ -122,7 +131,7 @@
     r8ResourceShrinkerModel
         .getResourceStore()
         .processToolsAttributes()
-        .forEach(resource -> trace(resource.value));
+        .forEach(resource -> trace(resource.value, "keep xml file"));
     for (Supplier<InputStream> manifestProvider : manifestProviders) {
       traceXml("AndroidManifest.xml", manifestProvider.get());
     }
@@ -212,6 +221,13 @@
               featureSplit,
               ResourceTableUtilKt.nullOutEntriesWithIds(resourceTable, resourceIdsToRemove, true));
         });
+    for (Map.Entry<Resource, String> resourceStringEntry : reachabilityMap.entrySet()) {
+      shrinkerDebugReporter.debug(
+          () ->
+              resourceStringEntry.getKey().toString()
+                  + " reachable from "
+                  + resourceStringEntry.getValue());
+    }
     return new ShrinkerResult(resEntriesToKeep, shrunkenTables);
   }
 
@@ -243,7 +259,7 @@
       // resources for the reachable marker.
       ProtoAndroidManifestUsageRecorderKt.recordUsagesFromNode(xmlNode, r8ResourceShrinkerModel)
           .iterator()
-          .forEachRemaining(resource -> trace(resource.value));
+          .forEachRemaining(resource -> trace(resource.value, xmlFile));
     } catch (IOException e) {
       errorHandler.apply(e);
     }
diff --git a/src/test/java/com/android/tools/r8/accessrelaxation/InvokeTypeConversionTest.java b/src/test/java/com/android/tools/r8/accessrelaxation/InvokeTypeConversionTest.java
index f29440c..e1c7e00 100644
--- a/src/test/java/com/android/tools/r8/accessrelaxation/InvokeTypeConversionTest.java
+++ b/src/test/java/com/android/tools/r8/accessrelaxation/InvokeTypeConversionTest.java
@@ -13,8 +13,8 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
-import com.android.tools.r8.dex.code.DexInvokeDirect;
-import com.android.tools.r8.dex.code.DexInvokeVirtual;
+import com.android.tools.r8.dex.code.DexInvokeDirectRange;
+import com.android.tools.r8.dex.code.DexInvokeVirtualRange;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.smali.SmaliBuilder;
@@ -121,7 +121,7 @@
           assertNotNull(method);
           DexCode code = method.getCode().asDexCode();
           // The given invoke line is remained as-is.
-          assertTrue(code.instructions[2] instanceof DexInvokeDirect);
+          assertTrue(code.instructions[2] instanceof DexInvokeDirectRange);
         });
   }
 
@@ -152,7 +152,7 @@
           assertNotNull(method);
           DexCode code = method.getCode().asDexCode();
           // The given invoke line is changed to invoke-virtual
-          assertTrue(code.instructions[2] instanceof DexInvokeVirtual);
+          assertTrue(code.instructions[2] instanceof DexInvokeVirtualRange);
         });
   }
 
diff --git a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkerLoggingTest.java b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkerLoggingTest.java
index 1028b38..67dbef6 100644
--- a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkerLoggingTest.java
+++ b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkerLoggingTest.java
@@ -118,9 +118,22 @@
       ensureResourceReachabilityState(strings, "drawable", "foobar", true);
       ensureRootResourceState(strings, "drawable", "foobar", true);
       ensureUnusedState(strings, "drawable", "foobar", false);
+    } else {
+      assertTrue(finished.get());
+      List<String> strings = StringUtils.splitLines(log.toString());
+      ensureReachableOptimized(strings, "string", "bar", true);
+      ensureReachableOptimized(strings, "string", "foo", true);
+      ensureReachableOptimized(strings, "drawable", "foobar", true);
+      ensureReachableOptimized(strings, "drawable", "unused_drawable", false);
     }
   }
 
+  private void ensureReachableOptimized(
+      List<String> logStrings, String type, String name, boolean reachable) {
+    assertEquals(
+        logStrings.stream().anyMatch(s -> s.startsWith(type + ":" + name + ":")), reachable);
+  }
+
   private void ensureDexReachableResourcesState(
       List<String> logStrings, String type, String name, boolean reachable) {
     // Example line:
diff --git a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeatures.java b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeatures.java
index 221f011..e924ca6 100644
--- a/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeatures.java
+++ b/src/test/java/com/android/tools/r8/androidresources/ResourceShrinkingWithFeatures.java
@@ -35,11 +35,17 @@
   @Parameter(1)
   public boolean optimized;
 
-  @Parameters(name = "{0}, optimized: {1}")
+  @Parameter(2)
+  public int featurePackageId;
+
+  @Parameters(name = "{0}, optimized: {1}, feature_package_id: {2}")
   public static List<Object[]> data() {
     return buildParameters(
         getTestParameters().withDefaultDexRuntime().withAllApiLevels().build(),
-        BooleanUtils.values());
+        BooleanUtils.values(),
+        // Ensure that we can handle resource ids both bigger and smaller than 127, see
+        // b/378470047
+        new Integer[] {0x7E, 0x80});
   }
 
   public static AndroidTestResource getTestResources(TemporaryFolder temp) throws Exception {
@@ -49,11 +55,10 @@
         .build(temp);
   }
 
-  public static AndroidTestResource getFeatureSplitTestResources(TemporaryFolder temp)
-      throws IOException {
+  public AndroidTestResource getFeatureSplitTestResources(TemporaryFolder temp) throws IOException {
     return new AndroidTestResourceBuilder()
         .withSimpleManifestAndAppNameString()
-        .setPackageId(0x7E)
+        .setPackageId(featurePackageId)
         .addRClassInitializeWithDefaultValues(FeatureSplit.R.string.class)
         .build(temp);
   }
@@ -115,9 +120,9 @@
             })
         .inspectShrunkenResourcesForFeature(
             resourceTableInspector -> {
-              resourceTableInspector.assertContainsResourceWithName("string", "feature_used");
-              resourceTableInspector.assertDoesNotContainResourceWithName(
-                  "string", "feature_unused");
+                resourceTableInspector.assertContainsResourceWithName("string", "feature_used");
+                resourceTableInspector.assertDoesNotContainResourceWithName(
+                    "string", "feature_unused");
             },
             FeatureSplit.class.getName())
         .run(parameters.getRuntime(), Base.class)
diff --git a/src/test/java/com/android/tools/r8/cf/TryRangeTestRunner.java b/src/test/java/com/android/tools/r8/cf/TryRangeTestRunner.java
index 21ee0fc..10abbeb 100644
--- a/src/test/java/com/android/tools/r8/cf/TryRangeTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/TryRangeTestRunner.java
@@ -120,9 +120,7 @@
       add = it.next();
     }
     it.removeInstructionIgnoreOutValue();
-    constNumber.setBlock(tryBlock);
-    add.setBlock(tryBlock);
-    tryBlock.getInstructions().add(0, add);
-    tryBlock.getInstructions().add(0, constNumber);
+    tryBlock.getInstructions().addFirst(add);
+    tryBlock.getInstructions().addFirst(constNumber);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/compatproguard/ForNameTest.java b/src/test/java/com/android/tools/r8/compatproguard/ForNameTest.java
index 9c7cf17..d64aa20 100644
--- a/src/test/java/com/android/tools/r8/compatproguard/ForNameTest.java
+++ b/src/test/java/com/android/tools/r8/compatproguard/ForNameTest.java
@@ -8,7 +8,7 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.dex.code.DexConstString;
-import com.android.tools.r8.dex.code.DexInvokeStatic;
+import com.android.tools.r8.dex.code.DexInvokeStaticRange;
 import com.android.tools.r8.dex.code.DexReturnVoid;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.smali.SmaliBuilder;
@@ -55,7 +55,7 @@
     assertTrue(code.instructions[0] instanceof DexConstString);
     DexConstString constString = (DexConstString) code.instructions[0];
     assertNotEquals(BOO, constString.getString().toString());
-    assertTrue(code.instructions[1] instanceof DexInvokeStatic);
+    assertTrue(code.instructions[1] instanceof DexInvokeStaticRange);
     assertTrue(code.instructions[2] instanceof DexReturnVoid);
   }
 
@@ -91,7 +91,7 @@
     assertTrue(code.instructions[0] instanceof DexConstString);
     DexConstString constString = (DexConstString) code.instructions[0];
     assertEquals(BOO, constString.getString().toString());
-    assertTrue(code.instructions[1] instanceof DexInvokeStatic);
+    assertTrue(code.instructions[1] instanceof DexInvokeStaticRange);
     assertTrue(code.instructions[2] instanceof DexReturnVoid);
   }
 
diff --git a/src/test/java/com/android/tools/r8/compatproguard/GetMembersTest.java b/src/test/java/com/android/tools/r8/compatproguard/GetMembersTest.java
index 3449aa1..eb24283 100644
--- a/src/test/java/com/android/tools/r8/compatproguard/GetMembersTest.java
+++ b/src/test/java/com/android/tools/r8/compatproguard/GetMembersTest.java
@@ -15,7 +15,7 @@
 import com.android.tools.r8.dex.code.DexConst4;
 import com.android.tools.r8.dex.code.DexConstClass;
 import com.android.tools.r8.dex.code.DexConstString;
-import com.android.tools.r8.dex.code.DexFilledNewArray;
+import com.android.tools.r8.dex.code.DexFilledNewArrayRange;
 import com.android.tools.r8.dex.code.DexInvokeVirtual;
 import com.android.tools.r8.dex.code.DexMoveResultObject;
 import com.android.tools.r8.dex.code.DexNewArray;
@@ -75,9 +75,9 @@
   private void inspectGetMethodTest(MethodSubject method) {
     // Accept either array construction style (differs based on minSdkVersion).
     DexCode code = method.getMethod().getCode().asDexCode();
-    if (code.instructions[1] instanceof DexFilledNewArray) {
+    if (code.instructions[1] instanceof DexFilledNewArrayRange) {
       assertTrue(code.instructions[0] instanceof DexConstClass);
-      assertTrue(code.instructions[1] instanceof DexFilledNewArray);
+      assertTrue(code.instructions[1] instanceof DexFilledNewArrayRange);
       assertTrue(code.instructions[2] instanceof DexMoveResultObject);
       assertTrue(code.instructions[3] instanceof DexConstClass);
       assertTrue(code.instructions[4] instanceof DexConstString);
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DayTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DayTest.java
index 3b5bfd0..93d7554 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DayTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DayTest.java
@@ -77,12 +77,10 @@
     if (parameters.isCfRuntime() && parameters.getRuntime().asCf().isOlderThan(CfVm.JDK9)) {
       return SIMPLIFIED_CHINESE_EXPECTED_RESULT_JDK8;
     }
-    if (parameters.isDexRuntime() && libraryDesugaringSpecification.hasTimeDesugaring(parameters)) {
-      if (libraryDesugaringSpecification == JDK8) {
-        return MISSING_STANDALONE;
-      } else {
-        return SIMPLIFIED_CHINESE_NARROW_DAY_ISSUE;
-      }
+    if (parameters.isDexRuntime()
+        && libraryDesugaringSpecification.hasTimeDesugaring(parameters)
+        && libraryDesugaringSpecification == JDK8) {
+      return MISSING_STANDALONE;
     }
     return SIMPLIFIED_CHINESE_EXPECTED_RESULT;
   }
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/FailingMethodEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/FailingMethodEnumUnboxingTest.java
index 46bc0d0..01a5e94 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/FailingMethodEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/FailingMethodEnumUnboxingTest.java
@@ -9,7 +9,6 @@
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.R8TestCompileResult;
-import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import java.util.EnumSet;
@@ -72,11 +71,8 @@
     for (Class<?> main : TESTS) {
       compile
           .run(parameters.getRuntime(), main)
-          .applyIf(
-              main == EnumSetTest.class && enumKeepRules.getKeepRules().isEmpty(),
-              // EnumSet and EnumMap cannot be used without the enumKeepRules.
-              SingleTestRunResult::assertFailure,
-              result -> result.assertSuccess().inspectStdOut(this::assertLines2By2Correct));
+          .assertSuccess()
+          .inspectStdOut(this::assertLines2By2Correct);
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/SplitBlockTest.java b/src/test/java/com/android/tools/r8/ir/SplitBlockTest.java
index 484aca5..dbbec8e 100644
--- a/src/test/java/com/android/tools/r8/ir/SplitBlockTest.java
+++ b/src/test/java/com/android/tools/r8/ir/SplitBlockTest.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionList;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.NumericType;
 import com.android.tools.r8.ir.code.Position;
@@ -97,7 +98,8 @@
 
       assertEquals(argumentInstructions, test.countArgumentInstructions());
       assertEquals(firstBlockInstructions, block.getInstructions().size());
-      assertTrue(!block.getInstructions().get(i).isArgument());
+      InstructionList instructionList = block.getInstructions();
+      assertTrue(!instructionList.getNth(i).isArgument());
 
       InstructionListIterator iterator = test.listIteratorAt(block, i);
       BasicBlock newBlock = iterator.split(code);
@@ -132,7 +134,8 @@
 
       assertEquals(argumentInstructions, test.countArgumentInstructions());
       assertEquals(firstBlockInstructions, block.getInstructions().size());
-      assertTrue(!block.getInstructions().get(i).isArgument());
+      InstructionList instructionList = block.getInstructions();
+      assertTrue(!instructionList.getNth(i).isArgument());
 
       InstructionListIterator iterator = test.listIteratorAt(block, i);
       BasicBlock newBlock = iterator.split(code, 1);
@@ -337,7 +340,8 @@
 
       assertEquals(argumentInstructions, test.countArgumentInstructions());
       assertEquals(firstBlockInstructions, block.getInstructions().size());
-      assertTrue(!block.getInstructions().get(i).isArgument());
+      InstructionList instructionList = block.getInstructions();
+      assertTrue(!instructionList.getNth(i).isArgument());
 
       InstructionListIterator iterator = test.listIteratorAt(block, i);
       BasicBlock newBlock = iterator.split(code);
@@ -459,7 +463,8 @@
 
       assertEquals(argumentInstructions, test.countArgumentInstructions());
       assertEquals(firstBlockInstructions, block.getInstructions().size());
-      assertTrue(!block.getInstructions().get(i).isArgument());
+      InstructionList instructionList = block.getInstructions();
+      assertTrue(!instructionList.getNth(i).isArgument());
 
       InstructionListIterator iterator = test.listIteratorAt(block, i);
       BasicBlock newBlock = iterator.split(code);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/ConstantRemovalTest.java b/src/test/java/com/android/tools/r8/ir/optimize/ConstantRemovalTest.java
index eff9d7a..adc7fb7 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/ConstantRemovalTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/ConstantRemovalTest.java
@@ -87,11 +87,11 @@
     //
     // Then test that peephole optimization realizes that the last const number
     // is needed and the value 10 is *not* still in register 0 at that point.
+    IRMetadata metadata = IRMetadata.unknown();
     final NumberGenerator basicBlockNumberGenerator = new NumberGenerator();
-    BasicBlock block = new BasicBlock();
+    BasicBlock block = new BasicBlock(metadata);
     block.setNumber(basicBlockNumberGenerator.next());
 
-    IRMetadata metadata = IRMetadata.unknown();
     Position position = SyntheticPosition.builder().disableMethodCheck().setLine(0).build();
 
     Value v3 = new Value(3, TypeElement.getLong(), null);
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 997f0e0..275c0e3 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
@@ -69,10 +69,10 @@
     //   return
     final NumberGenerator basicBlockNumberGenerator = new NumberGenerator();
     Position position = SyntheticPosition.builder().setLine(0).disableMethodCheck().build();
-    BasicBlock block2 = new BasicBlock();
+    BasicBlock block2 = new BasicBlock(metadata);
     BasicBlock block0 =
         BasicBlock.createGotoBlock(basicBlockNumberGenerator.next(), position, metadata, block2);
-    BasicBlock block1 = new BasicBlock();
+    BasicBlock block1 = new BasicBlock(metadata);
     block1.setNumber(basicBlockNumberGenerator.next());
     block2.setNumber(basicBlockNumberGenerator.next());
     block0.setFilledForTesting();
@@ -133,9 +133,9 @@
     //   goto block3
     final NumberGenerator basicBlockNumberGenerator = new NumberGenerator();
     Position position = SyntheticPosition.builder().setLine(0).disableMethodCheck().build();
-    BasicBlock block0 = new BasicBlock();
+    BasicBlock block0 = new BasicBlock(metadata);
     block0.setNumber(basicBlockNumberGenerator.next());
-    BasicBlock block2 = new BasicBlock();
+    BasicBlock block2 = new BasicBlock(metadata);
     BasicBlock block1 =
         BasicBlock.createGotoBlock(basicBlockNumberGenerator.next(), position, metadata);
     block2.setNumber(basicBlockNumberGenerator.next());
@@ -144,7 +144,7 @@
     block2.add(ret, metadata);
     block2.setFilledForTesting();
 
-    BasicBlock block3 = new BasicBlock();
+    BasicBlock block3 = new BasicBlock(metadata);
     block3.setNumber(basicBlockNumberGenerator.next());
     Instruction instruction = new Goto();
     instruction.setPosition(position);
@@ -196,8 +196,8 @@
             IRMetadata.unknown(),
             MethodConversionOptions.forD8(appView));
     new TrivialGotosCollapser(appView).run(code, Timing.empty());
-    assertTrue(block0.getInstructions().get(1).isIf());
-    assertEquals(block1, block0.getInstructions().get(1).asIf().fallthroughBlock());
+    assertTrue(block0.getInstructions().getFirst().getNext().isIf());
+    assertEquals(block1, block0.getInstructions().getFirst().getNext().asIf().fallthroughBlock());
     assertTrue(blocks.containsAll(ImmutableList.of(block0, block1, block2, block3)));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerPhiDirectUserAfterInlineTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerPhiDirectUserAfterInlineTest.java
index 269b2f9..9ef01cc 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerPhiDirectUserAfterInlineTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerPhiDirectUserAfterInlineTest.java
@@ -169,7 +169,7 @@
       // and block 4 to have:
       // vY : phi(v3, vX)
       BasicBlock basicBlock = irCode.blocks.get(0);
-      Argument argument = basicBlock.getInstructions().get(0).asArgument();
+      Argument argument = basicBlock.getInstructions().getFirst().asArgument();
       assertNotNull(argument);
       Value argumentValue = argument.outValue();
 
@@ -203,7 +203,7 @@
       secondPhi.addOperands(ImmutableList.of(argumentValue, firstPhi));
 
       // Replace the invoke to use the phi
-      InstanceGet instanceGet = block1.getInstructions().get(0).asInstanceGet();
+      InstanceGet instanceGet = block1.getInstructions().getFirst().asInstanceGet();
       assertNotNull(instanceGet);
       assertEquals(A.class.getTypeName(), instanceGet.getField().holder.toSourceString());
       instanceGet.replaceValue(0, firstPhi);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/ifs/IfThrowNullPointerExceptionTest.java b/src/test/java/com/android/tools/r8/ir/optimize/ifs/IfThrowNullPointerExceptionTest.java
index b9c24d6..11fb04d 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/ifs/IfThrowNullPointerExceptionTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/ifs/IfThrowNullPointerExceptionTest.java
@@ -102,8 +102,10 @@
       assertTrue(entryBlock.getInstructions().getFirst().isArgument());
       assertTrue(entryBlock.getInstructions().getLast().isReturn());
 
-      Instruction nullCheckInstruction =
-          entryBlock.getInstructions().get(1 + BooleanUtils.intValue(isNPEWithMessage));
+      Instruction nullCheckInstruction = entryBlock.getInstructions().getFirst().getNext();
+      if (isNPEWithMessage) {
+        nullCheckInstruction = nullCheckInstruction.getNext();
+      }
       assertFalse(isNPEWithMessage);
       assertTrue(nullCheckInstruction.isInvokeVirtual());
       assertEquals(
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
index e3e4fa2..864b64b 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
@@ -19,8 +19,11 @@
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.dex.code.DexInstruction;
 import com.android.tools.r8.dex.code.DexInvokeDirect;
+import com.android.tools.r8.dex.code.DexInvokeDirectRange;
 import com.android.tools.r8.dex.code.DexInvokeStatic;
+import com.android.tools.r8.dex.code.DexInvokeStaticRange;
 import com.android.tools.r8.dex.code.DexInvokeVirtual;
+import com.android.tools.r8.dex.code.DexInvokeVirtualRange;
 import com.android.tools.r8.dex.code.DexSgetObject;
 import com.android.tools.r8.dex.code.DexSputObject;
 import com.android.tools.r8.graph.DexCode;
@@ -377,19 +380,16 @@
                 .map(DexInstruction::getField)
                 .filter(fld -> isTypeOfInterest(fld.holder))
                 .map(DexField::toSourceString),
-            filterInstructionKind(code, DexInvokeStatic.class)
-                .map(insn -> (DexInvokeStatic) insn)
-                .map(DexInvokeStatic::getMethod)
+            filterInstructionKind(code, DexInvokeStatic.class, DexInvokeStaticRange.class)
+                .map(DexInstruction::getMethod)
                 .filter(method -> isTypeOfInterest(method.holder))
                 .map(method -> "STATIC: " + method.toSourceString()),
-            filterInstructionKind(code, DexInvokeVirtual.class)
-                .map(insn -> (DexInvokeVirtual) insn)
-                .map(DexInvokeVirtual::getMethod)
+            filterInstructionKind(code, DexInvokeVirtual.class, DexInvokeVirtualRange.class)
+                .map(DexInstruction::getMethod)
                 .filter(method -> isTypeOfInterest(method.holder))
                 .map(method -> "VIRTUAL: " + method.toSourceString()),
-            filterInstructionKind(code, DexInvokeDirect.class)
-                .map(insn -> (DexInvokeDirect) insn)
-                .map(DexInvokeDirect::getMethod)
+            filterInstructionKind(code, DexInvokeDirect.class, DexInvokeDirectRange.class)
+                .map(DexInstruction::getMethod)
                 .filter(method -> isTypeOfInterest(method.holder))
                 .map(method -> "DIRECT: " + method.toSourceString()))
         .map(txt -> txt.replace("java.lang.", ""))
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/ArgumentInLowRegisterWithMoreThan16RegistersTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/ArgumentInLowRegisterWithMoreThan16RegistersTest.java
index 24fbe6d..e5a34c5 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/ArgumentInLowRegisterWithMoreThan16RegistersTest.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/ArgumentInLowRegisterWithMoreThan16RegistersTest.java
@@ -44,7 +44,7 @@
                   inspector.clazz(Main.class).uniqueInstanceInitializer();
               assertThat(testMethodSubject, isPresent());
               assertEquals(
-                  2,
+                  1,
                   testMethodSubject
                       .streamInstructions()
                       .filter(InstructionSubject::isMove)
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/InvokeRangeAll4BitRegistersTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/InvokeRangeAll4BitRegistersTest.java
new file mode 100644
index 0000000..42b845d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/InvokeRangeAll4BitRegistersTest.java
@@ -0,0 +1,93 @@
+// Copyright (c) 2024, 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.regalloc;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+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 InvokeRangeAll4BitRegistersTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultDexRuntime().withMaximumApiLevel().build();
+  }
+
+  @Test
+  public void testD8Debug() throws Exception {
+    testForD8()
+        .addInnerClasses(getClass())
+        .debug()
+        .setMinApi(parameters)
+        .compile()
+        .runDex2Oat(parameters.getRuntime())
+        .assertNoVerificationErrors();
+  }
+
+  @Test
+  public void testD8Release() throws Exception {
+    testForD8()
+        .addInnerClasses(getClass())
+        .release()
+        .setMinApi(parameters)
+        .compile()
+        .runDex2Oat(parameters.getRuntime())
+        .assertNoVerificationErrors();
+  }
+
+  static class Main {
+
+    Main f;
+
+    static void test() {
+      int a0 = 0;
+      int a1 = 1;
+      int a2 = 2;
+      int a3 = 3;
+      int a4 = 4;
+      int a5 = 5;
+      int a6 = 6;
+      int a7 = 7;
+      int a8 = 8;
+      int a9 = 9;
+      int a10 = 10;
+      int a11 = 11;
+      int a12 = 12;
+      int a13 = 13;
+      int a14 = 14;
+      int a15 = 15;
+      Main main = accept16(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15);
+      main.f = main;
+    }
+
+    static Main accept16(
+        int a0,
+        int a1,
+        int a2,
+        int a3,
+        int a4,
+        int a5,
+        int a6,
+        int a7,
+        int a8,
+        int a9,
+        int a10,
+        int a11,
+        int a12,
+        int a13,
+        int a14,
+        int a15) {
+      return null;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/InvokeRangeAll4BitRegistersWideTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/InvokeRangeAll4BitRegistersWideTest.java
new file mode 100644
index 0000000..f5a7060
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/InvokeRangeAll4BitRegistersWideTest.java
@@ -0,0 +1,94 @@
+// Copyright (c) 2024, 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.regalloc;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+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 InvokeRangeAll4BitRegistersWideTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultDexRuntime().withMaximumApiLevel().build();
+  }
+
+  @Test
+  public void testD8Debug() throws Exception {
+    testForD8()
+        .addInnerClasses(getClass())
+        .debug()
+        .setMinApi(parameters)
+        .compile()
+        .runDex2Oat(parameters.getRuntime())
+        .assertNoVerificationErrors();
+  }
+
+  @Test
+  public void testD8Release() throws Exception {
+    testForD8()
+        .addInnerClasses(getClass())
+        .release()
+        .setMinApi(parameters)
+        .compile()
+        .runDex2Oat(parameters.getRuntime())
+        .assertNoVerificationErrors();
+  }
+
+  static class Main {
+
+    long f;
+
+    static void test() {
+      int a0 = 0;
+      int a1 = 1;
+      int a2 = 2;
+      int a3 = 3;
+      int a4 = 4;
+      int a5 = 5;
+      int a6 = 6;
+      int a7 = 7;
+      int a8 = 8;
+      int a9 = 9;
+      int a10 = 10;
+      int a11 = 11;
+      int a12 = 12;
+      int a13 = 13;
+      int a14 = 14;
+      int a15 = 15;
+      long wide = accept16(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15);
+      Main main = null;
+      main.f = wide;
+    }
+
+    static long accept16(
+        int a0,
+        int a1,
+        int a2,
+        int a3,
+        int a4,
+        int a5,
+        int a6,
+        int a7,
+        int a8,
+        int a9,
+        int a10,
+        int a11,
+        int a12,
+        int a13,
+        int a14,
+        int a15) {
+      return 0;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/InvokeRangeWithSameValueRepeatedTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/InvokeRangeWithSameValueRepeatedTest.java
new file mode 100644
index 0000000..95ade31
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/InvokeRangeWithSameValueRepeatedTest.java
@@ -0,0 +1,50 @@
+// Copyright (c) 2024, 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.regalloc;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+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 InvokeRangeWithSameValueRepeatedTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultDexRuntime().withMaximumApiLevel().build();
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    testForD8()
+        .addInnerClasses(getClass())
+        .addOptionsModification(
+            options -> {
+              options.getTestingOptions().enableRegisterAllocation8BitRefinement = true;
+              options.getTestingOptions().enableRegisterHintsForBlockedRegisters = true;
+            })
+        .release()
+        .setMinApi(parameters)
+        .compile()
+        .runDex2Oat(parameters.getRuntime())
+        .assertNoVerificationErrors();
+  }
+
+  static class Main {
+
+    void test(long a) {
+      invoke(a, a, a);
+    }
+
+    static void invoke(long a, long b, long c) {}
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/RedundantSpillingBeforeInvokeRangeTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/RedundantSpillingBeforeInvokeRangeTest.java
index 9ead116..1b947ab 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/RedundantSpillingBeforeInvokeRangeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/RedundantSpillingBeforeInvokeRangeTest.java
@@ -35,7 +35,6 @@
         .addInnerClasses(getClass())
         .addOptionsModification(
             options -> {
-              options.getTestingOptions().enableLiveIntervalsSplittingForInvokeRange = true;
               options.getTestingOptions().enableRegisterAllocation8BitRefinement = true;
               options.getTestingOptions().enableRegisterHintsForBlockedRegisters = true;
             })
@@ -62,7 +61,7 @@
 
     Main(long a, long b, long c, long d, long e, long f, long g, long h) {}
 
-    void test(long a, long b, long c, long d, long e, long f, long g, long h) {
+    Main test(long a, long b, long c, long d, long e, long f, long g, long h) {
       forceIntoLowRegister(a, a);
       forceIntoLowRegister(b, b);
       forceIntoLowRegister(c, c);
@@ -71,7 +70,7 @@
       forceIntoLowRegister(f, f);
       forceIntoLowRegister(g, g);
       forceIntoLowRegister(h, h);
-      new Main(a, b, c, d, e, f, g, h);
+      return new Main(a, b, c, d, e, f, g, h);
     }
 
     static void forceIntoLowRegister(long a, long b) {}
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
index ed272e7..804b482 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
@@ -5,6 +5,9 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
@@ -39,8 +42,12 @@
 import java.util.function.Consumer;
 import java.util.function.UnaryOperator;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
 
-public class RegisterMoveSchedulerTest {
+@RunWith(Parameterized.class)
+public class RegisterMoveSchedulerTest extends TestBase {
 
   private static class CollectMovesIterator implements InstructionListIterator {
 
@@ -56,7 +63,8 @@
     }
 
     @Override
-    public void replaceCurrentInstruction(Instruction newInstruction, Set<Value> affectedValues) {
+    public void replaceCurrentInstruction(
+        Instruction newInstruction, AffectedValues affectedValues) {
       throw new Unimplemented();
     }
 
@@ -121,7 +129,7 @@
 
     @Override
     public void replaceCurrentInstructionWithStaticGet(
-        AppView<?> appView, IRCode code, DexField field, Set<Value> affectedValues) {
+        AppView<?> appView, IRCode code, DexField field, AffectedValues affectedValues) {
       throw new Unimplemented();
     }
 
@@ -243,6 +251,15 @@
     }
   }
 
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public RegisterMoveSchedulerTest(TestParameters parameters) {
+    parameters.assertNoneRuntime();
+  }
+
   @Test
   public void testSingleParallelMove() {
     CollectMovesIterator moves = new CollectMovesIterator();
@@ -371,18 +388,10 @@
     scheduler.addMove(new RegisterMove(0, 3, TypeElement.getLong()));
     scheduler.schedule();
     assertEquals(3, moves.size());
-    Move firstMove = moves.get(0).asMove();
-    Move secondMove = moves.get(1).asMove();
-    Move thirdMove = moves.get(2).asMove();
-    assertEquals(ValueType.LONG, firstMove.outType());
-    assertEquals(ValueType.LONG, secondMove.outType());
-    assertEquals(ValueType.LONG, thirdMove.outType());
-    assertEquals(42, firstMove.dest().asFixedRegisterValue().getRegister());
-    assertEquals(1, firstMove.src().asFixedRegisterValue().getRegister());
-    assertEquals(0, secondMove.dest().asFixedRegisterValue().getRegister());
-    assertEquals(3, secondMove.src().asFixedRegisterValue().getRegister());
-    assertEquals(2, thirdMove.dest().asFixedRegisterValue().getRegister());
-    assertEquals(42, thirdMove.src().asFixedRegisterValue().getRegister());
+    assertEquals("42 <- 3", toString(moves.get(0)));
+    assertEquals("2 <- 1", toString(moves.get(1)));
+    assertEquals("0 <- 42", toString(moves.get(2)));
+    assertEquals(2, scheduler.getUsedTempRegisters());
   }
 
   @Test
@@ -443,12 +452,13 @@
     scheduler.addMove(new RegisterMove(12, 19, TypeElement.getLong()));
     scheduler.schedule();
     // In order to resolve these moves, we need to use two temporary register pairs.
-    assertEquals(6, moves.size());
-    assertEquals(42, moves.get(0).asMove().dest().asFixedRegisterValue().getRegister());
-    assertEquals(11, moves.get(0).asMove().src().asFixedRegisterValue().getRegister());
-    assertEquals(44, moves.get(1).asMove().dest().asFixedRegisterValue().getRegister());
-    assertEquals(13, moves.get(1).asMove().src().asFixedRegisterValue().getRegister());
-    assertEquals(12, moves.get(2).asMove().dest().asFixedRegisterValue().getRegister());
+    assertEquals(5, moves.size());
+    assertEquals("42 <- 13", toString(moves.get(0)));
+    assertEquals("14 <- 11", toString(moves.get(1)));
+    assertEquals("10 <- 17", toString(moves.get(2)));
+    assertEquals("16 <- 42", toString(moves.get(3)));
+    assertEquals("12 <- 19", toString(moves.get(4)));
+    assertEquals(2, scheduler.getUsedTempRegisters());
   }
 
   @Test
@@ -470,19 +480,68 @@
     scheduler.addMove(new RegisterMove(23, 28, TypeElement.getLong()));
     scheduler.schedule();
     // For this example we need recursive unblocking.
-    assertEquals(6, moves.size());
-    assertEquals(42, moves.get(0).asMove().dest().asFixedRegisterValue().getRegister());
-    assertEquals(26, moves.get(0).asMove().src().asFixedRegisterValue().getRegister());
-    assertEquals(26, moves.get(1).asMove().dest().asFixedRegisterValue().getRegister());
-    assertEquals(22, moves.get(1).asMove().src().asFixedRegisterValue().getRegister());
-    assertEquals(43, moves.get(2).asMove().dest().asFixedRegisterValue().getRegister());
-    assertEquals(28, moves.get(2).asMove().src().asFixedRegisterValue().getRegister());
-    assertEquals(28, moves.get(3).asMove().dest().asFixedRegisterValue().getRegister());
-    assertEquals(42, moves.get(3).asMove().src().asFixedRegisterValue().getRegister());
-    assertEquals(29, moves.get(4).asMove().dest().asFixedRegisterValue().getRegister());
-    assertEquals(24, moves.get(4).asMove().src().asFixedRegisterValue().getRegister());
-    assertEquals(23, moves.get(5).asMove().dest().asFixedRegisterValue().getRegister());
-    assertEquals(43, moves.get(5).asMove().src().asFixedRegisterValue().getRegister());
+    assertEquals(5, moves.size());
+    assertEquals("42 <- 28", toString(moves.get(0)));
+    assertEquals("29 <- 24", toString(moves.get(1)));
+    assertEquals("23 <- 42", toString(moves.get(2)));
+    assertEquals("28 <- 26", toString(moves.get(3)));
+    assertEquals("26 <- 22", toString(moves.get(4)));
+    assertEquals(2, scheduler.getUsedTempRegisters());
+  }
+
+  @Test
+  public void reuseTempRegister() {
+    CollectMovesIterator moves = new CollectMovesIterator();
+    int temp = 42;
+    RegisterMoveScheduler scheduler = new RegisterMoveScheduler(moves, temp);
+    scheduler.addMove(new RegisterMove(0, 1, TypeElement.getInt()));
+    scheduler.addMove(new RegisterMove(1, 0, TypeElement.getInt()));
+    scheduler.addMove(new RegisterMove(2, 3, TypeElement.getInt()));
+    scheduler.addMove(new RegisterMove(3, 2, TypeElement.getInt()));
+    scheduler.schedule();
+    // Verify that the temp register has been reused.
+    assertEquals("42 <- 1", toString(moves.get(0)));
+    assertEquals("1 <- 0", toString(moves.get(1)));
+    assertEquals("0 <- 42", toString(moves.get(2)));
+    assertEquals("42 <- 3", toString(moves.get(3)));
+    assertEquals("3 <- 2", toString(moves.get(4)));
+    assertEquals("2 <- 42", toString(moves.get(5)));
+    assertEquals(1, scheduler.getUsedTempRegisters());
+  }
+
+  @Test
+  public void useDestinationRegisterAsTemporary() {
+    CollectMovesIterator moves = new CollectMovesIterator();
+    int temp = 42;
+    RegisterMoveScheduler scheduler = new RegisterMoveScheduler(moves, temp);
+    scheduler.addMove(new RegisterMove(0, 1, TypeElement.getInt()));
+    scheduler.addMove(new RegisterMove(1, 0, TypeElement.getInt()));
+    scheduler.addMove(new RegisterMove(2, 3, TypeElement.getInt()));
+    scheduler.schedule();
+    // Verify that the temp register has been reused.
+    assertEquals("2 <- 1", toString(moves.get(0)));
+    assertEquals("1 <- 0", toString(moves.get(1)));
+    assertEquals("0 <- 2", toString(moves.get(2)));
+    assertEquals("2 <- 3", toString(moves.get(3)));
+    assertEquals(0, scheduler.getUsedTempRegisters());
+  }
+
+  @Test
+  public void openMoveCycle() {
+    CollectMovesIterator moves = new CollectMovesIterator();
+    int temp = 42;
+    RegisterMoveScheduler scheduler = new RegisterMoveScheduler(moves, temp);
+    scheduler.addMove(new RegisterMove(2, 0, TypeElement.getInt()));
+    scheduler.addMove(new RegisterMove(0, 2, TypeElement.getLong()));
+    scheduler.addMove(new RegisterMove(4, 1, TypeElement.getInt()));
+    scheduler.schedule();
+    // The cycle is blocked by the move 4 <- 1, so we should emit this move first.
+    assertEquals(4, moves.size());
+    assertEquals("4 <- 1", toString(moves.get(0)));
+    assertEquals("42 <- 2", toString(moves.get(1)));
+    assertEquals("2 <- 0", toString(moves.get(2)));
+    assertEquals("0 <- 42", toString(moves.get(3)));
+    assertEquals(2, scheduler.getUsedTempRegisters());
   }
 
   // Debugging aid.
@@ -495,4 +554,10 @@
     }
     System.out.println("----------------");
   }
+
+  private String toString(Instruction move) {
+    return move.outValue().asFixedRegisterValue().getRegister()
+        + " <- "
+        + move.getFirstOperand().asFixedRegisterValue().getRegister();
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/SpillToHighUnusedArgumentRegisterTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/SpillToHighUnusedArgumentRegisterTest.java
index f88127a..15ff3af 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/SpillToHighUnusedArgumentRegisterTest.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/SpillToHighUnusedArgumentRegisterTest.java
@@ -5,12 +5,11 @@
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.dex.code.DexMoveFrom16;
 import com.android.tools.r8.dex.code.DexMoveResult;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -57,17 +56,13 @@
                       .asDexInstruction()
                       .getInstruction();
 
-              DexMoveFrom16 spillMove =
+              // TODO(b/375142715): The test no longer spills the `i` value. Look into if the test
+              //  can be tweeked so that `i` is spilled, and validate that it is spilled to the
+              //  unused argument register.
+              assertTrue(
                   testMethodSubject
                       .streamInstructions()
-                      .filter(i -> i.isMoveFrom(moveResult.AA))
-                      .collect(MoreCollectors.onlyElement())
-                      .asDexInstruction()
-                      .getInstruction();
-              int firstArgumentRegister = code.registerSize - code.incomingRegisterSize;
-              // TODO(b/375142715): We could have spilled this value to the unused argument
-              //  register, which would have lead to fewer registers being used.
-              assertEquals(firstArgumentRegister - 1, spillMove.AA);
+                      .noneMatch(i -> i.isMoveFrom(moveResult.AA)));
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/ValueUsedInMultipleInvokeRangeInstructionsTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/ValueUsedInMultipleInvokeRangeInstructionsTest.java
new file mode 100644
index 0000000..152a959
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/ValueUsedInMultipleInvokeRangeInstructionsTest.java
@@ -0,0 +1,51 @@
+// Copyright (c) 2024, 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.regalloc;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+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 ValueUsedInMultipleInvokeRangeInstructionsTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultDexRuntime().withMaximumApiLevel().build();
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    testForD8()
+        .addInnerClasses(getClass())
+        .addOptionsModification(
+            options -> {
+              options.getTestingOptions().enableRegisterAllocation8BitRefinement = true;
+              options.getTestingOptions().enableRegisterHintsForBlockedRegisters = true;
+            })
+        .release()
+        .setMinApi(parameters)
+        .compile()
+        .runDex2Oat(parameters.getRuntime())
+        .assertNoVerificationErrors();
+  }
+
+  static class Main {
+
+    void test(long a) {
+      invoke(a, a, a);
+      invoke(a, a, a);
+    }
+
+    static void invoke(long a, long b, long c) {}
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/metadata/D8BuildMetadataTest.java b/src/test/java/com/android/tools/r8/metadata/D8BuildMetadataTest.java
index a10e2cc..79519aa 100644
--- a/src/test/java/com/android/tools/r8/metadata/D8BuildMetadataTest.java
+++ b/src/test/java/com/android/tools/r8/metadata/D8BuildMetadataTest.java
@@ -73,7 +73,7 @@
     if (parameters.isDexRuntime()) {
       assertNotNull(libraryDesugaringMetadata);
       assertEquals(
-          "com.tools.android:desugar_jdk_libs_configuration:2.1.2",
+          "com.tools.android:desugar_jdk_libs_configuration:2.1.3",
           libraryDesugaringMetadata.getIdentifier());
     } else {
       assertNull(libraryDesugaringMetadata);
diff --git a/src/test/java/com/android/tools/r8/metadata/R8BuildMetadataTest.java b/src/test/java/com/android/tools/r8/metadata/R8BuildMetadataTest.java
index 0216ecc..c1e2c35 100644
--- a/src/test/java/com/android/tools/r8/metadata/R8BuildMetadataTest.java
+++ b/src/test/java/com/android/tools/r8/metadata/R8BuildMetadataTest.java
@@ -152,7 +152,7 @@
     if (parameters.isDexRuntime()) {
       assertNotNull(libraryDesugaringMetadata);
       assertEquals(
-          "com.tools.android:desugar_jdk_libs_configuration:2.1.2",
+          "com.tools.android:desugar_jdk_libs_configuration:2.1.3",
           libraryDesugaringMetadata.getIdentifier());
     } else {
       assertNull(libraryDesugaringMetadata);
diff --git a/src/test/java/com/android/tools/r8/naming/IdentifierNameStringMarkerTest.java b/src/test/java/com/android/tools/r8/naming/IdentifierNameStringMarkerTest.java
index cc150aa..42a037e 100644
--- a/src/test/java/com/android/tools/r8/naming/IdentifierNameStringMarkerTest.java
+++ b/src/test/java/com/android/tools/r8/naming/IdentifierNameStringMarkerTest.java
@@ -18,9 +18,10 @@
 import com.android.tools.r8.dex.code.DexConst4;
 import com.android.tools.r8.dex.code.DexConstClass;
 import com.android.tools.r8.dex.code.DexConstString;
-import com.android.tools.r8.dex.code.DexFilledNewArray;
-import com.android.tools.r8.dex.code.DexInvokeDirect;
+import com.android.tools.r8.dex.code.DexFilledNewArrayRange;
+import com.android.tools.r8.dex.code.DexInvokeDirectRange;
 import com.android.tools.r8.dex.code.DexInvokeStatic;
+import com.android.tools.r8.dex.code.DexInvokeStaticRange;
 import com.android.tools.r8.dex.code.DexInvokeVirtual;
 import com.android.tools.r8.dex.code.DexIputObject;
 import com.android.tools.r8.dex.code.DexMoveResultObject;
@@ -85,7 +86,10 @@
     checkInstructions(
         code,
         ImmutableList.of(
-            DexInvokeDirect.class, DexConstString.class, DexIputObject.class, DexReturnVoid.class));
+            DexInvokeDirectRange.class,
+            DexConstString.class,
+            DexIputObject.class,
+            DexReturnVoid.class));
     DexConstString constString = (DexConstString) code.instructions[1];
     assertEquals(BOO, constString.getString().toString());
   }
@@ -119,7 +123,7 @@
     checkInstructions(
         code,
         ImmutableList.of(
-            DexInvokeDirect.class,
+            DexInvokeDirectRange.class,
             DexSgetObject.class,
             DexConstString.class,
             DexInvokeVirtual.class,
@@ -160,7 +164,7 @@
     checkInstructions(
         code,
         ImmutableList.of(
-            DexInvokeDirect.class,
+            DexInvokeDirectRange.class,
             DexSgetObject.class,
             DexConstString.class,
             DexInvokeVirtual.class,
@@ -411,7 +415,7 @@
     checkInstructions(
         code,
         ImmutableList.of(
-            DexInvokeDirect.class,
+            DexInvokeDirectRange.class,
             DexConstString.class,
             DexConstString.class,
             DexInvokeStatic.class,
@@ -455,11 +459,11 @@
     checkInstructions(
         code,
         ImmutableList.of(
-            DexInvokeDirect.class,
+            DexInvokeDirectRange.class,
             DexSgetObject.class,
             DexConstString.class,
             DexInvokeVirtual.class,
-            DexInvokeStatic.class,
+            DexInvokeStaticRange.class,
             DexReturnVoid.class));
     DexConstString constString = (DexConstString) code.instructions[2];
     assertEquals(BOO, constString.getString().toString());
@@ -500,12 +504,12 @@
     checkInstructions(
         code,
         ImmutableList.of(
-            DexInvokeDirect.class,
+            DexInvokeDirectRange.class,
             DexSgetObject.class,
             DexConstString.class,
             DexInvokeVirtual.class,
             DexConstString.class,
-            DexInvokeStatic.class,
+            DexInvokeStaticRange.class,
             DexReturnVoid.class));
     DexConstString constString = (DexConstString) code.instructions[2];
     assertEquals(BOO, constString.getString().toString());
@@ -555,7 +559,7 @@
     checkInstructions(
         code,
         ImmutableList.of(
-            DexInvokeDirect.class,
+            DexInvokeDirectRange.class,
             DexConstClass.class,
             DexConstString.class,
             DexInvokeStatic.class,
@@ -606,7 +610,7 @@
     checkInstructions(
         code,
         ImmutableList.of(
-            DexInvokeDirect.class,
+            DexInvokeDirectRange.class,
             DexConstClass.class,
             DexConstString.class,
             DexInvokeStatic.class,
@@ -662,13 +666,13 @@
 
     DexCode code = method.getCode().asDexCode();
     // Accept either array construction style (differs based on minSdkVersion).
-    if (code.instructions[2].getClass() == DexFilledNewArray.class) {
+    if (code.instructions[2].getClass() == DexFilledNewArrayRange.class) {
       checkInstructions(
           code,
           ImmutableList.of(
-              DexInvokeDirect.class,
+              DexInvokeDirectRange.class,
               DexConstClass.class,
-              DexFilledNewArray.class,
+              DexFilledNewArrayRange.class,
               DexMoveResultObject.class,
               DexConstString.class,
               DexInvokeStatic.class,
@@ -677,7 +681,7 @@
       checkInstructions(
           code,
           ImmutableList.of(
-              DexInvokeDirect.class,
+              DexInvokeDirectRange.class,
               DexConstClass.class,
               DexConst4.class,
               DexNewArray.class,
@@ -738,13 +742,13 @@
 
     DexCode code = method.getCode().asDexCode();
     // Accept either array construction style (differs based on minSdkVersion).
-    if (code.instructions[2].getClass() == DexFilledNewArray.class) {
+    if (code.instructions[2].getClass() == DexFilledNewArrayRange.class) {
       checkInstructions(
           code,
           ImmutableList.of(
-              DexInvokeDirect.class,
+              DexInvokeDirectRange.class,
               DexConstClass.class,
-              DexFilledNewArray.class,
+              DexFilledNewArrayRange.class,
               DexMoveResultObject.class,
               DexConstString.class,
               DexInvokeStatic.class,
@@ -753,7 +757,7 @@
       checkInstructions(
           code,
           ImmutableList.of(
-              DexInvokeDirect.class,
+              DexInvokeDirectRange.class,
               DexConstClass.class,
               DexConst4.class,
               DexNewArray.class,
diff --git a/src/test/java/com/android/tools/r8/optimize/ReproduceKT72888Test.java b/src/test/java/com/android/tools/r8/optimize/ReproduceKT72888Test.java
new file mode 100644
index 0000000..a61ef15
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/ReproduceKT72888Test.java
@@ -0,0 +1,111 @@
+// Copyright (c) 2024, 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.optimize;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+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 ReproduceKT72888Test extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  private static final String EXPECTED_OUTPUT = StringUtils.lines("3", "4", "null");
+
+  @Test
+  public void testJvm() throws Exception {
+    parameters.assumeJvmTestParameters();
+    testForJvm(parameters)
+        .addInnerClasses(getClass())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .addKeepClassRules(MyAnnotation.class, A.class, B.class)
+        .addKeepAttributeAnnotationDefault()
+        .addKeepRuntimeVisibleAnnotations()
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertFailureWithErrorThatThrows(ClassCastException.class);
+  }
+
+  @Retention(RetentionPolicy.RUNTIME)
+  @interface MyAnnotation {
+    int x() default 0;
+  }
+
+  public static class MyAnnotationImpl implements MyAnnotation {
+    private final int x;
+
+    public MyAnnotationImpl(int x) {
+      this.x = x + 2;
+    }
+
+    @Override
+    public int x() {
+      return x;
+    }
+
+    @Override
+    public Class<? extends java.lang.annotation.Annotation> annotationType() {
+      return MyAnnotation.class;
+    }
+  }
+
+  @MyAnnotation(x = 1)
+  static class A {}
+
+  @MyAnnotation(x = 2)
+  static class B {}
+
+  static class TestClass {
+    public static MyAnnotation copyOfMyAnnotation(Class<?> clazz) {
+      Object a = clazz.getAnnotation(MyAnnotation.class);
+      if (a == null) {
+        return null;
+      }
+      MyAnnotation copy = (MyAnnotation) a;
+      return new MyAnnotationImpl(copy.x());
+    }
+
+    public static void main(String[] args) {
+      MyAnnotation a = copyOfMyAnnotation(A.class);
+      System.out.println(a == null ? "null" : a.x());
+      a = copyOfMyAnnotation(B.class);
+      System.out.println(a == null ? "null" : a.x());
+      a = copyOfMyAnnotation(TestClass.class);
+      System.out.println(a == null ? "null" : a.x());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/regress/Regress302826300.java b/src/test/java/com/android/tools/r8/regress/Regress302826300.java
index d8cda04..baa5061 100644
--- a/src/test/java/com/android/tools/r8/regress/Regress302826300.java
+++ b/src/test/java/com/android/tools/r8/regress/Regress302826300.java
@@ -45,7 +45,7 @@
             .setMinApi(parameters)
             .run(parameters.getRuntime(), Foo.class);
     if (parameters.getRuntime().asDex().getVersion().isDalvik()) {
-      run.assertFailureWithErrorThatMatches(containsString("rejecting opcode 0x6e"));
+      run.assertFailureWithErrorThatMatches(containsString("rejecting opcode 0x"));
     } else {
       run.assertSuccessWithOutputLines(EXPECTED);
     }
diff --git a/src/test/java/com/android/tools/r8/regress/b68378480/B68378480.java b/src/test/java/com/android/tools/r8/regress/b68378480/B68378480.java
index 2492b31..7b0ca00 100644
--- a/src/test/java/com/android/tools/r8/regress/b68378480/B68378480.java
+++ b/src/test/java/com/android/tools/r8/regress/b68378480/B68378480.java
@@ -25,7 +25,6 @@
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.List;
-import java.util.concurrent.ExecutionException;
 import org.junit.Test;
 
 class SuperClass {
@@ -68,16 +67,14 @@
   }
 
   @Test
-  public void addExtraLocalToConstructor()
-      throws IOException, CompilationFailedException, ExecutionException {
+  public void addExtraLocalToConstructor() throws IOException, CompilationFailedException {
     DexCode code = compileClassesGetSubClassInit(AndroidApiLevel.L_MR1.getLevel());
     assertTrue(code.registerSize > code.incomingRegisterSize);
     assertTrue(Arrays.stream(code.instructions).anyMatch((i) -> i instanceof SingleConstant));
   }
 
   @Test
-  public void doNotAddExtraLocalToConstructor()
-      throws IOException, CompilationFailedException, ExecutionException {
+  public void doNotAddExtraLocalToConstructor() throws IOException, CompilationFailedException {
     DexCode code = compileClassesGetSubClassInit(AndroidApiLevel.M.getLevel());
     assertEquals(code.registerSize, code.incomingRegisterSize);
     assertTrue(Arrays.stream(code.instructions).noneMatch((i) -> i instanceof SingleConstant));
diff --git a/src/test/java/com/android/tools/r8/regress/b77496850/B77496850.java b/src/test/java/com/android/tools/r8/regress/b77496850/B77496850.java
index 375b845..c84f519 100644
--- a/src/test/java/com/android/tools/r8/regress/b77496850/B77496850.java
+++ b/src/test/java/com/android/tools/r8/regress/b77496850/B77496850.java
@@ -8,7 +8,9 @@
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.dex.Marker.Tool;
+import com.android.tools.r8.dex.code.DexInstruction;
 import com.android.tools.r8.dex.code.DexInvokeStatic;
+import com.android.tools.r8.dex.code.DexInvokeStaticRange;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
@@ -418,10 +420,9 @@
         factory.createString("isNaN"),
         factory.booleanDescriptor,
         new DexString[]{factory.doubleDescriptor});
-    for (int i = 0; i < code.instructions.length; i++) {
-      if (code.instructions[i] instanceof DexInvokeStatic) {
-        DexInvokeStatic invoke = (DexInvokeStatic) code.instructions[i];
-        if (invoke.getMethod() == doubleIsNaN) {
+    for (DexInstruction instruction : code.instructions) {
+      if (instruction instanceof DexInvokeStatic || instruction instanceof DexInvokeStaticRange) {
+        if (instruction.getMethod() == doubleIsNaN) {
           count++;
         }
       }
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
index 80ea5f5..359ccd0 100644
--- a/src/test/java/com/android/tools/r8/rewrite/arrays/SimplifyArrayConstructionTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/arrays/SimplifyArrayConstructionTest.java
@@ -53,7 +53,7 @@
   }
 
   private static final Class<?>[] DEX_ARRAY_INSTRUCTIONS = {
-    DexNewArray.class, DexFilledNewArray.class, DexFilledNewArrayRange.class, DexFillArrayData.class
+    DexNewArray.class, DexFilledNewArray.class, DexFillArrayData.class
   };
 
   private static final String[] EXPECTED_OUTPUT = {
@@ -334,12 +334,15 @@
 
   private static void assertArrayTypes(MethodSubject method, Class<?>... allowedArrayInst) {
     assertTrue(method.isPresent());
+    List<Class<?>> allowedClasses = Lists.newArrayList(allowedArrayInst);
+    if (allowedClasses.contains(DexFilledNewArray.class)) {
+      allowedClasses.add(DexFilledNewArrayRange.class);
+    }
     List<Class<?>> disallowedClasses = Lists.newArrayList(DEX_ARRAY_INSTRUCTIONS);
     for (Class<?> allowedArr : allowedArrayInst) {
       disallowedClasses.remove(allowedArr);
     }
-    assertTrue(
-        method.streamInstructions().anyMatch(isInstruction(Arrays.asList(allowedArrayInst))));
+    assertTrue(method.streamInstructions().anyMatch(isInstruction(allowedClasses)));
     assertTrue(method.streamInstructions().noneMatch(isInstruction(disallowedClasses)));
   }
 
diff --git a/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesFromClasspathOrLibraryTest.java b/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesFromClasspathOrLibraryTest.java
index 552e81d..d8fb33f 100644
--- a/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesFromClasspathOrLibraryTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/LibraryProvidedProguardRulesFromClasspathOrLibraryTest.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
 import static com.android.tools.r8.DiagnosticsMatcher.diagnosticOrigin;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
 import static com.android.tools.r8.OriginMatcher.hasPart;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
@@ -28,6 +29,7 @@
 import com.android.tools.r8.ResourceException;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.errors.DuplicateTypesDiagnostic;
 import com.android.tools.r8.origin.ArchiveEntryOrigin;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApiLevel;
@@ -50,10 +52,6 @@
 public class LibraryProvidedProguardRulesFromClasspathOrLibraryTest
     extends LibraryProvidedProguardRulesTestBase {
 
-  @interface Keep {}
-
-  public interface Interface {}
-
   private enum HowToAdd {
     API_CLASSPATH,
     API_LIBRARY,
@@ -108,6 +106,33 @@
   }
 
   @Test
+  public void testDuplicateClassesInLibraryJar() throws Exception {
+    assumeTrue(howToAdd == HowToAdd.API_LIBRARY);
+    assumeTrue(libraryType == LibraryType.JAR_WITH_RULES);
+    Path library =
+        buildLibrary(
+            ImmutableList.of(
+                "-keep class * implements " + Interface.class.getTypeName() + " { *; }"));
+    assertThrows(
+        CompilationFailedException.class,
+        () ->
+            testForR8(parameters.getBackend())
+                .addProgramClasses(A.class, B.class)
+                .addKeepRules("-libraryjars " + library.toAbsolutePath())
+                .addKeepRules("-libraryjars " + library.toAbsolutePath())
+                .setMinApi(parameters)
+                .allowStdoutMessages()
+                .apply(
+                    b ->
+                        ToolHelper.setReadEmbeddedRulesFromClasspathAndLibrary(
+                            b.getBuilder(), true))
+                .compileWithExpectedDiagnostics(
+                    diagnostics ->
+                        diagnostics.assertErrorsMatch(
+                            diagnosticType(DuplicateTypesDiagnostic.class))));
+  }
+
+  @Test
   public void providedKeepRuleImplements() throws Exception {
     CodeInspector inspector =
         runTest("-keep class * implements " + Interface.class.getTypeName() + " { *; }");
@@ -263,6 +288,12 @@
                                 diagnosticOrigin(is(Origin.unknown()))))));
   }
 
+  // Classes in the library.
+  @interface Keep {}
+
+  public interface Interface {}
+
+  // Classes in the program.
   static class A implements Interface {}
 
   @Keep
diff --git a/src/test/java/com/android/tools/r8/shaking/enums/EnumCollectionsTest.java b/src/test/java/com/android/tools/r8/shaking/enums/EnumCollectionsTest.java
new file mode 100644
index 0000000..2a9872c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/enums/EnumCollectionsTest.java
@@ -0,0 +1,208 @@
+// Copyright (c) 2024, 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.shaking.enums;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import java.util.Arrays;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.List;
+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 EnumCollectionsTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDefaultRuntimes().withMaximumApiLevel().build();
+  }
+
+  private static final List<String> EXPECTED_OUTPUT =
+      Arrays.asList(
+          "none: [A, B]",
+          "all: [B, C]",
+          "of: [C]",
+          "of: [D, E]",
+          "of: [E, F, G]",
+          "of: [F, G, H, I]",
+          "of: [G, H, I, J, K]",
+          "of: [H, I, J, K, L, M]",
+          "range: [I, J]",
+          "map: {J=1}",
+          "valueOf: K",
+          "phi: [B]");
+
+  public static class TestMain {
+    public enum EnumA {
+      A,
+      B
+    }
+
+    public enum EnumB {
+      B,
+      C
+    }
+
+    public enum EnumC {
+      C,
+      D
+    }
+
+    public enum EnumD {
+      D,
+      E
+    }
+
+    public enum EnumE {
+      E,
+      F,
+      G
+    }
+
+    public enum EnumF {
+      F,
+      G,
+      H,
+      I
+    }
+
+    public enum EnumG {
+      G,
+      H,
+      I,
+      J,
+      K
+    }
+
+    public enum EnumH {
+      H,
+      I,
+      J,
+      K,
+      L,
+      M
+    }
+
+    public enum EnumI {
+      I,
+      J
+    }
+
+    public enum EnumJ {
+      J,
+      K
+    }
+
+    public enum EnumK {
+      K,
+      L
+    }
+
+    @NeverInline
+    private static void noneOf() {
+      System.out.println("none: " + EnumSet.complementOf(EnumSet.noneOf(EnumA.class)));
+    }
+
+    @NeverInline
+    private static void allOf() {
+      System.out.println("all: " + EnumSet.allOf(EnumB.class));
+    }
+
+    @NeverInline
+    private static void of1() {
+      System.out.println("of: " + EnumSet.of(EnumC.C));
+    }
+
+    @NeverInline
+    private static void of2() {
+      System.out.println("of: " + EnumSet.of(EnumD.D, EnumD.E));
+    }
+
+    @NeverInline
+    private static void of3() {
+      System.out.println("of: " + EnumSet.of(EnumE.E, EnumE.F, EnumE.G));
+    }
+
+    @NeverInline
+    private static void of4() {
+      System.out.println("of: " + EnumSet.of(EnumF.F, EnumF.G, EnumF.H, EnumF.I));
+    }
+
+    @NeverInline
+    private static void of5() {
+      System.out.println("of: " + EnumSet.of(EnumG.G, EnumG.H, EnumG.I, EnumG.J, EnumG.K));
+    }
+
+    @NeverInline
+    private static void ofVarArgs() {
+      System.out.println("of: " + EnumSet.of(EnumH.H, EnumH.I, EnumH.J, EnumH.K, EnumH.L, EnumH.M));
+    }
+
+    @NeverInline
+    private static void range() {
+      System.out.println("range: " + EnumSet.range(EnumI.I, EnumI.J));
+    }
+
+    @NeverInline
+    private static void map() {
+      EnumMap<EnumJ, Integer> map = new EnumMap<>(EnumJ.class);
+      map.put(EnumJ.J, 1);
+      System.out.println("map: " + map);
+    }
+
+    @NeverInline
+    private static void valueOf() {
+      System.out.println("valueOf: " + EnumK.valueOf("K"));
+    }
+
+    public static void main(String[] args) {
+      // Use different methods to ensure Enqueuer.traceInvokeStatic() triggers for each one.
+      noneOf();
+      allOf();
+      of1();
+      of2();
+      of3();
+      of4();
+      of5();
+      ofVarArgs();
+      range();
+      map();
+      valueOf();
+      // Ensure phi as argument does not cause issues.
+      System.out.println(
+          "phi: " + EnumSet.of((Enum) (args.length > 10 ? (Object) EnumA.A : (Object) EnumB.B)));
+    }
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClassesAndInnerClasses(TestMain.class)
+        .run(parameters.getRuntime(), TestMain.class)
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClassesAndInnerClasses(TestMain.class)
+        .enableInliningAnnotations()
+        .addKeepMainRule(TestMain.class)
+        .compile()
+        .run(parameters.getRuntime(), TestMain.class)
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java b/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java
index 4ce22fc..55b0158 100644
--- a/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java
+++ b/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java
@@ -106,8 +106,7 @@
           if (defBlock.canThrow()) {
             // Found the invoke instruction / block.
             assertEquals(2, defBlock.getSuccessors().size());
-            assertTrue(
-                defBlock.getInstructions().get(defBlock.getInstructions().size() - 2).isInvoke());
+            assertTrue(defBlock.getInstructions().getLast().getPrev().isInvoke());
             for (BasicBlock returnPredecessor : block.getPredecessors()) {
               if (defBlock.hasCatchSuccessor(returnPredecessor)) {
                 hasExceptionalPredecessor = true;
diff --git a/src/test/java/com/android/tools/r8/smali/IfSimplificationTest.java b/src/test/java/com/android/tools/r8/smali/IfSimplificationTest.java
index 9642e4e..c7adad6 100644
--- a/src/test/java/com/android/tools/r8/smali/IfSimplificationTest.java
+++ b/src/test/java/com/android/tools/r8/smali/IfSimplificationTest.java
@@ -14,7 +14,7 @@
 import com.android.tools.r8.dex.code.DexIfLez;
 import com.android.tools.r8.dex.code.DexIfLtz;
 import com.android.tools.r8.dex.code.DexIfNez;
-import com.android.tools.r8.dex.code.DexInvokeVirtual;
+import com.android.tools.r8.dex.code.DexInvokeVirtualRange;
 import com.android.tools.r8.dex.code.DexReturn;
 import com.android.tools.r8.dex.code.DexReturnObject;
 import com.android.tools.r8.graph.DexCode;
@@ -399,7 +399,7 @@
             "          goto                :label_7");
     DexCode code = method.getCode().asDexCode();
     assertEquals(3, code.instructions.length);
-    assertTrue(code.instructions[0] instanceof DexInvokeVirtual);
+    assertTrue(code.instructions[0] instanceof DexInvokeVirtualRange);
     assertTrue(code.instructions[1] instanceof DexConst4);
     assertEquals(0, ((DexConst4) code.instructions[1]).B);
     assertTrue(code.instructions[2] instanceof DexReturnObject);
diff --git a/src/test/java/com/android/tools/r8/smali/OutlineTest.java b/src/test/java/com/android/tools/r8/smali/OutlineTest.java
index 9b534e2..b3a09bf 100644
--- a/src/test/java/com/android/tools/r8/smali/OutlineTest.java
+++ b/src/test/java/com/android/tools/r8/smali/OutlineTest.java
@@ -575,7 +575,7 @@
         DexInvokeStatic invoke = (DexInvokeStatic) mainCode.instructions[4];
         assertTrue(isOutlineMethodName(invoke.getMethod()));
       } else if (i == 3) {
-        DexInvokeStatic invoke = (DexInvokeStatic) mainCode.instructions[1];
+        DexInvokeStaticRange invoke = (DexInvokeStaticRange) mainCode.instructions[1];
         assertTrue(isOutlineMethodName(invoke.getMethod()));
       } else {
         assert i == 4 || i == 5;
@@ -1634,7 +1634,8 @@
   }
 
   private static boolean isOutlineInvoke(DexInstruction instruction) {
-    return instruction instanceof DexInvokeStatic && isOutlineMethodName(instruction.getMethod());
+    return (instruction instanceof DexInvokeStatic || instruction instanceof DexInvokeStaticRange)
+        && isOutlineMethodName(instruction.getMethod());
   }
 
   private void assertHasOutlineInvoke(DexEncodedMethod method) {
diff --git a/src/test/testbase/java/com/android/tools/r8/TestBase.java b/src/test/testbase/java/com/android/tools/r8/TestBase.java
index 8cdcbc0..58fd949 100644
--- a/src/test/testbase/java/com/android/tools/r8/TestBase.java
+++ b/src/test/testbase/java/com/android/tools/r8/TestBase.java
@@ -1523,10 +1523,11 @@
   }
 
   protected Stream<DexInstruction> filterInstructionKind(
-      DexCode dexCode, Class<? extends DexInstruction> kind) {
+      DexCode dexCode, Class<? extends DexInstruction>... kinds) {
     return Arrays.stream(dexCode.instructions)
-        .filter(kind::isInstance)
-        .map(kind::cast);
+        .filter(
+            instruction ->
+                Arrays.asList(kinds).stream().anyMatch(kind -> kind.isInstance(instruction)));
   }
 
   protected long countCall(MethodSubject method, String className, String methodName) {
diff --git a/third_party/openjdk/desugar_jdk_libs_11.tar.gz.sha1 b/third_party/openjdk/desugar_jdk_libs_11.tar.gz.sha1
index 9f5883e..7a144f7 100644
--- a/third_party/openjdk/desugar_jdk_libs_11.tar.gz.sha1
+++ b/third_party/openjdk/desugar_jdk_libs_11.tar.gz.sha1
@@ -1 +1 @@
-f3213584e94bf6951c3765c6b8d23405c332ca1b
\ No newline at end of file
+5a35d5323fe418db348576d23428cb3346d28535
\ No newline at end of file
diff --git a/tools/compiledump.py b/tools/compiledump.py
index c819865..e9a462d 100755
--- a/tools/compiledump.py
+++ b/tools/compiledump.py
@@ -502,7 +502,25 @@
     return False
 
 
+def compile_reflective_helper(temp, jdkhome):
+    gradle.RunGradle([utils.GRADLE_TASK_MAIN_COMPILE])
+    base_path = os.path.join(
+        utils.REPO_ROOT,
+        'src/main/java/com/android/tools/r8/utils/compiledump')
+
+    cmd = [
+        jdk.GetJavacExecutable(jdkhome),
+        '-d',
+        temp,
+        '-cp',
+        utils.BUILD_JAVA_MAIN_DIR,
+    ]
+    cmd.extend(os.path.join(base_path, f) for f in os.listdir(base_path))
+    utils.PrintCmd(cmd)
+    subprocess.check_output(cmd)
+
 def prepare_r8_wrapper(dist, temp, jdkhome):
+    compile_reflective_helper(temp, jdkhome)
     compile_wrapper_with_javac(
         dist, temp, jdkhome,
         os.path.join(
@@ -530,7 +548,7 @@
         '-d',
         temp,
         '-cp',
-        dist,
+        "%s:%s" % (dist, temp),
     ]
     utils.PrintCmd(cmd)
     subprocess.check_output(cmd)
diff --git a/tools/test.py b/tools/test.py
index c071e71..26eda1e 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -409,6 +409,10 @@
 
     if options.low_priority:
         gradle_args.append('--priority=low')
+        # Default is 3, but some VMs become unresponsive with this value.
+        # Increase to reduce concurrency.
+        if not os.environ.get('R8_GRADLE_CORES_PER_FORK'):
+          os.environ['R8_GRADLE_CORES_PER_FORK'] = '5'
 
     # Set all necessary Gradle properties and options first.
     if options.shard_count:
diff --git a/tools/toolhelper.py b/tools/toolhelper.py
index 5a6b8ee..cfc386c 100644
--- a/tools/toolhelper.py
+++ b/tools/toolhelper.py
@@ -69,7 +69,7 @@
             'com.android.tools.r8.tracereferences.TraceReferences'
         ])
     else:
-        cmd.extend(['-jar', utils.R8_JAR, tool])
+        cmd.extend(['-cp', utils.R8_JAR, 'com.android.tools.r8.SwissArmyKnife', tool])
     lib, args = extract_lib_from_args(args)
     if lib:
         cmd.extend(["--lib", lib])
diff --git a/tools/utils.py b/tools/utils.py
index 86d7e8d..66f8e36 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -32,7 +32,8 @@
 DEPENDENCIES_DIR = os.path.join(THIRD_PARTY, 'dependencies')
 
 BUILD = os.path.join(REPO_ROOT, 'build')
-BUILD_JAVA_MAIN_DIR = os.path.join(BUILD, 'classes', 'java', 'main')
+BUILD_JAVA_MAIN_DIR = os.path.join(REPO_ROOT, 'd8_r8', 'main', 'build',
+                                   'classes', 'java', 'main')
 LIBS = os.path.join(BUILD, 'libs')
 CUSTOM_CONVERSION_DIR = os.path.join(THIRD_PARTY, 'openjdk',
                                      'custom_conversion')
@@ -44,6 +45,7 @@
 GRADLE_TASK_CONSOLIDATED_LICENSE = ':main:consolidatedLicense'
 GRADLE_TASK_KEEP_ANNO_JAR = ':keepanno:keepAnnoAnnotationsJar'
 GRADLE_TASK_KEEP_ANNO_DOC = ':keepanno:keepAnnoAnnotationsDoc'
+GRADLE_TASK_MAIN_COMPILE = ':main:compileJava'
 GRADLE_TASK_R8 = ':main:r8WithRelocatedDeps'
 GRADLE_TASK_R8LIB = ':test:assembleR8LibWithRelocatedDeps'
 GRADLE_TASK_R8LIB_NO_DEPS = ':test:assembleR8LibNoDeps'