Use an intrusive linked list for BasicBlock instructions

 * Encapsulates all Instruction.setBlock() and IRMetadata.record()
   calls within InstructionList so they cannot be missed.
 * Updates passes that added the same instruction (temporarily) to two
   different blocks (or to the same block twice).
   * Instructions can now belong to only one list, so must be removed
     before added again.
 * Switches a handful of iterator uses to for loops (get a feel for
   the style)
   * I think many spots would benefit from using this style over
     iterators, as it means you do not need worry about invalidating
     iterators, or rewinding them.
 * Changes semantics of Block.listIterator(Instruction) to match
   Block.listIterator(index)
   * E.g. The given instruction will be the first to be returned by
     a call to next().
 * Moves some instruction mutating helper logic from
   BasicBlockInstructionListIterator to InstructionList & Instruction,
   so that they can be used without an iterator.
   * Likely makes sense to do more of this going forward, or to capture
     these helpers in an InstructionUtil class.
 * Leaves in some TODOs for trivial follow-up refactors in order to
   not make this even larger of a change than it is.

Bug: b/376663044
Change-Id: I11005b74efbbda9fe2b463a5f848c0f486868d59
diff --git a/src/main/java/com/android/tools/r8/cf/CfRegisterAllocator.java b/src/main/java/com/android/tools/r8/cf/CfRegisterAllocator.java
index 418fcea..a23ee09 100644
--- a/src/main/java/com/android/tools/r8/cf/CfRegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/cf/CfRegisterAllocator.java
@@ -521,10 +521,11 @@
 
   private void applyInstructionsBackwardsToRegisterLiveness(
       BasicBlock block, IntSet liveRegisters, int suffixSize) {
-    InstructionIterator iterator = block.iterator(block.getInstructions().size());
-    int instructionsLeft = suffixSize;
-    while (--instructionsLeft >= 0 && iterator.hasPrevious()) {
-      Instruction current = iterator.previous();
+    int i = 0;
+    for (var current = block.getLastInstruction(); current != null; current = current.getPrev()) {
+      if (++i > suffixSize) {
+        break;
+      }
       Value outValue = current.outValue();
       if (outValue != null && outValue.needsRegister()) {
         int register = getRegisterForValue(outValue);
diff --git a/src/main/java/com/android/tools/r8/cf/LoadStoreHelper.java b/src/main/java/com/android/tools/r8/cf/LoadStoreHelper.java
index 9139e16..a92f931 100644
--- a/src/main/java/com/android/tools/r8/cf/LoadStoreHelper.java
+++ b/src/main/java/com/android/tools/r8/cf/LoadStoreHelper.java
@@ -213,7 +213,7 @@
       storeBlock = it.split(this.code, this.blockIterator);
       it = storeBlock.listIterator(code);
     }
-    add(store, storeBlock, instruction.getPosition(), it);
+    add(store, instruction.getPosition(), it);
     if (hasCatchHandlers && !instruction.instructionTypeCanThrow()) {
       splitAfterStoredOutValue(it);
     }
@@ -242,7 +242,7 @@
       it = insertBlock.listIterator(code);
     }
     instruction.swapOutValue(newOutValue);
-    add(new Pop(newOutValue), insertBlock, instruction.getPosition(), it);
+    add(new Pop(newOutValue), instruction.getPosition(), it);
   }
 
   private static class PhiMove {
@@ -261,7 +261,7 @@
     List<StackValue> temps = new ArrayList<>(moves.size());
     for (PhiMove move : moves) {
       StackValue tmp = createStackValue(move.phi, topOfStack++);
-      add(load(tmp, move.operand), move.phi.getBlock(), position, it);
+      add(load(tmp, move.operand), position, it);
       temps.add(tmp);
       move.operand.removePhiUser(move.phi);
     }
@@ -269,7 +269,7 @@
       PhiMove move = moves.get(i);
       StackValue tmp = temps.get(i);
       FixedLocalValue out = new FixedLocalValue(move.phi);
-      add(new Store(out, tmp), move.phi.getBlock(), position, it);
+      add(new Store(out, tmp), position, it);
       move.phi.replaceUsers(out);
     }
   }
@@ -296,12 +296,11 @@
 
   private static void add(
       Instruction newInstruction, Instruction existingInstruction, InstructionListIterator it) {
-    add(newInstruction, existingInstruction.getBlock(), existingInstruction.getPosition(), it);
+    add(newInstruction, existingInstruction.getPosition(), it);
   }
 
   private static void add(
-      Instruction newInstruction, BasicBlock block, Position position, InstructionListIterator it) {
-    newInstruction.setBlock(block);
+      Instruction newInstruction, Position position, InstructionListIterator it) {
     newInstruction.setPosition(position);
     it.add(newInstruction);
   }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java
index adf8f89..ccffd96 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java
@@ -149,7 +149,7 @@
       NumberGenerator blockNumberGenerator = new NumberGenerator();
       NumberGenerator valueNumberGenerator = new NumberGenerator();
 
