Split live ranges for invoke/range

This changes the register allocation strategy for dealing with invoke/range. Unlike previously, this no longer creates alias SSA values using move instructions for all arguments to an invoke/range instruction. Instead, this splits the live ranges of values that are used in invoke/range instructions upfront. Similar to the previous strategy, when processing the next unhandled live intervals, we first assign registers to all subsequent split live intervals for the same value (and their linked values) that span an invoke/range instruction.

One key advantage is that this avoids the need for alias SSA values, which has the negative side effects that the same SSA value occupies two registers and that the register allocator did not leverage that a given value was known to be in multiple registers. As a concrete example, the register allocator would occasionally move a value to a low register prior to an invoke/range instruction, and then after the invoke/range add a redundant move for the same value to a low register, since the existing value in a low register would belong to a different SSA value.

This change identifies and solves the following issues:

* When the same value is used as more than one argument to the same invoke/range, alias SSA values are introduced since each live intervals can (currently) only have one register (two consecutive if wide).

* Unlike previously, the live intervals that are used to assign registers to invoke/range instructions can be live out of the invoke/range instruction. As a result, this may allocate all 4 bit or 8 bit registers in a way that there are no available registers after the invoke/range (effectively running out of registers). To mitigate this, we always block the lower most register(s) when the invoke/range has an out-value.

* When allocating registers for methods with <=16 registers this no longer pins arguments in their input register (except for the receiver), leading to lower register pressure.

* When allocating registers for methods with <=16 registers that have an invoke/range instruction and use catch handlers, register allocation now needs to block a dedicated move-exception register throughout the method, similarly to in 8 bit and 16 bit register allocation.

* A bug in the removeUnneededMovesOnExitingPaths optimization (debug mode only).

This saves 6-7% of all moves in Compose sample benchmarks and +10% of all moves in Composable functions.

Bug: b/302281605
Change-Id: I172640414ac94f79d170da7005daf80827f4562b
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/conversion/DexBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
index bdeeb96..e9785ee 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
@@ -67,6 +67,7 @@
 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;
@@ -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));
@@ -1078,7 +1083,7 @@
     return options;
   }
 
-  public RegisterAllocator getRegisterAllocator() {
+  public LinearScanRegisterAllocator getRegisterAllocator() {
     return registerAllocator;
   }
 
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/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index aa4bafe..0e8ca85 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
@@ -230,12 +230,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();
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..fe545c4 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;
@@ -42,6 +43,7 @@
 import com.android.tools.r8.utils.ArrayUtils;
 import com.android.tools.r8.utils.BooleanUtils;
 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;
@@ -195,8 +197,6 @@
   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;
@@ -252,6 +252,10 @@
             && isDedicatedMoveExceptionRegisterInLastLocalRegister());
   }
 
+  private boolean isDedicatedMoveExceptionRegister(int register) {
+    return hasDedicatedMoveExceptionRegister() && register == getMoveExceptionRegister();
+  }
+
   private boolean isDedicatedMoveExceptionRegisterInFirstLocalRegister() {
     assert hasDedicatedMoveExceptionRegister();
     if (mode.is4Bit() || mode.is16Bit()) {
@@ -763,13 +767,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 +817,7 @@
     this.mode = mode;
 
     if (retry) {
-      clearRegisterAssignments(mode);
+      clearRegisterAssignments();
       removeSpillAndPhiMoves();
     }
 
@@ -886,6 +896,8 @@
   // 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.
+  // TODO(b/376654519): This unsplits the entire argument live intervals or does nothing. Couldn't
+  //  we save some moves by partially unsplitting the argument live intervals?
   private boolean unsplitArguments() {
     if (mode.is4Bit()) {
       return false;
@@ -901,6 +913,10 @@
       boolean canUseArgumentRegister = true;
       boolean couldUseArgumentRegister = true;
       for (LiveIntervals child : intervals.getSplitChildren()) {
+        if (child.isInvokeRangeIntervals()) {
+          canUseArgumentRegister = false;
+          break;
+        }
         int registerConstraint = child.getRegisterLimit();
         if (registerConstraint < Constants.U16BIT_MAX) {
           couldUseArgumentRegister = false;
@@ -951,7 +967,7 @@
     return false;
   }
 
-  private void clearRegisterAssignments(ArgumentReuseMode mode) {
+  private void clearRegisterAssignments() {
     freeRegisters.clear();
     maxRegisterNumber = -1;
     active.clear();
@@ -960,11 +976,12 @@
     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 +1034,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 +1045,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 +1058,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 +1073,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 +1112,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,7 +1121,7 @@
     // 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()) {
+    if (!mode.is4Bit() || hasInvokeRangeLiveIntervals) {
       // Force all move exception ranges to start out with the exception in a fixed register.
       for (BasicBlock block : code.blocks) {
         Instruction instruction = block.entry();
@@ -1133,26 +1145,45 @@
           unhandled.add(split);
         }
       }
-      for (LiveIntervals intervals : moveExceptionIntervals) {
-        assert intervals.getRegisterLimit() == Constants.U8BIT_MAX;
-      }
     }
   }
 
-  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) {
@@ -1212,22 +1243,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,8 +1292,11 @@
     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;
   }
@@ -1320,90 +1350,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 +1373,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.
+      TreeSet<Integer> savedFreeRegisters = new TreeSet<>(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()
+                  && !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 +1524,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;
       }
     }
@@ -1897,7 +1899,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 +2016,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.
       //
@@ -2107,17 +2121,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 +2178,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 +2211,7 @@
       return false;
     }
     if (freePositions.isBlocked(register, needsRegisterPair)) {
-      return tryAllocateBlockedHint(unhandledInterval);
+      return tryAllocateBlockedHint(unhandledInterval, register);
     }
     int freePosition = freePositions.get(register);
     if (needsRegisterPair) {
@@ -2188,27 +2234,68 @@
     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 +2304,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 +2328,9 @@
       }
       intervalsWithRegister = intervals;
     }
+    if (intervalsWithRegister != null) {
+      return Collections.singleton(intervalsWithRegister);
+    }
     return Collections.emptyList();
   }
 