-      BasicBlock block = new BasicBlock();
+      BasicBlock block = new BasicBlock(metadata);
       block.setNumber(blockNumberGenerator.next());
 
       // Add "invoke-static <clinit>" for each of the class initializers to the exit block.
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysis.java
index fc21b02..99f161e 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/escape/EscapeAnalysis.java
@@ -131,9 +131,8 @@
       if (user.getBlock() == block) {
         // When the value of interest has the definition
         if (!root.isPhi()) {
-          List<Instruction> instructions = block.getInstructions();
           // Make sure we're not considering instructions prior to the value of interest.
-          if (instructions.indexOf(user) < instructions.indexOf(root.definition)) {
+          if (user.comesBefore(root.definition)) {
             continue;
           }
         }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/InstanceFieldValueAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/InstanceFieldValueAnalysis.java
index c60e298..ac5a053 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/InstanceFieldValueAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/InstanceFieldValueAnalysis.java
@@ -287,7 +287,7 @@
     // TODO(b/279877113): Extend this analysis to analyze the full remainder of this method.
     if (instancePut.getBlock().getSuccessors().isEmpty()) {
       InstructionListIterator instructionIterator =
-          instancePut.getBlock().listIterator(code, instancePut);
+          instancePut.getBlock().listIterator(code, instancePut.getNext());
       while (instructionIterator.hasNext()) {
         Instruction instruction = instructionIterator.next();
         if (instruction.readSet(appView, context).contains(field)) {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraintAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraintAnalysis.java
index fa31549..70eafed 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraintAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/inlining/SimpleInliningConstraintAnalysis.java
@@ -20,7 +20,6 @@
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.IfType;
 import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.JumpInstruction;
 import com.android.tools.r8.ir.code.StringSwitch;
@@ -78,21 +77,18 @@
 
     // Run a bounded depth-first traversal to collect the path constraints that lead to early
     // returns.
-    InstructionIterator instructionIterator =
-        code.entryBlock().iterator(code.getNumberOfArguments());
-    return analyzeInstructionsInBlock(code.entryBlock(), 0, 0, instructionIterator);
+    BasicBlock block = code.entryBlock();
+    Instruction firstNonArgument = block.getInstructions().getNth(code.getNumberOfArguments());
+    return analyzeInstructionsInBlock(block, 0, 0, firstNonArgument);
   }
 
   private SimpleInliningConstraintWithDepth analyzeInstructionsInBlock(
       BasicBlock block, int branchDepth, int instructionDepth) {
-    return analyzeInstructionsInBlock(block, branchDepth, instructionDepth, block.iterator());
+    return analyzeInstructionsInBlock(block, branchDepth, instructionDepth, block.entry());
   }
 
   private SimpleInliningConstraintWithDepth analyzeInstructionsInBlock(
-      BasicBlock block,
-      int branchDepth,
-      int instructionDepth,
-      InstructionIterator instructionIterator) {
+      BasicBlock block, int branchDepth, int instructionDepth, Instruction instruction) {
     if (!seen.add(block)
         || block.hasCatchHandlers()
         || block.exit().isThrow()
@@ -102,7 +98,6 @@
 
     // Move the instruction iterator forward to the block's jump instruction, while incrementing the
     // instruction depth of the depth-first traversal.
-    Instruction instruction = instructionIterator.next();
     SimpleInliningConstraint blockConstraint = AlwaysSimpleInliningConstraint.getInstance();
     while (!instruction.isJumpInstruction()) {
       assert !instruction.isArgument();
@@ -116,7 +111,7 @@
       } else {
         blockConstraint = blockConstraint.meet(instructionConstraint);
       }
-      instruction = instructionIterator.next();
+      instruction = instruction.getNext();
     }
 
     // If we have exceeded the threshold, then all paths from this instruction will not lead to any
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
index 3dce430..84bf5e1 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedExtensionRegistryShrinker.java
@@ -154,7 +154,7 @@
         // Already removed.
         continue;
       }
-      IRCodeUtils.removeInstructionAndTransitiveInputsIfNotUsed(code, instruction);
+      IRCodeUtils.removeInstructionAndTransitiveInputsIfNotUsed(instruction);
     }
     code.removeRedundantBlocks();
     assert code.isConsistentSSA(appView);
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
index 1b3837a..4919305 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteShrinker.java
@@ -341,7 +341,8 @@
       ProtoMessageInfo protoMessageInfo) {
     // Position iterator immediately before the call to newMessageInfo().
     BasicBlock block = newMessageInfoInvoke.getBlock();
-    InstructionListIterator instructionIterator = block.listIterator(code, newMessageInfoInvoke);
+    InstructionListIterator instructionIterator =
+        block.listIterator(code, newMessageInfoInvoke.getNext());
     Instruction previous = instructionIterator.previous();
     instructionIterator.setInsertionPosition(newMessageInfoInvoke.getPosition());
     assert previous == newMessageInfoInvoke;
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index 59fde05..5e5c563 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -47,7 +47,6 @@
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.Iterator;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
@@ -173,9 +172,8 @@
   // Catch handler information about which successors are catch handlers and what their guards are.
   private CatchHandlers<Integer> catchHandlers = CatchHandlers.EMPTY_INDICES;
 
-  // TODO(b/270398965): Replace LinkedList.
-  @SuppressWarnings("JdkObsolete")
-  private LinkedList<Instruction> instructions = new LinkedList<>();
+  // Linked list of instructions belonging to this block.
+  private final InstructionList instructions = new InstructionList(this);
 
   private int number = -1;
   private List<Phi> phis = new ArrayList<>();
@@ -202,6 +200,23 @@
   // Map of registers to current SSA value. Used during SSA numbering and cleared once filled.
   private Map<Integer, Value> currentDefinitions = new HashMap<>();
 
+  // Metadata shared by all blocks of an IRCode.
+  // TODO(b/376663044): Remove |metadata| parameter from methods in this class.
+  private IRMetadata metadata;
+
+  public BasicBlock(IRMetadata metadata) {
+    this.metadata = metadata;
+  }
+
+  public IRMetadata getMetadata() {
+    return metadata;
+  }
+
+  // Required when moving a block between code objects (inlining).
+  void setMetadata(IRMetadata metadata) {
+    this.metadata = metadata;
+  }
+
   public <BT, CT> TraversalContinuation<BT, CT> traverseNormalPredecessors(
       BiFunction<? super BasicBlock, ? super CT, TraversalContinuation<BT, CT>> fn,
       CT initialValue) {
@@ -524,7 +539,8 @@
       } else if (exit().isIf()) {
         if (indexOfNewBlock >= successors.size() - 2 && indexOfOldBlock >= successors.size() - 2) {
           // New and old are true target and fallthrough, replace last instruction with a goto.
-          Instruction instruction = getInstructions().removeLast();
+          Instruction instruction = instructions.getLast();
+          instructions.removeIgnoreValues(instruction);
           // Iterate in reverse order to ensure that POP instructions are inserted in correct order.
           for (int i = instruction.inValues().size() - 1; i >= 0; i--) {
             Value value = instruction.inValues().get(i);
@@ -536,7 +552,6 @@
               } else {
                 assert !(value instanceof StackValues);
                 Pop pop = new Pop(value);
-                pop.setBlock(this);
                 pop.setPosition(instruction.getPosition());
                 getInstructions().addLast(pop);
               }
@@ -546,7 +561,6 @@
             }
           }
           Instruction exit = new Goto();
-          exit.setBlock(this);
           exit.setPosition(instruction.getPosition());
           getInstructions().addLast(exit);
         } else if (indexOfOldBlock >= successors.size() - 2) {
@@ -738,7 +752,7 @@
     return nextInstructionNumber;
   }
 
-  public LinkedList<Instruction> getInstructions() {
+  public InstructionList getInstructions() {
     return instructions;
   }
 
@@ -797,22 +811,24 @@
   }
 
   public Instruction entry() {
-    return instructions.get(0);
+    return instructions.getFirst();
   }
 
   public JumpInstruction exit() {
     assert filled;
-    assert instructions.get(instructions.size() - 1).isJumpInstruction();
-    return instructions.get(instructions.size() - 1).asJumpInstruction();
+    assert instructions.getLast().isJumpInstruction();
+    return instructions.getLast().asJumpInstruction();
+  }
+
+  public Instruction getLastInstruction() {
+    return instructions.getLast();
   }
 
   public Instruction exceptionalExit() {
     assert hasCatchHandlers();
-    InstructionIterator it = iterator(instructions.size());
-    while (it.hasPrevious()) {
-      Instruction instruction = it.previous();
-      if (instruction.instructionTypeCanThrow()) {
-        return instruction;
+    for (Instruction ins = getLastInstruction(); ins != null; ins = ins.getPrev()) {
+      if (ins.instructionTypeCanThrow()) {
+        return ins;
       }
     }
     return null;
@@ -892,15 +908,9 @@
     phis.removeAll(phisToRemove);
   }
 
-  public void add(Instruction next, IRCode code) {
-    add(next, code.metadata());
-  }
-
-  public void add(Instruction next, IRMetadata metadata) {
+  public void add(Instruction next, IRMetadata unused_metadata) {
     assert !isFilled();
-    instructions.add(next);
-    metadata.record(next);
-    next.setBlock(this);
+    instructions.addLast(next);
   }
 
   public void close(IRBuilder builder) {
@@ -1493,49 +1503,11 @@
     return builder.toString();
   }
 
-  public void addPhiMove(Move move) {
-    // TODO(ager): Consider this more, is it always the case that we should add it before the
-    // exit instruction?
-    Instruction branch = exit();
-    instructions.set(instructions.size() - 1, move);
-    instructions.add(branch);
-  }
-
-  public void setInstructions(LinkedList<Instruction> instructions) {
-    this.instructions = instructions;
-  }
-
-  /**
-   * Remove a number of instructions. The instructions to remove are given as indexes in the
-   * instruction stream.
-   */
-  // TODO(b/270398965): Replace LinkedList.
-  @SuppressWarnings("JdkObsolete")
-  public void removeInstructions(List<Integer> toRemove) {
-    if (!toRemove.isEmpty()) {
-      LinkedList<Instruction> newInstructions = new LinkedList<>();
-      int nextIndex = 0;
-      for (Integer index : toRemove) {
-        assert index >= nextIndex;  // Indexes in toRemove must be sorted ascending.
-        newInstructions.addAll(instructions.subList(nextIndex, index));
-        instructions.get(index).clearBlock();
-        nextIndex = index + 1;
-      }
-      if (nextIndex < instructions.size()) {
-        newInstructions.addAll(instructions.subList(nextIndex, instructions.size()));
-      }
-      assert instructions.size() == newInstructions.size() + toRemove.size();
-      setInstructions(newInstructions);
-    }
-  }
-
   /**
    * Remove an instruction.
    */
   public void removeInstruction(Instruction toRemove) {
-    int index = instructions.indexOf(toRemove);
-    assert index >= 0;
-    removeInstructions(Collections.singletonList(index));
+    instructions.removeIgnoreValues(toRemove);
   }
 
   /**
@@ -1563,7 +1535,7 @@
    */
   public static BasicBlock createGotoBlock(
       int blockNumber, Position position, IRMetadata metadata) {
-    BasicBlock block = new BasicBlock();
+    BasicBlock block = new BasicBlock(metadata);
     block.add(new Goto(), metadata);
     block.close(null);
     block.setNumber(blockNumber);
@@ -1580,7 +1552,7 @@
    * @param theIf the if instruction
    */
   public static BasicBlock createIfBlock(int blockNumber, If theIf, IRMetadata metadata) {
-    BasicBlock block = new BasicBlock();
+    BasicBlock block = new BasicBlock(metadata);
     block.add(theIf, metadata);
     block.close(null);
     block.setNumber(blockNumber);
@@ -1598,7 +1570,7 @@
    */
   public static BasicBlock createIfBlock(
       int blockNumber, If theIf, IRMetadata metadata, Instruction... instructions) {
-    BasicBlock block = new BasicBlock();
+    BasicBlock block = new BasicBlock(metadata);
     for (Instruction instruction : instructions) {
       block.add(instruction, metadata);
     }
@@ -1610,7 +1582,7 @@
 
   public static BasicBlock createSwitchBlock(
       int blockNumber, IntSwitch theSwitch, IRMetadata metadata) {
-    BasicBlock block = new BasicBlock();
+    BasicBlock block = new BasicBlock(metadata);
     block.add(theSwitch, metadata);
     block.close(null);
     block.setNumber(blockNumber);
@@ -1621,14 +1593,14 @@
       IRCode code, Position position, DexType guard, AppView<?> appView) {
     TypeElement guardTypeLattice =
         TypeElement.fromDexType(guard, Nullability.definitelyNotNull(), appView);
-    BasicBlock block = new BasicBlock();
+    BasicBlock block = new BasicBlock(code.metadata());
     MoveException moveException =
         new MoveException(code.createValue(guardTypeLattice), guard, appView.options());
     moveException.setPosition(position);
     Throw throwInstruction = new Throw(moveException.outValue);
     throwInstruction.setPosition(position);
-    block.add(moveException, code);
-    block.add(throwInstruction, code);
+    block.instructions.addLast(moveException);
+    block.instructions.addLast(throwInstruction);
     block.close(null);
     block.setNumber(code.getNextBlockNumber());
     return block;
@@ -1779,9 +1751,9 @@
   // visible to exceptional successors.
   private boolean verifyNoValuesAfterThrowingInstruction() {
     if (hasCatchHandlers()) {
-      InstructionIterator iterator = iterator(instructions.size());
-      while (iterator.hasPrevious()) {
-        Instruction instruction = iterator.previous();
+      for (Instruction instruction = getLastInstruction();
+          instruction != null;
+          instruction = instruction.getPrev()) {
         if (instruction.instructionTypeCanThrow()) {
           return true;
         }
@@ -1791,58 +1763,53 @@
     return true;
   }
 
-  public BasicBlockInstructionIterator iterator() {
-    return new BasicBlockInstructionIterator(this);
+  public InstructionIterator iterator() {
+    return instructions.iterator();
   }
 
-  public BasicBlockInstructionIterator iterator(int index) {
-    return new BasicBlockInstructionIterator(this, index);
+  public InstructionIterator iterator(Instruction instruction) {
+    return instructions.iterator(instruction);
   }
 
-  public BasicBlockInstructionIterator iterator(Instruction instruction) {
-    return new BasicBlockInstructionIterator(this, instruction);
+  public BasicBlockInstructionListIterator listIterator(IRCode unused_code) {
+    return listIterator();
   }
 
-  public BasicBlockInstructionListIterator listIterator(IRCode code) {
-    return listIterator(code.metadata());
+  public BasicBlockInstructionListIterator listIterator() {
+    return new BasicBlockInstructionListIterator(this);
   }
 
-  public BasicBlockInstructionListIterator listIterator(IRMetadata metadata) {
-    return new BasicBlockInstructionListIterator(metadata, this);
+  public BasicBlockInstructionListIterator listIterator(IRCode unused_code, int index) {
+    // TODO(b/376663044): Convert uses of index to use Instruction instead.
+    return new BasicBlockInstructionListIterator(this, index);
   }
 
-  public BasicBlockInstructionListIterator listIterator(IRCode code, int index) {
-    return new BasicBlockInstructionListIterator(code.metadata(), this, index);
-  }
-
-  /**
-   * Creates an instruction list iterator starting at <code>instruction</code>.
-   *
-   * <p>The cursor will be positioned after <code>instruction</code>. Calling <code>next</code> on
-   * the returned iterator will return the instruction after <code>instruction</code>. Calling
-   * <code>previous</code> will return <code>instruction</code>.
-   */
-  public BasicBlockInstructionListIterator listIterator(IRCode code, Instruction instruction) {
-    return new BasicBlockInstructionListIterator(code.metadata(), this, instruction);
+  /** Creates an instruction list iterator starting at <code>firstInstructionToReturn</code>. */
+  public BasicBlockInstructionListIterator listIterator(
+      IRCode unused_code, Instruction firstInstructionToReturn) {
+    return new BasicBlockInstructionListIterator(this, firstInstructionToReturn);
   }
 
   /**
    * Creates a new empty block as a successor for this block.
    *
-   * The new block will have all the normal successors of the original block.
+   * <p>The new block will have all the normal successors of the original block.
    *
-   * The catch successors are either on the original block or the new block depending on the
+   * <p>The catch successors are either on the original block or the new block depending on the
    * value of <code>keepCatchHandlers</code>.
    *
-   * The current block still has all the instructions, and the new block is empty instruction-wise.
+   * <p>The current block still has all the instructions, and the new block is empty
+   * instruction-wise.
    *
    * @param blockNumber block number for new block
    * @param keepCatchHandlers keep catch successors on the original block
+   * @param firstInstructionOfNewBlock this and all following are moved to the new block
    * @return the new block
    */
-  BasicBlock createSplitBlock(int blockNumber, boolean keepCatchHandlers) {
+  BasicBlock createSplitBlock(
+      int blockNumber, boolean keepCatchHandlers, Instruction firstInstructionOfNewBlock) {
     boolean hadCatchHandlers = hasCatchHandlers();
-    BasicBlock newBlock = new BasicBlock();
+    BasicBlock newBlock = new BasicBlock(metadata);
     newBlock.setNumber(blockNumber);
 
     // Copy all successors including catch handlers to the new block, and update predecessors.
@@ -1862,6 +1829,10 @@
     // Link the two blocks
     link(newBlock);
 
+    if (firstInstructionOfNewBlock != null) {
+      newBlock.instructions.severFrom(firstInstructionOfNewBlock);
+    }
+
     // Mark the new block filled and sealed.
     newBlock.filled = true;
     newBlock.sealed = true;
@@ -1936,7 +1907,7 @@
       exceptionTypeLattice = move.getOutType();
       exceptionType = move.getExceptionType();
       assert move.getDebugValues().isEmpty();
-      getInstructions().remove(0);
+      instructions.removeIgnoreValues(move);
     }
     // Create new predecessor blocks.
     List<BasicBlock> newPredecessors = new ArrayList<>(predecessors.size());
@@ -1946,19 +1917,19 @@
         throw new CompilationError(
             "Invalid block structure: catch block reachable via non-exceptional flow.");
       }
-      BasicBlock newBlock = new BasicBlock();
+      BasicBlock newBlock = new BasicBlock(metadata);
       newBlock.setNumber(code.getNextBlockNumber());
       newPredecessors.add(newBlock);
       if (hasMoveException) {
         Value value = code.createValue(exceptionTypeLattice, move.getLocalInfo());
         values.add(value);
         MoveException newMove = new MoveException(value, exceptionType, options);
-        newBlock.add(newMove, code);
+        newBlock.instructions.addLast(newMove);
         newMove.setPosition(position);
       }
       Goto next = new Goto();
       next.setPosition(position);
-      newBlock.add(next, code);
+      newBlock.instructions.addLast(next);
       newBlock.close(null);
       newBlock.getMutableSuccessors().add(this);
       newBlock.getMutablePredecessors().add(predecessor);
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionIterator.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionIterator.java
deleted file mode 100644
index f0771c3..0000000
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionIterator.java
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.ir.code;
-
-import java.util.ListIterator;
-
-public class BasicBlockInstructionIterator implements InstructionIterator {
-
-  private final ListIterator<Instruction> instructionIterator;
-
-  BasicBlockInstructionIterator(BasicBlock block) {
-    this.instructionIterator = block.getInstructions().listIterator();
-  }
-
-  BasicBlockInstructionIterator(BasicBlock block, int index) {
-    this.instructionIterator = block.getInstructions().listIterator(index);
-  }
-
-  BasicBlockInstructionIterator(BasicBlock block, Instruction instruction) {
-    this(block);
-    nextUntil(x -> x == instruction);
-  }
-
-  @Override
-  public boolean hasPrevious() {
-    return instructionIterator.hasPrevious();
-  }
-
-  @Override
-  public Instruction previous() {
-    return instructionIterator.previous();
-  }
-
-  @Override
-  public boolean hasNext() {
-    return instructionIterator.hasNext();
-  }
-
-  @Override
-  public Instruction next() {
-    return instructionIterator.next();
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
index d1fc0ed..af87309 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
@@ -33,35 +33,33 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
+import java.util.NoSuchElementException;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.UnaryOperator;
 
 public class BasicBlockInstructionListIterator implements InstructionListIterator {
 
-  protected final BasicBlock block;
-  protected final ListIterator<Instruction> listIterator;
+  private Instruction next;
   protected Instruction current;
-  private Position position = null;
+  protected final BasicBlock block;
+  private final InstructionList instructionList;
+  private Position position;
 
-  private final IRMetadata metadata;
-
-  BasicBlockInstructionListIterator(IRMetadata metadata, BasicBlock block) {
-    this.block = block;
-    this.listIterator = block.getInstructions().listIterator();
-    this.metadata = metadata;
+  BasicBlockInstructionListIterator(BasicBlock block) {
+    this(block, 0);
   }
 
-  BasicBlockInstructionListIterator(IRMetadata metadata, BasicBlock block, int index) {
-    this.block = block;
-    this.listIterator = block.getInstructions().listIterator(index);
-    this.metadata = metadata;
+  BasicBlockInstructionListIterator(BasicBlock block, int index) {
+    // TODO(b/376663044): Convert uses of index to use Instruction instead.
+    this(block, index == block.size() ? null : block.getInstructions().getNth(index));
   }
 
-  BasicBlockInstructionListIterator(
-      IRMetadata metadata, BasicBlock block, Instruction instruction) {
-    this(metadata, block);
-    nextUntil(x -> x == instruction);
+  BasicBlockInstructionListIterator(BasicBlock block, Instruction firstInstructionToReturn) {
+    this.block = block;
+    this.instructionList = block.getInstructions();
+    this.current = firstInstructionToReturn == null ? null : firstInstructionToReturn.getPrev();
+    this.next = firstInstructionToReturn;
   }
 
   public BasicBlock getBlock() {
@@ -70,54 +68,59 @@
 
   @Override
   public boolean hasNext() {
-    return listIterator.hasNext();
+    return next != null;
   }
 
   @Override
   public Instruction next() {
-    current = listIterator.next();
-    return current;
+    Instruction ret = next;
+    if (ret == null) {
+      throw new NoSuchElementException();
+    }
+    assert ret.block == block : "Iterator invalidated: " + ret;
+    current = ret;
+    next = ret.next;
+    return ret;
   }
 
   @Override
   public int nextIndex() {
-    return listIterator.nextIndex();
+    throw new UnsupportedOperationException();
   }
 
   @Override
   public Instruction peekNext() {
-    // Reset current since listIterator.remove() changes based on whether next() or previous() was
-    // last called.
-    // E.g.: next() -> current=C
-    // peekNext(): next() -> current=D, previous() -> current=D
-    current = null;
-    return IteratorUtils.peekNext(listIterator);
+    assert next == null || next.block == block : "Iterator invalidated: " + next;
+    return next;
   }
 
   @Override
   public boolean hasPrevious() {
-    return listIterator.hasPrevious();
+    return next == null ? !instructionList.isEmpty() : next.prev != null;
   }
 
   @Override
   public Instruction previous() {
-    current = listIterator.previous();
-    return current;
+    Instruction ret = next == null ? instructionList.getLastOrNull() : next.prev;
+    if (ret == null) {
+      throw new NoSuchElementException();
+    }
+    assert ret.block == block : "Iterator invalidated: " + next;
+    current = ret;
+    next = ret;
+    return ret;
   }
 
   @Override
   public int previousIndex() {
-    return listIterator.previousIndex();
+    throw new UnsupportedOperationException();
   }
 
   @Override
   public Instruction peekPrevious() {
-    // Reset current since listIterator.remove() changes based on whether next() or previous() was
-    // last called.
-    // E.g.: previous() -> current=B
-    // peekPrevious(): previous() -> current=A, next() -> current=A
-    current = null;
-    return IteratorUtils.peekPrevious(listIterator);
+    Instruction ret = next == null ? instructionList.getLastOrNull() : next.prev;
+    assert ret == null || ret.block == block : "Iterator invalidated: " + next;
+    return ret;
   }
 
   @Override
@@ -153,13 +156,10 @@
    */
   @Override
   public void add(Instruction instruction) {
-    instruction.setBlock(block);
-    assert instruction.getBlock() == block;
     if (!instruction.hasPosition() && hasInsertionPosition()) {
       instruction.setPosition(getInsertionPosition());
     }
-    listIterator.add(instruction);
-    metadata.record(instruction);
+    instructionList.addBefore(instruction, next);
   }
 
   private boolean hasPriorThrowingInstruction() {
@@ -239,10 +239,14 @@
    */
   @Override
   public void set(Instruction instruction) {
-    instruction.setBlock(block);
-    assert instruction.getBlock() == block;
-    listIterator.set(instruction);
-    metadata.record(instruction);
+    if (current == null) {
+      throw new IllegalStateException();
+    }
+    instructionList.replace(current, instruction);
+    if (current == next) {
+      next = instruction;
+    }
+    current = instruction;
   }
 
   @Override
@@ -253,6 +257,19 @@
     }
   }
 
+  /** Updates |current| and |next|, and returns the old |current|. */
+  private Instruction removeHelper() {
+    Instruction target = current;
+    if (target == null) {
+      throw new IllegalStateException();
+    }
+    if (target == next) {
+      next = target.next;
+    }
+    current = null;
+    return target;
+  }
+
   /**
    * Remove the current instruction (aka the {@link Instruction} returned by the previous call to
    * {@link #next}.
@@ -264,74 +281,31 @@
    */
   @Override
   public void remove() {
-    if (current == null) {
-      throw new IllegalStateException();
-    }
-    assert current.outValue() == null || !current.outValue().isUsed();
-    assert current.getDebugValues().isEmpty();
-    for (int i = 0; i < current.inValues().size(); i++) {
-      Value value = current.inValues().get(i);
-      value.removeUser(current);
-    }
-    // These needs to stay to ensure that an optimization incorrectly not taking debug info into
-    // account still produces valid code when run without enabled assertions.
-    for (Value value : current.getDebugValues()) {
-      value.removeDebugUser(current);
-    }
-    if (current.getLocalInfo() != null) {
-      for (Instruction user : current.outValue().debugUsers()) {
-        user.removeDebugValue(current.outValue());
-      }
-    }
-    listIterator.remove();
-    current = null;
+    instructionList.removeAndDetachInValues(removeHelper());
   }
 
   @Override
   public void removeInstructionIgnoreOutValue() {
-    if (current == null) {
-      throw new IllegalStateException();
-    }
-    listIterator.remove();
-    current = null;
+    instructionList.removeIgnoreValues(removeHelper());
   }
 
   @Override
   public void removeOrReplaceByDebugLocalRead() {
-    if (current == null) {
-      throw new IllegalStateException();
-    }
-    if (current.getDebugValues().isEmpty()) {
-      remove();
-    } else {
-      replaceCurrentInstruction(new DebugLocalRead());
-    }
+    instructionList.removeOrReplaceByDebugLocalRead(removeHelper());
   }
 
   @Override
-  public void replaceCurrentInstruction(Instruction newInstruction, Set<Value> affectedValues) {
+  public void replaceCurrentInstruction(Instruction newInstruction, AffectedValues affectedValues) {
     if (current == null) {
       throw new IllegalStateException();
     }
-    for (Value value : current.inValues()) {
-      value.removeUser(current);
+    if (current == next) {
+      // TODO(b/376663044): This should not advance the cursor. Prior implementation used remove()
+      // and add() rather than set(), which causes replaced item to appear when iterating backwards.
+      // E.g.: Should be: next = newInstruction;
+      next = next.next;
     }
-    if (current.hasUsedOutValue()) {
-      assert newInstruction.outValue() != null;
-      if (affectedValues != null && !newInstruction.getOutType().equals(current.getOutType())) {
-        current.outValue().addAffectedValuesTo(affectedValues);
-      }
-      current.outValue().replaceUsers(newInstruction.outValue());
-    }
-    current.moveDebugValues(newInstruction);
-    newInstruction.setBlock(block);
-    if (!newInstruction.hasPosition()) {
-      newInstruction.setPosition(current.getPosition());
-    }
-    listIterator.remove();
-    listIterator.add(newInstruction);
-    current.clearBlock();
-    metadata.record(newInstruction);
+    instructionList.replace(current, newInstruction, affectedValues);
     current = newInstruction;
   }
 
@@ -508,7 +482,7 @@
 
   @Override
   public void replaceCurrentInstructionWithStaticGet(
-      AppView<?> appView, IRCode code, DexField field, Set<Value> affectedValues) {
+      AppView<?> appView, IRCode code, DexField field, AffectedValues affectedValues) {
     if (current == null) {
       throw new IllegalStateException();
     }
@@ -691,22 +665,17 @@
     // Get the position at which the block is being split.
     Position position = getPreviousPosition();
 
-    // Prepare the new block, placing the exception handlers on the block with the throwing
-    // instruction.
-    BasicBlock newBlock = block.createSplitBlock(code.getNextBlockNumber(), keepCatchHandlers);
-
     // Add a goto instruction.
-    Goto newGoto = new Goto(block);
-    listIterator.add(newGoto);
+    Goto newGoto = new Goto();
+    instructionList.addBefore(newGoto, next);
     newGoto.setPosition(position);
 
-    // Move all remaining instructions to the new block.
-    while (listIterator.hasNext()) {
-      Instruction instruction = listIterator.next();
-      newBlock.getInstructions().addLast(instruction);
-      instruction.setBlock(newBlock);
-      listIterator.remove();
-    }
+    // Prepare the new block, placing the exception handlers on the block with the throwing
+    // instruction.
+    BasicBlock newBlock =
+        block.createSplitBlock(code.getNextBlockNumber(), keepCatchHandlers, next);
+    next = null;
+    current = null;
 
     // Insert the new block in the block list right after the current block.
     if (blocksIterator == null) {
@@ -917,10 +886,8 @@
         entryBlockIterator = entryBlock.listIterator(code);
         // Insert cast instruction into the new block.
         inlineEntry.listIterator(code).add(castInstruction);
-        castInstruction.setBlock(inlineEntry);
         assert castInstruction.getBlock().getInstructions().size() == 2;
       } else {
-        castInstruction.setBlock(entryBlock);
         entryBlockIterator = entryBlock.listIterator(code);
         entryBlockIterator.add(castInstruction);
       }
@@ -1011,9 +978,12 @@
     assert IteratorUtils.peekNext(blocksIterator) == invokeBlock;
 
     // Insert inlinee blocks into the IR code of the callee, before the invoke block.
+    IRMetadata ourMetadata = block.getMetadata();
+    ourMetadata.merge(inlineEntry.getMetadata());
     for (BasicBlock bb : inlinee.blocks) {
       bb.setNumber(code.getNextBlockNumber());
       blocksIterator.add(bb);
+      bb.setMetadata(ourMetadata);
     }
 
     // If the invoke block had catch handlers copy those down to all inlined blocks.
@@ -1057,7 +1027,7 @@
       it.previous();
       return it;
     }
-    BasicBlock newExitBlock = new BasicBlock();
+    BasicBlock newExitBlock = new BasicBlock(code.metadata());
     newExitBlock.setNumber(code.getNextBlockNumber());
     Return newReturn;
     if (normalExits.get(0).exit().asReturn().isReturnVoid()) {
@@ -1090,7 +1060,7 @@
     }
     // The newly constructed return will be eliminated as part of inlining so we set position none.
     newReturn.setPosition(Position.none());
-    newExitBlock.add(newReturn, metadata);
+    newExitBlock.add(newReturn, code.metadata());
     for (BasicBlock exitBlock : normalExits) {
       InstructionListIterator it = exitBlock.listIterator(code, exitBlock.getInstructions().size());
       Instruction oldExit = it.previous();
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlockIterator.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlockIterator.java
index 0b9c54c..0411cd2 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlockIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlockIterator.java
@@ -105,12 +105,10 @@
       throw new IllegalStateException();
     }
     // Remove all instructions from the block before removing the block.
-    InstructionListIterator iterator = current.listIterator(code);
-    while (iterator.hasNext()) {
-      Instruction instruction = iterator.next();
-      instruction.clearDebugValues();
-      iterator.remove();
+    for (Instruction ins = current.entry(); ins != null; ins = ins.getNext()) {
+      ins.detachInValues();
     }
+    current.getInstructions().clear();
     listIterator.remove();
     current = null;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/DominatorTree.java b/src/main/java/com/android/tools/r8/ir/code/DominatorTree.java
index 20eadc9..e04ff68 100644
--- a/src/main/java/com/android/tools/r8/ir/code/DominatorTree.java
+++ b/src/main/java/com/android/tools/r8/ir/code/DominatorTree.java
@@ -27,7 +27,7 @@
 
   private final BasicBlock[] sorted;
   private BasicBlock[] doms;
-  private final BasicBlock normalExitBlock = new BasicBlock();
+  private final BasicBlock normalExitBlock;
 
   private final int unreachableStartIndex;
 
@@ -40,6 +40,7 @@
   public DominatorTree(IRCode code, Assumption assumption) {
     assert assumption != null;
     assert assumption == MAY_HAVE_UNREACHABLE_BLOCKS || code.getUnreachableBlocks().isEmpty();
+    normalExitBlock = new BasicBlock(code.metadata());
 
     ImmutableList<BasicBlock> blocks = code.topologicallySortedBlocks();
     // Add the internal exit block to the block list.
diff --git a/src/main/java/com/android/tools/r8/ir/code/Goto.java b/src/main/java/com/android/tools/r8/ir/code/Goto.java
index 9beaff7..3c5a357 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Goto.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Goto.java
@@ -8,25 +8,10 @@
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.lightir.LirBuilder;
-import com.android.tools.r8.utils.ListUtils;
 import java.util.List;
 import java.util.ListIterator;
 
 public class Goto extends JumpInstruction {
-
-  public Goto() {
-    super();
-  }
-
-  public Goto(BasicBlock block) {
-    this();
-    setBlock(block);
-  }
-
-  public static Builder builder() {
-    return new Builder();
-  }
-
   @Override
   public int opcode() {
     return Opcodes.GOTO;
@@ -77,7 +62,7 @@
     // Avoids BasicBlock.exit(), since it will assert when block is invalid.
     if (myBlock != null
         && !myBlock.getSuccessors().isEmpty()
-        && ListUtils.last(myBlock.getInstructions()) == this) {
+        && myBlock.getInstructions().getLastOrNull() == this) {
       return super.toString() + "block " + getTarget().getNumberAsString();
     }
     return super.toString() + "block <unknown>";
@@ -129,24 +114,4 @@
   public void buildLir(LirBuilder<Value, ?> builder) {
     builder.addGoto(getTarget());
   }
-
-  public static class Builder extends BuilderBase<Builder, Goto> {
-
-    private BasicBlock target;
-
-    public Builder setTarget(BasicBlock target) {
-      this.target = target;
-      return self();
-    }
-
-    @Override
-    public Goto build() {
-      return amend(new Goto(target));
-    }
-
-    @Override
-    public Builder self() {
-      return this;
-    }
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index 307acdf..cbf7075 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -383,10 +383,10 @@
       if (hasSeenThrowingInstruction) {
         List<BasicBlock> successors = block.getSuccessors();
         if (successors.size() == 1 && ListUtils.first(successors).getPredecessors().size() > 1) {
-          BasicBlock splitBlock = block.createSplitBlock(getNextBlockNumber(), true);
-          Goto newGoto = new Goto(block);
+          BasicBlock splitBlock = block.createSplitBlock(getNextBlockNumber(), true, null);
+          Goto newGoto = new Goto();
           newGoto.setPosition(Position.none());
-          splitBlock.listIterator(this).add(newGoto);
+          splitBlock.getInstructions().addLast(newGoto);
           blockIterator.add(splitBlock);
         }
       }
@@ -1202,10 +1202,10 @@
   }
 
   public Argument getLastArgument() {
-    InstructionIterator instructionIterator = entryBlock().iterator(getNumberOfArguments() - 1);
-    Argument lastArgument = instructionIterator.next().asArgument();
+    Instruction lastArgInstr = entryBlock().getInstructions().getNth(getNumberOfArguments() - 1);
+    Argument lastArgument = lastArgInstr.asArgument();
     assert lastArgument != null;
-    assert !instructionIterator.peekNext().isArgument();
+    assert !lastArgInstr.getNext().isArgument();
     return lastArgument;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
index 13e393a..6eb4a80 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
@@ -104,7 +104,7 @@
 
   @Override
   public void replaceCurrentInstructionWithStaticGet(
-      AppView<?> appView, IRCode code, DexField field, Set<Value> affectedValues) {
+      AppView<?> appView, IRCode code, DexField field, AffectedValues affectedValues) {
     instructionIterator.replaceCurrentInstructionWithStaticGet(
         appView, code, field, affectedValues);
   }
@@ -278,7 +278,7 @@
   }
 
   @Override
-  public void replaceCurrentInstruction(Instruction newInstruction, Set<Value> affectedValues) {
+  public void replaceCurrentInstruction(Instruction newInstruction, AffectedValues affectedValues) {
     instructionIterator.replaceCurrentInstruction(newInstruction, affectedValues);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCodeUtils.java b/src/main/java/com/android/tools/r8/ir/code/IRCodeUtils.java
index e918ad4..4bb02dd 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCodeUtils.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCodeUtils.java
@@ -100,7 +100,7 @@
     } else {
       assert false;
     }
-    internalRemoveInstructionAndTransitiveInputsIfNotUsed(code, worklist);
+    internalRemoveInstructionAndTransitiveInputsIfNotUsed(worklist);
   }
 
   /**
@@ -109,14 +109,12 @@
    *
    * <p>Use with caution!
    */
-  public static void removeInstructionAndTransitiveInputsIfNotUsed(
-      IRCode code, Instruction instruction) {
-    internalRemoveInstructionAndTransitiveInputsIfNotUsed(
-        code, DequeUtils.newArrayDeque(instruction));
+  public static void removeInstructionAndTransitiveInputsIfNotUsed(Instruction instruction) {
+    internalRemoveInstructionAndTransitiveInputsIfNotUsed(DequeUtils.newArrayDeque(instruction));
   }
 
   private static void internalRemoveInstructionAndTransitiveInputsIfNotUsed(
-      IRCode code, Deque<InstructionOrPhi> worklist) {
+      Deque<InstructionOrPhi> worklist) {
     Set<InstructionOrPhi> removed = Sets.newIdentityHashSet();
     while (!worklist.isEmpty()) {
       InstructionOrPhi instructionOrPhi = worklist.removeFirst();
@@ -145,7 +143,7 @@
       } else {
         Instruction current = instructionOrPhi.asInstruction();
         if (!current.hasOutValue() || !current.outValue().hasAnyUsers()) {
-          current.getBlock().listIterator(code, current).removeOrReplaceByDebugLocalRead();
+          current.removeOrReplaceByDebugLocalRead();
           for (Value inValue : current.inValues()) {
             worklist.add(inValue.isPhi() ? inValue.asPhi() : inValue.definition);
           }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index d55ada3..2773951 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -28,6 +28,7 @@
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions;
+import com.android.tools.r8.ir.optimize.AffectedValues;
 import com.android.tools.r8.ir.optimize.DeadCodeRemover.DeadInstructionResult;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
@@ -48,13 +49,15 @@
 
 public abstract class Instruction
     implements AbstractInstruction, InstructionOrPhi, MaterializingInstructionsInfo {
-
-  protected Value outValue = null;
+  // Package-private to be used by InstructionList.
+  BasicBlock block;
+  Instruction prev;
+  Instruction next;
+  protected Value outValue;
   protected final List<Value> inValues = new ArrayList<>();
-  private BasicBlock block = null;
   private int number = -1;
-  private Set<Value> debugValues = null;
-  private Position position = null;
+  private Set<Value> debugValues;
+  private Position position;
 
   protected Instruction(Value outValue) {
     setOutValue(outValue);
@@ -82,6 +85,14 @@
     return position != null;
   }
 
+  public Instruction getPrev() {
+    return prev;
+  }
+
+  public Instruction getNext() {
+    return next;
+  }
+
   @Override
   public final Position getPosition() {
     assert position != null;
@@ -341,32 +352,56 @@
     return block;
   }
 
-  /**
-   * Set the basic block of this instruction. See IRBuilder.
-   */
-  public void setBlock(BasicBlock block) {
-    assert block != null;
-    this.block = block;
+  /** Prepares instruction for removal by removing its in-values. */
+  public Instruction detachInValues() {
+    for (Value value : inValues) {
+      value.removeUser(this);
+    }
+    // These needs to stay to ensure that an optimization incorrectly not taking debug info into
+    // account still produces valid code when run without enabled assertions.
+    if (debugValues != null) {
+      for (Value value : debugValues) {
+        value.removeDebugUser(this);
+      }
+    }
+    if (getLocalInfo() != null) {
+      for (Instruction user : outValue.debugUsers()) {
+        user.removeDebugValue(outValue);
+      }
+    }
+    return this;
   }
 
-  /**
-   * Clear the basic block of this instruction. Use when removing an instruction from a block.
-   */
-  public void clearBlock() {
-    assert block != null;
-    block = null;
+  public void removeOrReplaceByDebugLocalRead() {
+    block.getInstructions().removeOrReplaceByDebugLocalRead(this);
   }
 
-  public void removeOrReplaceByDebugLocalRead(IRCode code) {
-    getBlock().listIterator(code, this).removeOrReplaceByDebugLocalRead();
+  // TODO(b/376663044): Delete.
+  public void removeOrReplaceByDebugLocalRead(IRCode unused_code) {
+    block.getInstructions().removeOrReplaceByDebugLocalRead(this);
   }
 
-  public void replace(Instruction newInstruction, IRCode code) {
-    replace(newInstruction, code, null);
+  public void removeIgnoreValues() {
+    block.getInstructions().removeIgnoreValues(this);
   }
 
-  public void replace(Instruction newInstruction, IRCode code, Set<Value> affectedValues) {
-    getBlock().listIterator(code, this).replaceCurrentInstruction(newInstruction, affectedValues);
+  public void replace(Instruction newInstruction) {
+    block.getInstructions().replace(this, newInstruction);
+  }
+
+  // TODO(b/376663044): Delete.
+  public void replace(Instruction newInstruction, IRCode unused_code) {
+    block.getInstructions().replace(this, newInstruction);
+  }
+
+  public void replace(Instruction newInstruction, AffectedValues affectedValues) {
+    block.getInstructions().replace(this, newInstruction, affectedValues);
+  }
+
+  // TODO(b/376663044): Delete.
+  public void replace(
+      Instruction newInstruction, IRCode unused_code, AffectedValues affectedValues) {
+    block.getInstructions().replace(this, newInstruction, affectedValues);
   }
 
   /**
@@ -1630,6 +1665,20 @@
     // Intentionally empty.
   }
 
+  /** Returns whether the given instruction is encountered via continuous calls to getNext(). */
+  public boolean comesBefore(Instruction target) {
+    assert target != this;
+    assert target.block == block; // Probably a bug if this does not hold.
+    Instruction cur = next;
+    while (cur != null) {
+      if (cur == target) {
+        return true;
+      }
+      cur = cur.next;
+    }
+    return false;
+  }
+
   public static class SideEffectAssumption {
 
     public static final SideEffectAssumption NONE = new SideEffectAssumption();
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionList.java b/src/main/java/com/android/tools/r8/ir/code/InstructionList.java
new file mode 100644
index 0000000..f95b67a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionList.java
@@ -0,0 +1,302 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.code;
+
+import com.android.tools.r8.ir.optimize.AffectedValues;
+import java.util.Spliterator;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+/**
+ * Linked list of instructions using Instruction.prev and Instruction.next fields.
+ *
+ * <pre>
+ * Design notes:
+ * - There is a 1:1 relationship between a BasicBlock and InstructionList.
+ *   - We might want to just merge this into BasicBlock.
+ * - Updates IRMetadata and sets Instruction.block fields when instructions are added.
+ * - Does not implement Collection / List interfaces to better reflect that this is a
+ *   special-purpose collection, where operations sometimes mutate the state of elements.
+ * - Does not implement a non-assert "contains" method. Use Instruction.block to check containment.
+ * - Does not clear next/prev fields upon removal.
+ *   - Because there has not yet been a need to reuse instructions (except for the special case of
+ *     block splitting).
+ *   - |block| is cleared, and asserts prevent instructions from being added that already have a
+ *     block set.
+ * </pre>
+ */
+public class InstructionList implements Iterable<Instruction> {
+  private final BasicBlock ownerBlock;
+  private Instruction head;
+  private Instruction tail;
+  private int size;
+
+  public InstructionList(BasicBlock ownerBlock) {
+    this.ownerBlock = ownerBlock;
+  }
+
+  public int size() {
+    return size;
+  }
+
+  public boolean isEmpty() {
+    return head == null;
+  }
+
+  public Instruction getFirst() {
+    assert !isEmpty();
+    return head;
+  }
+
+  public Instruction getFirstOrNull() {
+    return head;
+  }
+
+  /** Returns the instruction at the given index. This is O(n). */
+  public Instruction getNth(int n) {
+    assert n >= 0 && n < size : "n=" + n + " size=" + size;
+    if (n > size / 2) {
+      Instruction ret = tail;
+      n = size - n - 1;
+      for (int i = 0; i < n; ++i) {
+        ret = ret.prev;
+      }
+      return ret;
+    }
+    Instruction ret = head;
+    for (int i = 0; i < n; ++i) {
+      ret = ret.next;
+    }
+    return ret;
+  }
+
+  public Instruction getLast() {
+    assert !isEmpty();
+    return tail;
+  }
+
+  public Instruction getLastOrNull() {
+    return tail;
+  }
+
+  public void addFirst(Instruction newInstruction) {
+    addBefore(newInstruction, head);
+  }
+
+  public void addLast(Instruction newInstruction) {
+    addBefore(newInstruction, null);
+  }
+
+  /**
+   * Inserts newInstruction before existingInstruction. If existingInstruction is null, adds at the
+   * end.
+   */
+  public void addBefore(Instruction newInstruction, Instruction existingInstruction) {
+    assert newInstruction.block == null;
+    if (existingInstruction != null) {
+      assert linearScanFinds(existingInstruction);
+    }
+    if (size == 0) {
+      assert existingInstruction == null;
+      head = newInstruction;
+      tail = newInstruction;
+    } else if (existingInstruction == null) {
+      // Add to end.
+      newInstruction.prev = tail;
+      tail.next = newInstruction;
+      tail = newInstruction;
+    } else {
+      Instruction prevInstruction = existingInstruction.prev;
+      newInstruction.prev = prevInstruction;
+      newInstruction.next = existingInstruction;
+      existingInstruction.prev = newInstruction;
+      if (prevInstruction == null) {
+        assert head == existingInstruction;
+        head = newInstruction;
+      } else {
+        prevInstruction.next = newInstruction;
+      }
+    }
+
+    size += 1;
+    newInstruction.block = ownerBlock;
+    ownerBlock.getMetadata().record(newInstruction);
+  }
+
+  /** Adopt all instructions from another list (use this to split blocks). */
+  public void severFrom(Instruction firstInstructionToMove) {
+    assert isEmpty();
+    BasicBlock sourceBlock = firstInstructionToMove.block;
+    InstructionList sourceList = sourceBlock.getInstructions();
+    // So far no need to sever between methods.
+    assert sourceBlock.getMetadata() == ownerBlock.getMetadata();
+
+    head = firstInstructionToMove;
+    tail = sourceList.tail;
+
+    Instruction lastInstructionToRemain = firstInstructionToMove.prev;
+    sourceList.tail = lastInstructionToRemain;
+    if (lastInstructionToRemain == null) {
+      sourceList.head = null;
+    } else {
+      lastInstructionToRemain.next = null;
+      firstInstructionToMove.prev = null;
+    }
+
+    int count = 0;
+    BasicBlock newBlock = ownerBlock;
+    for (Instruction current = firstInstructionToMove; current != null; current = current.next) {
+      count += 1;
+      current.block = newBlock;
+    }
+    size = count;
+    sourceList.size -= count;
+  }
+
+  public void replace(Instruction oldInstruction, Instruction newInstruction) {
+    replace(oldInstruction, newInstruction, null);
+  }
+
+  /**
+   * Replace the oldInstruction with newInstruction.
+   *
+   * <p>Removes in-values from oldInstruction.
+   *
+   * <p>If the current instruction produces an out-value the new instruction must also produce an
+   * out-value, and all uses of the current instructions out-value will be replaced by the new
+   * instructions out-value.
+   *
+   * <p>The debug information of the current instruction will be attached to the new instruction.
+   */
+  public void replace(
+      Instruction target, Instruction newInstruction, AffectedValues affectedValues) {
+    for (Value value : target.inValues()) {
+      value.removeUser(target);
+    }
+    if (target.hasUsedOutValue()) {
+      assert newInstruction.outValue() != null;
+      if (affectedValues != null && !newInstruction.getOutType().equals(target.getOutType())) {
+        target.outValue().addAffectedValuesTo(affectedValues);
+      }
+      target.outValue().replaceUsers(newInstruction.outValue());
+    }
+    target.moveDebugValues(newInstruction);
+    if (!newInstruction.hasPosition()) {
+      newInstruction.setPosition(target.getPosition());
+    }
+
+    addBefore(newInstruction, target);
+    removeIgnoreValues(target);
+  }
+
+  /**
+   * Safe removal function that will insert a DebugLocalRead to take over the debug values if any
+   * are associated with the current instruction.
+   */
+  public void removeOrReplaceByDebugLocalRead(Instruction target) {
+    if (target.getDebugValues().isEmpty()) {
+      removeAndDetachInValues(target);
+    } else {
+      replace(target, new DebugLocalRead());
+    }
+  }
+
+  /**
+   * Remove the given instruction.
+   *
+   * <p>The instruction will be removed and uses of its in-values removed.
+   *
+   * <p>If the current instruction produces an out-value this out value must not have any users.
+   */
+  public void removeAndDetachInValues(Instruction target) {
+    assert target.outValue() == null || !target.outValue().isUsed();
+    removeIgnoreValues(target.detachInValues());
+  }
+
+  /** Removes without doing any validation of in / out values. */
+  public void removeIgnoreValues(Instruction target) {
+    assert linearScanFinds(target);
+    target.block = null;
+    Instruction prev = target.prev;
+    Instruction next = target.next;
+    if (head == target) {
+      head = next;
+    }
+    if (tail == target) {
+      tail = prev;
+    }
+    if (prev != null) {
+      prev.next = next;
+    }
+    if (next != null) {
+      next.prev = prev;
+    }
+    size -= 1;
+  }
+
+  /** Removes all instructions. Instructions removed in this way may not be reused. */
+  public void clear() {
+    head = null;
+    tail = null;
+    size = 0;
+  }
+
+  // Non-assert uses should check instruction.block for containment. */
+  private boolean linearScanFinds(Instruction instruction) {
+    for (Instruction cur = head; cur != null; cur = cur.next) {
+      if (cur == instruction) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public Stream<Instruction> stream() {
+    return StreamSupport.stream(new SpliteratorImpl(), false);
+  }
+
+  @Override
+  public BasicBlockInstructionListIterator iterator() {
+    return new BasicBlockInstructionListIterator(ownerBlock);
+  }
+
+  public BasicBlockInstructionListIterator iterator(Instruction firstInstructionToReturn) {
+    return new BasicBlockInstructionListIterator(ownerBlock, firstInstructionToReturn);
+  }
+
+  private class SpliteratorImpl implements Spliterator<Instruction> {
+    Instruction next = head;
+
+    @Override
+    public boolean tryAdvance(Consumer<? super Instruction> action) {
+      if (next == null) {
+        return false;
+      }
+      action.accept(next);
+      next = next.next;
+      return true;
+    }
+
+    @Override
+    public Spliterator<Instruction> trySplit() {
+      return null;
+    }
+
+    @Override
+    public long estimateSize() {
+      return size;
+    }
+
+    @Override
+    public long getExactSizeIfKnown() {
+      return size;
+    }
+
+    @Override
+    public int characteristics() {
+      return SIZED | DISTINCT | NONNULL;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
index c1e8aa6..b9c54f1 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
@@ -98,7 +98,7 @@
     next();
   }
 
-  /** See {@link #replaceCurrentInstruction(Instruction, Set)}. */
+  /** See {@link #replaceCurrentInstruction(Instruction, AffectedValues)}. */
   default void replaceCurrentInstruction(Instruction newInstruction) {
     replaceCurrentInstruction(newInstruction, null);
   }
@@ -119,7 +119,7 @@
    * @param newInstruction the instruction to insert instead of the current.
    * @param affectedValues if non-null, all users of the out value will be added to this set.
    */
-  void replaceCurrentInstruction(Instruction newInstruction, Set<Value> affectedValues);
+  void replaceCurrentInstruction(Instruction newInstruction, AffectedValues affectedValues);
 
   // Do not show a deprecation warning for InstructionListIterator.remove().
   @SuppressWarnings("deprecation")
@@ -229,7 +229,7 @@
   void replaceCurrentInstructionWithNullCheck(AppView<?> appView, Value object);
 
   void replaceCurrentInstructionWithStaticGet(
-      AppView<?> appView, IRCode code, DexField field, Set<Value> affectedValues);
+      AppView<?> appView, IRCode code, DexField field, AffectedValues affectedValues);
 
   void replaceCurrentInstructionWithThrow(
       AppView<?> appView,
diff --git a/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
index 0885e72..99b0a1b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
@@ -64,7 +64,7 @@
   }
 
   @Override
-  public void replaceCurrentInstruction(Instruction newInstruction, Set<Value> affectedValues) {
+  public void replaceCurrentInstruction(Instruction newInstruction, AffectedValues affectedValues) {
     currentBlockIterator.replaceCurrentInstruction(newInstruction, affectedValues);
   }
 
@@ -133,7 +133,7 @@
 
   @Override
   public void replaceCurrentInstructionWithStaticGet(
-      AppView<?> appView, IRCode code, DexField field, Set<Value> affectedValues) {
+      AppView<?> appView, IRCode code, DexField field, AffectedValues affectedValues) {
     currentBlockIterator.replaceCurrentInstructionWithStaticGet(
         appView, code, field, affectedValues);
   }
@@ -322,7 +322,7 @@
     if (target == null || target.size() < 2) {
       return null;
     }
-    return target.getInstructions().get(target.size() - 2);
+    return target.getLastInstruction().getPrev();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
index 2d4b2ee..480be85 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -41,6 +41,7 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Inc;
 import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionList;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.JumpInstruction;
@@ -69,7 +70,6 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
@@ -79,7 +79,6 @@
 
   private static final int PEEPHOLE_OPTIMIZATION_PASSES = 2;
   private static final int SUFFIX_SHARING_OVERHEAD = 30;
-  private static final int IINC_PATTERN_SIZE = 4;
 
   public final AppView<?> appView;
   private final ProgramMethod method;
@@ -234,15 +233,13 @@
     Set<UninitializedThisLocalRead> uninitializedThisLocalReads = Sets.newIdentityHashSet();
     for (BasicBlock exitBlock : code.blocks) {
       if (exitBlock.exit().isThrow() && !exitBlock.hasCatchHandlers()) {
-        LinkedList<Instruction> instructions = exitBlock.getInstructions();
-        Instruction throwing = instructions.removeLast();
+        InstructionList instructions = exitBlock.getInstructions();
+        Instruction throwing = instructions.getLast();
         assert throwing.isThrow();
         UninitializedThisLocalRead read = new UninitializedThisLocalRead(code.getThis());
         read.setPosition(throwing.getPosition());
         uninitializedThisLocalReads.add(read);
-        read.setBlock(exitBlock);
-        instructions.addLast(read);
-        instructions.addLast(throwing);
+        instructions.addBefore(read, throwing);
       }
     }
     return uninitializedThisLocalReads;
@@ -322,7 +319,6 @@
         it.previous();
         Value constValue = code.createValue(inValue.getType());
         Instruction newInstruction = new ConstNumber(constValue, -1);
-        newInstruction.setBlock(block);
         newInstruction.setPosition(current.getPosition());
         it.add(newInstruction);
         it.next();
@@ -473,44 +469,43 @@
   @SuppressWarnings("ReferenceEquality")
   private void rewriteIincPatterns() {
     for (BasicBlock block : code.blocks) {
-      InstructionListIterator it = block.listIterator(code);
-      // Test that we have enough instructions for iinc.
-      while (IINC_PATTERN_SIZE <= block.getInstructions().size() - it.nextIndex()) {
-        Instruction loadOrConst1 = it.next();
-        if (!loadOrConst1.isLoad() && !loadOrConst1.isConstNumber()) {
+      InstructionList instructions = block.getInstructions();
+      for (Instruction ins = instructions.getFirst(); ins != null; ins = ins.getNext()) {
+        boolean isLoad = ins.isLoad();
+        if (!isLoad && !ins.isConstNumber()) {
           continue;
         }
         Load load;
         ConstNumber constNumber;
-        if (loadOrConst1.isLoad()) {
-          load = loadOrConst1.asLoad();
-          constNumber = it.next().asConstNumber();
+        Instruction nextInstruction = ins.getNext();
+        if (isLoad) {
+          load = ins.asLoad();
+          constNumber = nextInstruction.asConstNumber();
         } else {
-          load = it.next().asLoad();
-          constNumber = loadOrConst1.asConstNumber();
+          load = nextInstruction.asLoad();
+          constNumber = ins.asConstNumber();
         }
-        Instruction add = it.next().asAdd();
-        Instruction store = it.next().asStore();
-        // Reset pointer to load.
-        it.previous();
-        it.previous();
-        it.previous();
-        it.previous();
         if (load == null
             || constNumber == null
-            || add == null
-            || store == null
             || constNumber.getOutType() != TypeElement.getInt()) {
-          it.next();
+          continue;
+        }
+        Instruction add = nextInstruction.getNext();
+        if (add == null) {
+          break;
+        }
+        Instruction store = add.getNext();
+        if (store == null) {
+          break;
+        }
+        if (!add.isAdd() || !store.isStore()) {
           continue;
         }
         int increment = constNumber.getIntValue();
         if (increment < Byte.MIN_VALUE || Byte.MAX_VALUE < increment) {
-          it.next();
           continue;
         }
         if (getLocalRegister(load.src()) != getLocalRegister(store.outValue())) {
-          it.next();
           continue;
         }
         Position position = add.getPosition();
@@ -519,16 +514,15 @@
             || position != store.getPosition()) {
           continue;
         }
-        it.removeInstructionIgnoreOutValue();
-        it.next();
-        it.removeInstructionIgnoreOutValue();
-        it.next();
-        it.removeInstructionIgnoreOutValue();
-        it.next();
+
         Inc inc = new Inc(store.outValue(), load.inValues().get(0), increment);
         inc.setPosition(position);
-        inc.setBlock(block);
-        it.set(inc);
+        instructions.addBefore(inc, store);
+
+        load.removeIgnoreValues();
+        constNumber.removeIgnoreValues();
+        add.removeIgnoreValues();
+        store.removeIgnoreValues();
       }
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
index e9785ee..6f835bd 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
@@ -58,6 +58,7 @@
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.InstructionList;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.IntSwitch;
 import com.android.tools.r8.ir.code.JumpInstruction;
@@ -74,7 +75,6 @@
 import com.android.tools.r8.utils.InternalOutputMode;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
@@ -947,8 +947,7 @@
       item = tryItems.get(i);
       coalescedTryItems.add(item);
       // Trim the range start for non-throwing instructions when starting a new range.
-      List<com.android.tools.r8.ir.code.Instruction> instructions = blocksWithHandlers.get(i)
-          .getInstructions();
+      InstructionList instructions = blocksWithHandlers.get(i).getInstructions();
       for (com.android.tools.r8.ir.code.Instruction insn : instructions) {
         if (insn.instructionTypeCanThrow()) {
           item.start = getInfo(insn).getOffset();
@@ -1033,10 +1032,9 @@
 
   private int trimEnd(BasicBlock block) {
     // Trim the range end for non-throwing instructions when end has been computed.
-    List<com.android.tools.r8.ir.code.Instruction> instructions = block.getInstructions();
-    for (com.android.tools.r8.ir.code.Instruction insn : Lists.reverse(instructions)) {
-      if (insn.instructionTypeCanThrow()) {
-        Info info = getInfo(insn);
+    for (Instruction ins = block.getLastInstruction(); ins != null; ins = ins.getPrev()) {
+      if (ins.instructionTypeCanThrow()) {
+        Info info = getInfo(ins);
         return info.getOffset() + info.getSize();
       }
     }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index 2c84747..8e0b85a 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -282,9 +282,8 @@
     }
   }
 
-  public static class BlockInfo {
-
-    BasicBlock block = new BasicBlock();
+  public class BlockInfo {
+    BasicBlock block = new BasicBlock(metadata);
     IntSet normalPredecessors = new IntArraySet();
     IntSet normalSuccessors = new IntArraySet();
     IntSet exceptionalPredecessors = new IntArraySet();
@@ -697,14 +696,13 @@
     // Insert definitions for all uninitialized local values.
     if (uninitializedDebugLocalValues != null) {
       Position position = entryBlock.getPosition();
-      InstructionListIterator it = entryBlock.listIterator(metadata);
+      InstructionListIterator it = entryBlock.listIterator();
       it.nextUntil(i -> !i.isArgument());
       it.previous();
       for (List<Value> values : uninitializedDebugLocalValues.values()) {
         for (Value value : values) {
           if (value.isUsed()) {
             Instruction def = new DebugLocalUninitialized(value);
-            def.setBlock(entryBlock);
             def.setPosition(position);
             it.add(def);
           }
@@ -797,7 +795,7 @@
       return;
     }
     for (BasicBlock block : blocks) {
-      InstructionListIterator it = block.listIterator(metadata);
+      InstructionListIterator it = block.listIterator();
       Position current = null;
       while (it.hasNext()) {
         Instruction instruction = it.next();
@@ -1760,8 +1758,7 @@
   }
 
   public void addMoveResult(int dest) {
-    List<Instruction> instructions = currentBlock.getInstructions();
-    Invoke invoke = instructions.get(instructions.size() - 1).asInvoke();
+    Invoke invoke = currentBlock.getInstructions().getLast().asInvoke();
     assert invoke.outValue() == null;
     assert invoke.instructionTypeCanThrow();
     DexType outType = invoke.getReturnType();
@@ -2410,7 +2407,7 @@
         Set<BasicBlock> moveExceptionTargets = Sets.newIdentityHashSet();
         catchHandlers.forEach(
             (exceptionType, targetOffset) -> {
-              BasicBlock header = new BasicBlock();
+              BasicBlock header = new BasicBlock(currentBlock.getMetadata());
               header.incrementUnfilledPredecessorCount();
               ssaWorklist.add(
                   new MoveExceptionWorklistItem(
@@ -2669,7 +2666,7 @@
   }
 
   private static BasicBlock createSplitEdgeBlock(BasicBlock source, BasicBlock target) {
-    BasicBlock splitBlock = new BasicBlock();
+    BasicBlock splitBlock = new BasicBlock(source.getMetadata());
     splitBlock.incrementUnfilledPredecessorCount();
     splitBlock.getMutablePredecessors().add(source);
     splitBlock.getMutableSuccessors().add(target);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
index e8b022d..e9b1aa4 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
@@ -1079,7 +1079,7 @@
     AffectedValues affectedValues = new AffectedValues();
     for (UnusedArgument unusedArgument : unusedArguments) {
       InstructionListIterator instructionIterator =
-          unusedArgument.getBlock().listIterator(code, unusedArgument);
+          unusedArgument.getBlock().listIterator(code, unusedArgument.getNext());
       if (unusedArgument.outValue().hasAnyUsers()) {
         // This is an unused argument with a default value. The unused argument is an operand of the
         // phi. This use is eliminated after constant propagation + branch pruning. We eliminate the
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java
index 221adce..990257e 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/BranchSimplifier.java
@@ -28,6 +28,7 @@
 import com.android.tools.r8.ir.code.IfType;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.InstructionList;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.IntSwitch;
 import com.android.tools.r8.ir.code.InvokeStatic;
@@ -525,21 +526,20 @@
                 ConstNumber cstToUse = trueNumber.isIntegerOne() ? trueNumber : falseNumber;
                 BasicBlock phiBlock = phi.getBlock();
                 Position phiPosition = phiBlock.getPosition();
-                int insertIndex = 0;
+                InstructionList instructions = phiBlock.getInstructions();
+                Instruction prevHead = instructions.getFirst();
                 if (cstToUse.getBlock() == trueBlock || cstToUse.getBlock() == falseBlock) {
                   // The constant belongs to the block to remove, create a new one.
                   cstToUse = ConstNumber.copyOf(code, cstToUse);
-                  cstToUse.setBlock(phiBlock);
                   cstToUse.setPosition(phiPosition);
-                  phiBlock.getInstructions().add(insertIndex++, cstToUse);
+                  instructions.addBefore(cstToUse, prevHead);
                 }
                 phi.replaceUsers(newOutValue);
                 Instruction newInstruction =
                     Xor.create(NumericType.INT, newOutValue, testValue, cstToUse.outValue());
-                newInstruction.setBlock(phiBlock);
                 // The xor is replacing a phi so it does not have an actual position.
                 newInstruction.setPosition(phiPosition);
-                phiBlock.listIterator(code, insertIndex).add(newInstruction);
+                instructions.addBefore(newInstruction, prevHead);
                 deadPhis++;
               }
             }
@@ -565,7 +565,7 @@
 
     int instructionSize = b.getInstructions().size();
     if (b.exit().isGoto() && (instructionSize == 2 || instructionSize == 3)) {
-      Instruction constInstruction = b.getInstructions().get(instructionSize - 2);
+      Instruction constInstruction = b.getInstructions().getLast().getPrev();
       if (constInstruction.isConstNumber()) {
         if (!constInstruction.asConstNumber().isIntegerOne()
             && !constInstruction.asConstNumber().isIntegerZero()) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
index a213f7f..2bb1a5a 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
@@ -143,7 +143,7 @@
             ConstNumber newConstant =
                 ConstNumber.copyOf(code, constValue.definition.asConstNumber());
             newConstant.setPosition(currentInstruction.getPosition());
-            newConstant.setBlock(currentInstruction.getBlock());
+            currentInstruction.getBlock();
             currentInstruction.replaceValue(constValue, newConstant.outValue());
             constValue.removeUser(currentInstruction);
             instructionIterator.previous();
@@ -274,9 +274,7 @@
                   for (Instruction user : constantValue.uniqueUsers()) {
                     ConstNumber newCstNum = ConstNumber.copyOf(code, constNumber);
                     newCstNum.setPosition(user.getPosition());
-                    InstructionListIterator iterator = user.getBlock().listIterator(code, user);
-                    iterator.previous();
-                    iterator.add(newCstNum);
+                    user.getBlock().getInstructions().addBefore(newCstNum, user);
                     user.replaceValue(constantValue, newCstNum.outValue());
                   }
                   constantValue.clearUsers();
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
index e408515..f2f808e 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/FilledNewArrayRewriter.java
@@ -517,14 +517,13 @@
         assert !instructionIterator.getBlock().hasCatchHandlers();
         for (Value inValue : copy.asNewArrayFilled().inValues()) {
           instructionIterator.add(inValue.getDefinition());
-          inValue.getDefinition().setBlock(instructionIterator.getBlock());
+          inValue.getDefinition();
           inValue.getDefinition().setPosition(newArrayEmpty.getPosition());
         }
       } else {
         assert false;
         return elementValue;
       }
-      copy.setBlock(instructionIterator.getBlock());
       copy.setPosition(newArrayEmpty.getPosition());
       instructionIterator.add(copy);
       addToRemove(elementValue.getDefinition());
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
index 1e327f8..69fb225 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/NaturalIntLoopRemover.java
@@ -413,11 +413,11 @@
 
     private void patchControlFlow(IRCode code, BasicBlock comparisonBlock) {
       assert loopExit.getPhis().isEmpty(); // Edges should be split.
-      comparisonBlock.replaceLastInstruction(new Goto(loopBodyEntry), code);
+      comparisonBlock.replaceLastInstruction(new Goto(), code);
       comparisonBlock.removeSuccessor(loopExit);
 
       backPredecessor.replaceSuccessor(comparisonBlock, loopExit);
-      backPredecessor.replaceLastInstruction(new Goto(loopExit), code);
+      backPredecessor.replaceLastInstruction(new Goto(), code);
       comparisonBlock.removePredecessor(backPredecessor);
       loopExit.replacePredecessor(comparisonBlock, backPredecessor);
     }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ParentConstructorHoistingCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ParentConstructorHoistingCodeRewriter.java
index 43d249f..7b74732 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/ParentConstructorHoistingCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ParentConstructorHoistingCodeRewriter.java
@@ -23,7 +23,6 @@
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.TraversalContinuation;
 import com.android.tools.r8.utils.WorkList;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import java.util.ArrayDeque;
 import java.util.Deque;
@@ -96,15 +95,14 @@
         return;
       }
       // Change the instruction order and continue the hoisting.
-      List<Instruction> newInstructionOrder =
-          ImmutableList.<Instruction>builderWithExpectedSize(constants.size() + 2)
-              .addAll(constants)
-              .add(invoke)
-              .add(previousInstruction)
-              .build();
-      instructionIterator.next();
-      instructionIterator.set(newInstructionOrder);
-      IteratorUtils.skip(instructionIterator, -newInstructionOrder.size() - 1);
+      int numInstructions = constants.size() + 2;
+      for (int i = 0; i < numInstructions; ++i) {
+        instructionIterator.next().removeIgnoreValues();
+      }
+      instructionIterator.addAll(constants);
+      instructionIterator.add(invoke);
+      instructionIterator.add(previousInstruction);
+      IteratorUtils.skip(instructionIterator, -numInstructions);
     }
   }
 
@@ -121,8 +119,10 @@
     }
 
     // Remove the constants and the invoke from the block.
-    constants.forEach(constant -> block.getInstructions().removeFirst());
-    block.getInstructions().removeFirst();
+    int numToRemove = constants.size() + 1;
+    for (int i = 0; i < numToRemove; ++i) {
+      block.entry().removeIgnoreValues();
+    }
 
     // Add the constants and the invoke before the exit instruction in the predecessor block.
     InstructionListIterator predecessorInstructionIterator =
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/RedundantConstNumberRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/RedundantConstNumberRemover.java
index f031b5c..07b007c 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/RedundantConstNumberRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/RedundantConstNumberRemover.java
@@ -103,11 +103,10 @@
 
       if (ifInstruction.isZeroTest()) {
         changed |=
-            replaceDominatedConstNumbers(0, lhs, trueTarget, constantsByValue, code, dominatorTree);
+            replaceDominatedConstNumbers(0, lhs, trueTarget, constantsByValue, dominatorTree);
         if (lhs.knownToBeBoolean()) {
           changed |=
-              replaceDominatedConstNumbers(
-                  1, lhs, falseTarget, constantsByValue, code, dominatorTree);
+              replaceDominatedConstNumbers(1, lhs, falseTarget, constantsByValue, dominatorTree);
         }
       } else {
         assert rhs != null;
@@ -119,7 +118,6 @@
                   rhs,
                   trueTarget,
                   constantsByValue,
-                  code,
                   dominatorTree);
           if (lhs.knownToBeBoolean() && rhs.knownToBeBoolean()) {
             changed |=
@@ -128,7 +126,6 @@
                     rhs,
                     falseTarget,
                     constantsByValue,
-                    code,
                     dominatorTree);
           }
         } else {
@@ -140,7 +137,6 @@
                   lhs,
                   trueTarget,
                   constantsByValue,
-                  code,
                   dominatorTree);
           if (lhs.knownToBeBoolean() && rhs.knownToBeBoolean()) {
             changed |=
@@ -149,7 +145,6 @@
                     lhs,
                     falseTarget,
                     constantsByValue,
-                    code,
                     dominatorTree);
           }
         }
@@ -202,7 +197,6 @@
       Value newValue,
       BasicBlock dominator,
       LazyBox<Long2ReferenceMap<List<ConstNumber>>> constantsByValueSupplier,
-      IRCode code,
       LazyBox<DominatorTree> dominatorTree) {
     if (newValue.hasLocalInfo()) {
       // We cannot replace a constant with a value that has local info, because that could change
@@ -254,7 +248,7 @@
       if (dominatorTree.computeIfAbsent().dominatedBy(block, dominator)) {
         if (newValue.getType().lessThanOrEqual(value.getType(), appView)) {
           value.replaceUsers(newValue);
-          block.listIterator(code, constNumber).removeOrReplaceByDebugLocalRead();
+          constNumber.removeOrReplaceByDebugLocalRead();
           constantWithValueIterator.remove();
           changed = true;
         } else if (value.getType().isNullType()) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/StringSwitchRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/StringSwitchRemover.java
index 74fa808..4eb842c 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/StringSwitchRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/StringSwitchRemover.java
@@ -280,7 +280,7 @@
         newBlocksWithStrings.add(newBlock);
         if (previous == null) {
           // Replace the string-switch instruction by a goto instruction.
-          block.exit().replace(new Goto(newBlock), code);
+          block.exit().replace(new Goto(), code);
           block.link(newBlock);
         } else {
           // Set the fallthrough block for the previously added if-instruction.
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/SwitchCaseEliminator.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/SwitchCaseEliminator.java
index 4a1a1d4..5b95f17 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/SwitchCaseEliminator.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/SwitchCaseEliminator.java
@@ -23,7 +23,6 @@
 class SwitchCaseEliminator {
 
   private final BasicBlock block;
-  private final BasicBlock defaultTarget;
   private final InstructionListIterator iterator;
   private final Switch theSwitch;
 
@@ -35,7 +34,6 @@
 
   SwitchCaseEliminator(Switch theSwitch, InstructionListIterator iterator) {
     this.block = theSwitch.getBlock();
-    this.defaultTarget = theSwitch.fallthroughBlock();
     this.iterator = iterator;
     this.theSwitch = theSwitch;
   }
@@ -147,8 +145,7 @@
 
   private void replaceSwitchByGoto() {
     assert !hasAlwaysHitCase() || alwaysHitTarget != null;
-    BasicBlock target = hasAlwaysHitCase() ? alwaysHitTarget : defaultTarget;
-    iterator.replaceCurrentInstruction(new Goto(target));
+    iterator.replaceCurrentInstruction(new Goto());
   }
 
   private void replaceSwitchByOptimizedSwitch(
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialCheckCastAndInstanceOfRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialCheckCastAndInstanceOfRemover.java
index 4a2d298..3b1d253 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialCheckCastAndInstanceOfRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/TrivialCheckCastAndInstanceOfRemover.java
@@ -115,7 +115,7 @@
                   typeAnalysis -> typeAnalysis.setKeepRedundantBlocksAfterAssumeRemoval(true));
             }
             if (block.size() != blockSizeBeforeAssumeRemoval) {
-              it = previous != null ? block.listIterator(code, previous) : block.listIterator(code);
+              it = block.listIterator(code, previous != null ? previous.getNext() : block.entry());
             }
           }
         } else if (current.isInstanceOf()) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/AssertionsRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/AssertionsRewriter.java
index d002613..92fd99c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/AssertionsRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/AssertionsRewriter.java
@@ -495,7 +495,7 @@
                     dexItemFactory.createMethod(configuration.getAssertionHandler()),
                     null,
                     ImmutableList.of(throwInstruction.exception())));
-            Goto gotoBlockAfterAssertion = new Goto(throwingBlock);
+            Goto gotoBlockAfterAssertion = new Goto();
             gotoBlockAfterAssertion.setPosition(throwInstruction.getPosition());
             throwingBlock.link(throwSuccessorAfterHandler.get(throwInstruction));
             iterator.add(gotoBlockAfterAssertion);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/BasicBlockInstructionsEquivalence.java b/src/main/java/com/android/tools/r8/ir/optimize/BasicBlockInstructionsEquivalence.java
index b57fd9b..cf599a6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/BasicBlockInstructionsEquivalence.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/BasicBlockInstructionsEquivalence.java
@@ -7,12 +7,12 @@
 import com.android.tools.r8.ir.code.CatchHandlers;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionList;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions;
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.google.common.base.Equivalence;
 import java.util.Arrays;
-import java.util.Iterator;
 import java.util.List;
 
 class BasicBlockInstructionsEquivalence extends Equivalence<BasicBlock> {
@@ -30,19 +30,19 @@
   }
 
   private boolean hasIdenticalInstructions(BasicBlock first, BasicBlock second) {
-    List<Instruction> instructions0 = first.getInstructions();
-    List<Instruction> instructions1 = second.getInstructions();
+    InstructionList instructions0 = first.getInstructions();
+    InstructionList instructions1 = second.getInstructions();
     if (instructions0.size() != instructions1.size()) {
       return false;
     }
-    Iterator<Instruction> it0 = instructions0.iterator();
-    Iterator<Instruction> it1 = instructions1.iterator();
-    while (it0.hasNext()) {
-      Instruction i0 = it0.next();
-      Instruction i1 = it1.next();
+    Instruction i0 = instructions0.getFirstOrNull();
+    Instruction i1 = instructions1.getFirstOrNull();
+    while (i0 != null) {
       if (!i0.identicalAfterRegisterAllocation(i1, allocator, conversionOptions)) {
         return false;
       }
+      i0 = i0.getNext();
+      i1 = i1.getNext();
     }
 
     if (!allocator.hasEqualTypesAtEntry(first, second)) {
@@ -93,21 +93,21 @@
   }
 
   private int computeHash(BasicBlock basicBlock) {
-    List<Instruction> instructions = basicBlock.getInstructions();
+    InstructionList instructions = basicBlock.getInstructions();
     int hash = instructions.size();
     int i = 0;
-    for (Instruction instruction : instructions) {
+    for (Instruction inst = instructions.getFirstOrNull(); inst != null; inst = inst.getNext()) {
       if (++i > MAX_HASH_INSTRUCTIONS) {
         break;
       }
       int hashPart = 0;
-      if (instruction.outValue() != null && instruction.outValue().needsRegister()) {
-        hashPart += allocator.getRegisterForValue(instruction.outValue(), instruction.getNumber());
+      if (inst.outValue() != null && inst.outValue().needsRegister()) {
+        hashPart += allocator.getRegisterForValue(inst.outValue(), inst.getNumber());
       }
-      for (Value inValue : instruction.inValues()) {
+      for (Value inValue : inst.inValues()) {
         hashPart = hashPart << 4;
         if (inValue.needsRegister()) {
-          hashPart += allocator.getRegisterForValue(inValue, instruction.getNumber());
+          hashPart += allocator.getRegisterForValue(inValue, inst.getNumber());
         }
       }
       hash = hash * 3 + hashPart;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index 0e8ca85..f204dae 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -14,7 +14,6 @@
 import com.android.tools.r8.ir.code.DebugLocalsChange;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Move;
 import com.android.tools.r8.ir.code.Position;
@@ -259,15 +258,13 @@
     IntSet clobberedRegisters = new IntOpenHashSet();
     // Backwards instruction scan collecting the registers used by actual instructions.
     boolean inEntrySpillMoves = false;
-    InstructionIterator it = block.iterator(block.getInstructions().size());
-    while (it.hasPrevious()) {
-      Instruction instruction = it.previous();
-      if (instruction == postSpillLocalsChange) {
+    for (Instruction ins = block.getLastInstruction(); ins != null; ins = ins.getPrev()) {
+      if (ins == postSpillLocalsChange) {
         inEntrySpillMoves = true;
       }
       // If this is a move in the block-entry spill moves check if it is unneeded.
-      if (inEntrySpillMoves && instruction.isMove()) {
-        Move move = instruction.asMove();
+      if (inEntrySpillMoves && ins.isMove()) {
+        Move move = ins.asMove();
         int dst = allocator.getRegisterForValue(move.dest(), move.getNumber());
         int src = allocator.getRegisterForValue(move.src(), move.getNumber());
         if (!usedRegisters.contains(dst) && !clobberedRegisters.contains(src)) {
@@ -275,18 +272,17 @@
           continue;
         }
       }
-      if (instruction.outValue() != null && instruction.outValue().needsRegister()) {
-        int register =
-            allocator.getRegisterForValue(instruction.outValue(), instruction.getNumber());
+      if (ins.outValue() != null && ins.outValue().needsRegister()) {
+        int register = allocator.getRegisterForValue(ins.outValue(), ins.getNumber());
         // The register is defined anew, so uses before this are on distinct values.
         usedRegisters.remove(register);
         // Mark it clobbered to avoid any uses in locals after this point to become invalid.
         clobberedRegisters.add(register);
       }
-      if (!instruction.inValues().isEmpty()) {
-        for (Value inValue : instruction.inValues()) {
+      if (!ins.inValues().isEmpty()) {
+        for (Value inValue : ins.inValues()) {
           if (inValue.needsRegister()) {
-            int register = allocator.getRegisterForValue(inValue, instruction.getNumber());
+            int register = allocator.getRegisterForValue(inValue, ins.getNumber());
             // Record the register as being used.
             usedRegisters.add(register);
           }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
index 7a18075..a4e941c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
@@ -252,10 +252,7 @@
       for (Instruction oldInstruction : entry.getValue()) {
         if (oldInstruction != newInstruction) {
           oldInstruction.outValue().replaceUsers(newInstruction.outValue());
-          oldInstruction
-              .getBlock()
-              .listIterator(code, oldInstruction)
-              .removeOrReplaceByDebugLocalRead();
+          oldInstruction.removeOrReplaceByDebugLocalRead();
 
           // If the removed instruction is an insertion point for another constant, then record that
           // the constant should instead be inserted at the point where the removed instruction has
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index d73ab2a..add17fb 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -660,7 +660,7 @@
         code.prepareBlocksForCatchHandlers();
 
         // Create a block for holding the monitor-exit instruction.
-        BasicBlock monitorExitBlock = new BasicBlock();
+        BasicBlock monitorExitBlock = new BasicBlock(code.metadata());
         monitorExitBlock.setNumber(code.getNextBlockNumber());
 
         // For each block in the code that may throw, add a catch-all handler targeting the
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java
index 84fe7f8..3f22277 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java
@@ -11,7 +11,7 @@
 import com.android.tools.r8.ir.code.Goto;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.InstructionList;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Value;
@@ -26,7 +26,6 @@
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.IdentityHashMap;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
@@ -157,17 +156,15 @@
 
       // Remove the instruction from the normal successors.
       for (BasicBlock normalSuccessor : normalSuccessors) {
-        normalSuccessor.getInstructions().removeFirst();
+        normalSuccessor.entry().removeIgnoreValues();
       }
 
       // Move the instruction into the predecessor.
       if (instruction.isJumpInstruction()) {
         // Replace jump instruction in predecessor with the jump instruction from the normal
         // successors.
-        LinkedList<Instruction> instructions = block.getInstructions();
-        instructions.removeLast();
-        instructions.add(instruction);
-        instruction.setBlock(block);
+        block.getLastInstruction().removeIgnoreValues();
+        block.getInstructions().addLast(instruction);
 
         // Take a copy of the old normal successors before performing a destructive update.
         List<BasicBlock> oldNormalSuccessors = new ArrayList<>(normalSuccessors);
@@ -191,8 +188,7 @@
         }
       } else {
         // Insert instruction before the jump instruction in the predecessor.
-        block.getInstructions().listIterator(block.getInstructions().size() - 1).add(instruction);
-        instruction.setBlock(block);
+        block.getInstructions().addBefore(instruction, block.getLastInstruction());
 
         // Update locals-at-entry if needed.
         if (instruction.isDebugLocalsChange()) {
@@ -238,7 +234,7 @@
     BasicBlock normalExit = null;
     List<BasicBlock> normalExits = code.computeNormalExitBlocks();
     if (normalExits.size() > 1) {
-      normalExit = new BasicBlock();
+      normalExit = new BasicBlock(code.metadata());
       normalExit.getMutablePredecessors().addAll(normalExits);
       blocks = new ArrayList<>(code.blocks);
       blocks.add(normalExit);
@@ -259,8 +255,7 @@
           if (pred.exit().isGoto()
               && pred.getSuccessors().size() == 1
               && pred.getInstructions().size() > 1) {
-            List<Instruction> instructions = pred.getInstructions();
-            Instruction lastInstruction = instructions.get(instructions.size() - 2);
+            Instruction lastInstruction = pred.getLastInstruction().getPrev();
             List<BasicBlock> value = lastInstructionToBlocks.computeIfAbsent(
                 equivalence.wrap(lastInstruction), (k) -> new ArrayList<>());
             value.add(pred);
@@ -297,7 +292,7 @@
           }
           BasicBlock newBlock =
               createAndInsertBlockForSuffix(
-                  code.getNextBlockNumber(),
+                  code,
                   commonSuffixSize,
                   predsWithSameLastInstruction,
                   block == normalExit ? null : block,
@@ -318,7 +313,7 @@
   }
 
   private static BasicBlock createAndInsertBlockForSuffix(
-      int blockNumber,
+      IRCode code,
       int suffixSize,
       List<BasicBlock> preds,
       BasicBlock successorBlock,
@@ -326,49 +321,57 @@
     BasicBlock first = preds.get(0);
     assert (successorBlock != null && first.exit().isGoto())
         || (successorBlock == null && first.exit().isReturn());
-    BasicBlock newBlock = new BasicBlock();
-    newBlock.setNumber(blockNumber);
-    InstructionIterator from = first.iterator(first.getInstructions().size());
+    BasicBlock newBlock = new BasicBlock(code.metadata());
+    newBlock.setNumber(code.getNextBlockNumber());
     Int2ReferenceMap<DebugLocalInfo> newBlockEntryLocals = null;
     if (first.getLocalsAtEntry() != null) {
       newBlockEntryLocals = new Int2ReferenceOpenHashMap<>(first.getLocalsAtEntry());
       int prefixSize = first.getInstructions().size() - suffixSize;
-      InstructionIterator it = first.iterator();
+      Instruction instruction = first.entry();
       for (int i = 0; i < prefixSize; i++) {
-        Instruction instruction = it.next();
         if (instruction.isDebugLocalsChange()) {
           instruction.asDebugLocalsChange().apply(newBlockEntryLocals);
         }
+        instruction = instruction.getNext();
       }
     }
 
     allocator.addNewBlockToShareIdenticalSuffix(newBlock, suffixSize, preds);
 
     boolean movedThrowingInstruction = false;
+    Instruction instruction = first.getLastInstruction();
     for (int i = 0; i < suffixSize; i++) {
-      Instruction instruction = from.previous();
       movedThrowingInstruction = movedThrowingInstruction || instruction.instructionTypeCanThrow();
-      newBlock.getInstructions().addFirst(instruction);
-      instruction.setBlock(newBlock);
+      instruction = instruction.getPrev();
     }
+    newBlock
+        .getInstructions()
+        .severFrom(instruction == null ? first.entry() : instruction.getNext());
     if (movedThrowingInstruction && first.hasCatchHandlers()) {
       newBlock.transferCatchHandlers(first);
     }
+
     for (BasicBlock pred : preds) {
-      Position lastPosition = pred.getPosition();
-      LinkedList<Instruction> instructions = pred.getInstructions();
-      for (int i = 0; i < suffixSize; i++) {
-        instructions.removeLast();
+      Position lastPosition;
+      InstructionList instructions = pred.getInstructions();
+      if (pred == first) {
+        // Already removed from first via severFrom().
+        lastPosition = newBlock.getPosition();
+      } else {
+        lastPosition = pred.getPosition();
+        for (int i = 0; i < suffixSize; i++) {
+          instructions.removeIgnoreValues(instructions.getLast());
+        }
       }
-      for (Instruction instruction : pred.getInstructions()) {
-        if (instruction.getPosition().isSome()) {
-          lastPosition = instruction.getPosition();
+      for (Instruction ins = instructions.getLastOrNull(); ins != null; ins = ins.getPrev()) {
+        if (ins.getPosition().isSome()) {
+          lastPosition = ins.getPosition();
+          break;
         }
       }
       Goto jump = new Goto();
-      jump.setBlock(pred);
       jump.setPosition(lastPosition);
-      instructions.add(jump);
+      instructions.addLast(jump);
       newBlock.getMutablePredecessors().add(pred);
       if (successorBlock != null) {
         pred.replaceSuccessor(successorBlock, newBlock);
@@ -411,15 +414,15 @@
     if (!Objects.equals(localsAtBlockExit(block0), localsAtBlockExit(block1))) {
       return 0;
     }
-    InstructionIterator it0 = block0.iterator(block0.getInstructions().size());
-    InstructionIterator it1 = block1.iterator(block1.getInstructions().size());
+    Instruction i0 = block0.getLastInstruction();
+    Instruction i1 = block1.getLastInstruction();
     int suffixSize = 0;
-    while (it0.hasPrevious() && it1.hasPrevious()) {
-      Instruction i0 = it0.previous();
-      Instruction i1 = it1.previous();
+    while (i0 != null && i1 != null) {
       if (!i0.identicalAfterRegisterAllocation(i1, allocator, code.getConversionOptions())) {
         return suffixSize;
       }
+      i0 = i0.getPrev();
+      i1 = i1.getPrev();
       suffixSize++;
     }
     return suffixSize;
@@ -464,9 +467,8 @@
             assert !otherPred.getPredecessors().contains(pred);
             otherPred.getMutablePredecessors().add(pred);
             Goto exit = new Goto();
-            exit.setBlock(pred);
             exit.setPosition(otherPred.getPosition());
-            pred.getInstructions().add(exit);
+            pred.getInstructions().addLast(exit);
           } else {
             blockToIndex.put(wrapper, predIndex);
           }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/PhiOptimizations.java b/src/main/java/com/android/tools/r8/ir/optimize/PhiOptimizations.java
index 9a5e3b9..ec3a5a4 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/PhiOptimizations.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/PhiOptimizations.java
@@ -7,7 +7,6 @@
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.Load;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Store;
@@ -86,10 +85,8 @@
   private static int getStackHeightAtInstructionBackwards(Instruction instruction) {
     int stackHeight = 0;
     BasicBlock block = instruction.getBlock();
-    InstructionIterator it = block.iterator(block.getInstructions().size() - 1);
-    while (it.hasPrevious()) {
-      Instruction current = it.previous();
-      if (current == instruction) {
+    for (Instruction ins = block.exit().getPrev(); ins != null; ins = ins.getPrev()) {
+      if (ins == instruction) {
         break;
       }
       stackHeight -= PeepholeHelper.numberOfValuesPutOnStack(instruction);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RuntimeWorkaroundCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/RuntimeWorkaroundCodeRewriter.java
index bcb57ce..06f3c5f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/RuntimeWorkaroundCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RuntimeWorkaroundCodeRewriter.java
@@ -65,7 +65,6 @@
       // Split out the last recursive call in its own block.
       InstructionListIterator splitIterator =
           lastSelfRecursiveCall.getBlock().listIterator(code, lastSelfRecursiveCall);
-      splitIterator.previous();
       BasicBlock newBlock = splitIterator.split(code, 1);
       // Generate rethrow block.
       DexType guard = appView.dexItemFactory().throwableType;
@@ -251,7 +250,6 @@
               && target.getNormalPredecessors().size() > 1
               && target.getNormalSuccessors().size() > 1) {
             Instruction fixit = new AlwaysMaterializingNop();
-            fixit.setBlock(handler);
             fixit.setPosition(handler.getPosition());
             handler.getInstructions().addFirst(fixit);
           }
@@ -362,12 +360,10 @@
     // Forced definition of const-zero
     Value fixitValue = code.createValue(TypeElement.getInt());
     Instruction fixitDefinition = new AlwaysMaterializingDefinition(fixitValue);
-    fixitDefinition.setBlock(addBefore.getBlock());
     fixitDefinition.setPosition(addBefore.getPosition());
     it.add(fixitDefinition);
     // Forced user of the forced definition to ensure it has a user and thus live range.
     Instruction fixitUser = new AlwaysMaterializingUser(fixitValue);
-    fixitUser.setBlock(addBefore.getBlock());
     fixitUser.setPosition(addBefore.getPosition());
     it.add(fixitUser);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/FieldValueHelper.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/FieldValueHelper.java
index 0e185da..a7cf6e1 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/FieldValueHelper.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/FieldValueHelper.java
@@ -14,7 +14,6 @@
 import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Phi.RegisterReadType;
@@ -125,14 +124,9 @@
 
   @SuppressWarnings("ReferenceEquality")
   private Value getValueDefinedInTheBlock(BasicBlock block, Instruction stopAt) {
-    InstructionIterator iterator =
-        stopAt == null ? block.iterator(block.getInstructions().size()) : block.iterator(stopAt);
-
     Instruction valueProducingInsn = null;
-    while (iterator.hasPrevious()) {
-      Instruction instruction = iterator.previous();
-      assert instruction != null;
-
+    Instruction instruction = stopAt != null ? stopAt : block.getLastInstruction();
+    do {
       if (instruction == root
           || (instruction.isInstancePut()
               && instruction.asInstancePut().getField() == field
@@ -140,7 +134,8 @@
         valueProducingInsn = instruction;
         break;
       }
-    }
+      instruction = instruction.getPrev();
+    } while (instruction != null);
 
     if (valueProducingInsn == null) {
       return null;
@@ -151,7 +146,7 @@
 
     assert root == valueProducingInsn;
     if (defaultValue == null) {
-      InstructionListIterator it = block.listIterator(code, root);
+      InstructionListIterator it = block.listIterator(code, root.getNext());
       // If we met newInstance it means that default value is supposed to be used.
       if (field.type.isPrimitiveType()) {
         defaultValue =
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
index 403719e..da709f6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
@@ -36,7 +36,6 @@
 import com.android.tools.r8.ir.code.AliasedValueConfiguration;
 import com.android.tools.r8.ir.code.AssumeAndCheckCastAliasedValueConfiguration;
 import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.BasicBlockInstructionListIterator;
 import com.android.tools.r8.ir.code.BasicBlockIterator;
 import com.android.tools.r8.ir.code.CheckCast;
 import com.android.tools.r8.ir.code.IRCode;
@@ -634,7 +633,7 @@
       if (user.isInstanceOf()) {
         InstanceOf instanceOf = user.asInstanceOf();
         InstructionListIterator instructionIterator =
-            user.getBlock().listIterator(code, instanceOf);
+            user.getBlock().listIterator(code, instanceOf.getNext());
         instructionIterator.replaceCurrentInstructionWithConstBoolean(
             code, appView.appInfo().isSubtype(eligibleClass.getType(), instanceOf.type()));
         continue;
@@ -802,8 +801,7 @@
       affectedValues.addAll(newValue.affectedValues());
     }
     if (replacement != null) {
-      BasicBlockInstructionListIterator it = fieldRead.getBlock().listIterator(code, fieldRead);
-      it.replaceCurrentInstruction(replacement, affectedValues);
+      fieldRead.replace(replacement, affectedValues);
     } else {
       removeInstruction(fieldRead);
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
index 8b566cf..5452d6e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
@@ -296,8 +296,7 @@
       }
 
       if (ordinalToTargetMap.isEmpty()) {
-        switchInsn.replace(
-            Goto.builder().setTarget(block.getUniqueNormalSuccessor()).build(), code);
+        switchInsn.replace(new Goto(), code);
       } else {
         int[] keys = ordinalToTargetMap.keySet().toIntArray();
         Arrays.sort(keys);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
index caf6e7e..0a54d91 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/outliner/OutlinerImpl.java
@@ -1305,16 +1305,12 @@
           returnValue = null;
         }
         Invoke outlineInvoke = new InvokeStatic(outlineMethod, returnValue, in);
-        outlineInvoke.setBlock(lastInstruction.getBlock());
+        lastInstruction.getBlock();
         outlineInvoke.setPosition(
             positionBuilder.hasOutlinePositions()
                 ? positionBuilder.build()
                 : Position.syntheticNone());
-        InstructionListIterator endIterator =
-            lastInstruction.getBlock().listIterator(code, lastInstruction);
-        Instruction instructionBeforeEnd = endIterator.previous();
-        assert instructionBeforeEnd == lastInstruction;
-        endIterator.set(outlineInvoke);
+        lastInstruction.replace(outlineInvoke);
         if (outlineInvoke.hasOutValue()
             && returnValue.getType().isReferenceType()
             && returnValue.getType().nullability().isDefinitelyNotNull()) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/peepholes/PeepholeHelper.java b/src/main/java/com/android/tools/r8/ir/optimize/peepholes/PeepholeHelper.java
index 5fef766..c0ea809 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/peepholes/PeepholeHelper.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/peepholes/PeepholeHelper.java
@@ -58,18 +58,12 @@
       InstructionListIterator it, List<Instruction> instructions) {
     assert !instructions.isEmpty();
     for (Instruction instruction : instructions) {
+      // Add duplicate users to offset the users being removed by removeOrReplaceByDebugLocalRead().
       for (Value inValue : instruction.inValues()) {
         inValue.addUser(instruction);
       }
+      instruction.removeOrReplaceByDebugLocalRead();
       it.add(instruction);
     }
-    Instruction current = it.nextUntil(i -> i == instructions.get(0));
-    for (int i = 0; i < instructions.size(); i++) {
-      assert current == instructions.get(i);
-      it.removeOrReplaceByDebugLocalRead();
-      current = it.next();
-    }
-    it.previousUntil(i -> i == instructions.get(instructions.size() - 1));
-    it.next();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
index 3cc749b..58af88f 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
@@ -354,7 +354,6 @@
       while (it.hasNext()) {
         Instruction instruction = it.next();
         if (instruction.isDebugLocalRead()) {
-          instruction.clearDebugValues();
           it.remove();
         }
       }
@@ -3166,9 +3165,9 @@
         }
       }
       LinkedHashSetUtils.addAll(live, phiOperands);
-      List<Instruction> instructions = block.getInstructions();
+      int numInstructionsDelta = block.getInstructions().size() * INSTRUCTION_NUMBER_DELTA;
       for (Value value : live) {
-        int end = block.entry().getNumber() + instructions.size() * INSTRUCTION_NUMBER_DELTA;
+        int end = block.entry().getNumber() + numInstructionsDelta;
         // Make sure that phi operands do not overlap the phi live range. The phi operand is
         // not live until the next instruction, but only until the gap before the next instruction
         // where the phi value takes over.
@@ -3177,10 +3176,8 @@
         }
         addLiveRange(value, block, end, liveIntervals, code);
       }
-      InstructionIterator iterator = block.iterator(block.getInstructions().size());
-      while (iterator.hasPrevious()) {
-        Instruction instruction = iterator.previous();
-        Value definition = instruction.outValue();
+      for (Instruction ins = block.getLastInstruction(); ins != null; ins = ins.getPrev()) {
+        Value definition = ins.outValue();
         if (definition != null) {
           // For instructions that define values which have no use create a live range covering
           // the instruction. This will typically be instructions that can have side effects even
@@ -3193,23 +3190,23 @@
             addLiveRange(
                 definition,
                 block,
-                instruction.getNumber() + INSTRUCTION_NUMBER_DELTA - 1,
+                ins.getNumber() + INSTRUCTION_NUMBER_DELTA - 1,
                 liveIntervals,
                 code);
-            assert !code.getConversionOptions().isGeneratingClassFiles() || instruction.isArgument()
+            assert !code.getConversionOptions().isGeneratingClassFiles() || ins.isArgument()
                 : "Arguments should be the only potentially unused local in CF";
           }
           live.remove(definition);
         }
-        for (Value use : instruction.inValues()) {
+        for (Value use : ins.inValues()) {
           if (use.needsRegister()) {
-            assert unconstrainedForCf(instruction.maxInValueRegister(), code);
+            assert unconstrainedForCf(ins.maxInValueRegister(), code);
             if (!live.contains(use)) {
               live.add(use);
-              addLiveRange(use, block, instruction.getNumber(), liveIntervals, code);
+              addLiveRange(use, block, ins.getNumber(), liveIntervals, code);
             }
             if (code.getConversionOptions().isGeneratingDex()) {
-              int inConstraint = instruction.maxInValueRegister();
+              int inConstraint = ins.maxInValueRegister();
               LiveIntervals useIntervals = use.getLiveIntervals();
               // Arguments are always kept in their original, incoming register. For every
               // unconstrained use of an argument we therefore use its incoming register.
@@ -3223,11 +3220,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
-                      && !isInvokeRange(instruction);
+                  use.isArgument() && inConstraint == Constants.U16BIT_MAX && !isInvokeRange(ins);
               if (!isUnconstrainedArgumentUse) {
-                useIntervals.addUse(new LiveIntervalsUse(instruction.getNumber(), inConstraint));
+                useIntervals.addUse(new LiveIntervalsUse(ins.getNumber(), inConstraint));
               }
             }
           }
@@ -3239,24 +3234,20 @@
         // 'r1 <- check-cast r0' maps to 'move r1, r0; check-cast r1' and when that
         // happens r1 could be clobbered on the exceptional edge if r1 initially contained
         // a value that is used in the exceptional code.
-        if (instruction.instructionTypeCanThrow()) {
+        if (ins.instructionTypeCanThrow()) {
           for (Value use : liveAtThrowingInstruction) {
             if (use.needsRegister() && !live.contains(use)) {
               live.add(use);
               addLiveRange(
-                  use,
-                  block,
-                  getLiveRangeEndOnExceptionalFlow(instruction, use),
-                  liveIntervals,
-                  code);
+                  use, block, getLiveRangeEndOnExceptionalFlow(ins, use), liveIntervals, code);
             }
           }
         }
         if (appView.options().debug || code.context().isReachabilitySensitive()) {
           // In debug mode, or if the method is reachability sensitive, extend the live range
           // to cover the full scope of a local variable (encoded as debug values).
-          int number = instruction.getNumber();
-          List<Value> sortedDebugValues = new ArrayList<>(instruction.getDebugValues());
+          int number = ins.getNumber();
+          List<Value> sortedDebugValues = new ArrayList<>(ins.getDebugValues());
           sortedDebugValues.sort(Value::compareTo);
           for (Value use : sortedDebugValues) {
             assert use.needsRegister();
diff --git a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
index 27f4575..fd08861 100644
--- a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
@@ -356,7 +356,7 @@
       return blocks.computeIfAbsent(
           instructionIndex,
           k -> {
-            BasicBlock block = new BasicBlock();
+            BasicBlock block = new BasicBlock(irMetadata);
             block.setNumber(basicBlockNumberGenerator.next());
             return block;
           });
@@ -424,9 +424,7 @@
       int index = toInstructionIndexInIR(peekNextInstructionIndex());
       advanceInstructionState();
       instruction.setPosition(currentPosition);
-      currentBlock.getInstructions().add(instruction);
-      irMetadata.record(instruction);
-      instruction.setBlock(currentBlock);
+      currentBlock.getInstructions().addLast(instruction);
       int[] debugEndIndices = code.getDebugLocalEnds(index);
       if (debugEndIndices != null) {
         for (int encodedDebugEndIndex : debugEndIndices) {
@@ -471,9 +469,7 @@
       // which would otherwise advance the state.
       Argument argument = new Argument(dest, currentBlock.size(), isBooleanType);
       argument.setPosition(currentPosition);
-      currentBlock.getInstructions().add(argument);
-      irMetadata.record(argument);
-      argument.setBlock(currentBlock);
+      currentBlock.getInstructions().addLast(argument);
       return argument;
     }
 
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FieldReadBeforeWriteDfsAnalysis.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FieldReadBeforeWriteDfsAnalysis.java
index 1fdfb27..36ca4f0 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FieldReadBeforeWriteDfsAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/FieldReadBeforeWriteDfsAnalysis.java
@@ -23,7 +23,6 @@
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.ir.code.Assume;
 import com.android.tools.r8.ir.code.BasicBlock;
-import com.android.tools.r8.ir.code.BasicBlockInstructionIterator;
 import com.android.tools.r8.ir.code.CheckCast;
 import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.ConstString;
@@ -142,9 +141,8 @@
       }
     }
 
-    AnalysisContinuation applyInstructions(BasicBlockInstructionIterator instructionIterator) {
-      while (instructionIterator.hasNext()) {
-        Instruction instruction = instructionIterator.next();
+    AnalysisContinuation applyInstructions(Instruction instruction) {
+      for (; instruction != null; instruction = instruction.getNext()) {
         assert !instruction.hasOutValue() || !isMaybeInstance(instruction.outValue());
         AnalysisContinuation continuation;
         // TODO(b/339210038): Extend this to many other instructions, such as ConstClass,
@@ -354,11 +352,9 @@
       }
       addBlockToStack(block);
       addInstanceAlias(getNewInstance().outValue());
-      BasicBlockInstructionIterator instructionIterator = block.iterator(uniqueConstructorInvoke);
       // Start the analysis from the invoke-direct instruction. This is important if we can tell
       // that the constructor definitely writes some fields.
-      instructionIterator.previous();
-      return applyInstructions(instructionIterator).toTraversalContinuation();
+      return applyInstructions(uniqueConstructorInvoke).toTraversalContinuation();
     }
   }
 
@@ -379,7 +375,7 @@
       }
       addBlockToStack(block);
       applyPhis(block);
-      AnalysisContinuation continuation = applyInstructions(block.iterator());
+      AnalysisContinuation continuation = applyInstructions(block.entry());
       assert continuation.isAbortOrContinue();
       return TraversalContinuation.breakIf(continuation.isAbort());
     }
diff --git a/src/test/java/com/android/tools/r8/cf/TryRangeTestRunner.java b/src/test/java/com/android/tools/r8/cf/TryRangeTestRunner.java
index 21ee0fc..10abbeb 100644
--- a/src/test/java/com/android/tools/r8/cf/TryRangeTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/TryRangeTestRunner.java
@@ -120,9 +120,7 @@
       add = it.next();
     }
     it.removeInstructionIgnoreOutValue();
-    constNumber.setBlock(tryBlock);
-    add.setBlock(tryBlock);
-    tryBlock.getInstructions().add(0, add);
-    tryBlock.getInstructions().add(0, constNumber);
+    tryBlock.getInstructions().addFirst(add);
+    tryBlock.getInstructions().addFirst(constNumber);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/SplitBlockTest.java b/src/test/java/com/android/tools/r8/ir/SplitBlockTest.java
index 484aca5..dbbec8e 100644
--- a/src/test/java/com/android/tools/r8/ir/SplitBlockTest.java
+++ b/src/test/java/com/android/tools/r8/ir/SplitBlockTest.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionList;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.NumericType;
 import com.android.tools.r8.ir.code.Position;
@@ -97,7 +98,8 @@
 
       assertEquals(argumentInstructions, test.countArgumentInstructions());
       assertEquals(firstBlockInstructions, block.getInstructions().size());
-      assertTrue(!block.getInstructions().get(i).isArgument());
+      InstructionList instructionList = block.getInstructions();
+      assertTrue(!instructionList.getNth(i).isArgument());
 
       InstructionListIterator iterator = test.listIteratorAt(block, i);
       BasicBlock newBlock = iterator.split(code);
@@ -132,7 +134,8 @@
 
       assertEquals(argumentInstructions, test.countArgumentInstructions());
       assertEquals(firstBlockInstructions, block.getInstructions().size());
-      assertTrue(!block.getInstructions().get(i).isArgument());
+      InstructionList instructionList = block.getInstructions();
+      assertTrue(!instructionList.getNth(i).isArgument());
 
       InstructionListIterator iterator = test.listIteratorAt(block, i);
       BasicBlock newBlock = iterator.split(code, 1);
@@ -337,7 +340,8 @@
 
       assertEquals(argumentInstructions, test.countArgumentInstructions());
       assertEquals(firstBlockInstructions, block.getInstructions().size());
-      assertTrue(!block.getInstructions().get(i).isArgument());
+      InstructionList instructionList = block.getInstructions();
+      assertTrue(!instructionList.getNth(i).isArgument());
 
       InstructionListIterator iterator = test.listIteratorAt(block, i);
       BasicBlock newBlock = iterator.split(code);
@@ -459,7 +463,8 @@
 
       assertEquals(argumentInstructions, test.countArgumentInstructions());
       assertEquals(firstBlockInstructions, block.getInstructions().size());
-      assertTrue(!block.getInstructions().get(i).isArgument());
+      InstructionList instructionList = block.getInstructions();
+      assertTrue(!instructionList.getNth(i).isArgument());
 
       InstructionListIterator iterator = test.listIteratorAt(block, i);
       BasicBlock newBlock = iterator.split(code);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/ConstantRemovalTest.java b/src/test/java/com/android/tools/r8/ir/optimize/ConstantRemovalTest.java
index eff9d7a..adc7fb7 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/ConstantRemovalTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/ConstantRemovalTest.java
@@ -87,11 +87,11 @@
     //
     // Then test that peephole optimization realizes that the last const number
     // is needed and the value 10 is *not* still in register 0 at that point.
+    IRMetadata metadata = IRMetadata.unknown();
     final NumberGenerator basicBlockNumberGenerator = new NumberGenerator();
-    BasicBlock block = new BasicBlock();
+    BasicBlock block = new BasicBlock(metadata);
     block.setNumber(basicBlockNumberGenerator.next());
 
-    IRMetadata metadata = IRMetadata.unknown();
     Position position = SyntheticPosition.builder().disableMethodCheck().setLine(0).build();
 
     Value v3 = new Value(3, TypeElement.getLong(), null);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/TrivialGotoEliminationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/TrivialGotoEliminationTest.java
index 997f0e0..275c0e3 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/TrivialGotoEliminationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/TrivialGotoEliminationTest.java
@@ -69,10 +69,10 @@
     //   return
     final NumberGenerator basicBlockNumberGenerator = new NumberGenerator();
     Position position = SyntheticPosition.builder().setLine(0).disableMethodCheck().build();
-    BasicBlock block2 = new BasicBlock();
+    BasicBlock block2 = new BasicBlock(metadata);
     BasicBlock block0 =
         BasicBlock.createGotoBlock(basicBlockNumberGenerator.next(), position, metadata, block2);
-    BasicBlock block1 = new BasicBlock();
+    BasicBlock block1 = new BasicBlock(metadata);
     block1.setNumber(basicBlockNumberGenerator.next());
     block2.setNumber(basicBlockNumberGenerator.next());
     block0.setFilledForTesting();
@@ -133,9 +133,9 @@
     //   goto block3
     final NumberGenerator basicBlockNumberGenerator = new NumberGenerator();
     Position position = SyntheticPosition.builder().setLine(0).disableMethodCheck().build();
-    BasicBlock block0 = new BasicBlock();
+    BasicBlock block0 = new BasicBlock(metadata);
     block0.setNumber(basicBlockNumberGenerator.next());
-    BasicBlock block2 = new BasicBlock();
+    BasicBlock block2 = new BasicBlock(metadata);
     BasicBlock block1 =
         BasicBlock.createGotoBlock(basicBlockNumberGenerator.next(), position, metadata);
     block2.setNumber(basicBlockNumberGenerator.next());
@@ -144,7 +144,7 @@
     block2.add(ret, metadata);
     block2.setFilledForTesting();
 
-    BasicBlock block3 = new BasicBlock();
+    BasicBlock block3 = new BasicBlock(metadata);
     block3.setNumber(basicBlockNumberGenerator.next());
     Instruction instruction = new Goto();
     instruction.setPosition(position);
@@ -196,8 +196,8 @@
             IRMetadata.unknown(),
             MethodConversionOptions.forD8(appView));
     new TrivialGotosCollapser(appView).run(code, Timing.empty());
-    assertTrue(block0.getInstructions().get(1).isIf());
-    assertEquals(block1, block0.getInstructions().get(1).asIf().fallthroughBlock());
+    assertTrue(block0.getInstructions().getFirst().getNext().isIf());
+    assertEquals(block1, block0.getInstructions().getFirst().getNext().asIf().fallthroughBlock());
     assertTrue(blocks.containsAll(ImmutableList.of(block0, block1, block2, block3)));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerPhiDirectUserAfterInlineTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerPhiDirectUserAfterInlineTest.java
index 269b2f9..9ef01cc 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerPhiDirectUserAfterInlineTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerPhiDirectUserAfterInlineTest.java
@@ -169,7 +169,7 @@
       // and block 4 to have:
       // vY : phi(v3, vX)
       BasicBlock basicBlock = irCode.blocks.get(0);
-      Argument argument = basicBlock.getInstructions().get(0).asArgument();
+      Argument argument = basicBlock.getInstructions().getFirst().asArgument();
       assertNotNull(argument);
       Value argumentValue = argument.outValue();
 
@@ -203,7 +203,7 @@
       secondPhi.addOperands(ImmutableList.of(argumentValue, firstPhi));
 
       // Replace the invoke to use the phi
-      InstanceGet instanceGet = block1.getInstructions().get(0).asInstanceGet();
+      InstanceGet instanceGet = block1.getInstructions().getFirst().asInstanceGet();
       assertNotNull(instanceGet);
       assertEquals(A.class.getTypeName(), instanceGet.getField().holder.toSourceString());
       instanceGet.replaceValue(0, firstPhi);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/ifs/IfThrowNullPointerExceptionTest.java b/src/test/java/com/android/tools/r8/ir/optimize/ifs/IfThrowNullPointerExceptionTest.java
index b9c24d6..11fb04d 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/ifs/IfThrowNullPointerExceptionTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/ifs/IfThrowNullPointerExceptionTest.java
@@ -102,8 +102,10 @@
       assertTrue(entryBlock.getInstructions().getFirst().isArgument());
       assertTrue(entryBlock.getInstructions().getLast().isReturn());
 
-      Instruction nullCheckInstruction =
-          entryBlock.getInstructions().get(1 + BooleanUtils.intValue(isNPEWithMessage));
+      Instruction nullCheckInstruction = entryBlock.getInstructions().getFirst().getNext();
+      if (isNPEWithMessage) {
+        nullCheckInstruction = nullCheckInstruction.getNext();
+      }
       assertFalse(isNPEWithMessage);
       assertTrue(nullCheckInstruction.isInvokeVirtual());
       assertEquals(
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
index ed272e7..fab0924 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
@@ -56,7 +56,8 @@
     }
 
     @Override
-    public void replaceCurrentInstruction(Instruction newInstruction, Set<Value> affectedValues) {
+    public void replaceCurrentInstruction(
+        Instruction newInstruction, AffectedValues affectedValues) {
       throw new Unimplemented();
     }
 
@@ -121,7 +122,7 @@
 
     @Override
     public void replaceCurrentInstructionWithStaticGet(
-        AppView<?> appView, IRCode code, DexField field, Set<Value> affectedValues) {
+        AppView<?> appView, IRCode code, DexField field, AffectedValues affectedValues) {
       throw new Unimplemented();
     }
 
diff --git a/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java b/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java
index 4ce22fc..55b0158 100644
--- a/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java
+++ b/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java
@@ -106,8 +106,7 @@
           if (defBlock.canThrow()) {
             // Found the invoke instruction / block.
             assertEquals(2, defBlock.getSuccessors().size());
-            assertTrue(
-                defBlock.getInstructions().get(defBlock.getInstructions().size() - 2).isInvoke());
+            assertTrue(defBlock.getInstructions().getLast().getPrev().isInvoke());
             for (BasicBlock returnPredecessor : block.getPredecessors()) {
               if (defBlock.hasCatchSuccessor(returnPredecessor)) {
                 hasExceptionalPredecessor = true;