@@ -2480,10 +2578,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,26 +2885,35 @@
     }
   }
 
-  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);
+                }
               }
-            }
-          }
-        }
+            });
       }
     }
   }
@@ -2826,7 +2931,7 @@
             split != null;
             split = sortedChildren.poll()) {
           int position = split.getStart();
-          if (!isPinnedArgumentRegister(split)) {
+          if (!canSkipArgumentMove(split)) {
             spillMoves.addSpillOrRestoreMove(toGapPosition(position), split, current);
           }
           current = split;
@@ -2881,7 +2986,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 +3015,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;
   }
@@ -2981,18 +3110,12 @@
     // 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);
       }
     }
   }
@@ -3086,7 +3209,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(instruction);
               if (!isUnconstrainedArgumentUse) {
                 useIntervals.addUse(new LiveIntervalsUse(instruction.getNumber(), inConstraint));
               }
@@ -3250,121 +3375,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 +3433,6 @@
         last.getLiveIntervals().link(next.getLiveIntervals());
         last = next;
       }
-      lastArgumentValue = last;
     }
   }
 
@@ -3411,18 +3444,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,7 +3510,7 @@
 
   private int getFreeConsecutiveRegisters(int numberOfRegisters, boolean prioritizeSmallRegisters) {
     int oldMaxRegisterNumber = maxRegisterNumber;
-    TreeSet<Integer> freeRegistersWithDesiredOrdering = this.freeRegisters;
+    TreeSet<Integer> freeRegistersWithDesiredOrdering = freeRegisters;
     if (prioritizeSmallRegisters) {
       freeRegistersWithDesiredOrdering =
           new TreeSet<>(
@@ -3498,7 +3528,7 @@
                 // Otherwise use their normal ordering.
                 return x - y;
               });
-      freeRegistersWithDesiredOrdering.addAll(this.freeRegisters);
+      freeRegistersWithDesiredOrdering.addAll(freeRegisters);
     }
 
     Iterator<Integer> freeRegistersIterator = freeRegistersWithDesiredOrdering.iterator();
@@ -3553,20 +3583,14 @@
     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 +3604,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 +3623,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 +3632,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..4ecc6bd 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,10 +38,10 @@
   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 boolean spilled = false;
+  private boolean isInvokeRangeIntervals = false;
   private boolean usedInMonitorOperations = false;
 
   // Only registers up to and including the registerLimit are allowed for this interval.
@@ -99,6 +99,10 @@
     }
   }
 
+  public boolean hasHint() {
+    return hint != null;
+  }
+
   public Integer getHint() {
     return hint;
   }
@@ -132,7 +136,6 @@
   }
 
   public void link(LiveIntervals next) {
-    assert numberOfConsecutiveRegisters == -1;
     nextConsecutive = next;
     next.previousConsecutive = this;
   }
@@ -146,23 +149,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 +179,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 +293,19 @@
     register = n;
   }
 
+  public boolean isInvokeRangeIntervals() {
+    return isInvokeRangeIntervals;
+  }
+
+  public void setIsInvokeRangeIntervals() {
+    assert !isInvokeRangeIntervals;
+    isInvokeRangeIntervals = true;
+  }
+
+  public void unsetIsInvokeRangeIntervals() {
+    isInvokeRangeIntervals = false;
+  }
+
   private int computeMaxNonSpilledRegister() {
     assert splitParent == this;
     assert maxNonSpilledRegister == NO_REGISTER;
@@ -456,7 +458,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()) {
@@ -610,10 +612,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/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/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/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/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/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/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/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/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/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/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/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) {