Sort moves for suffix sharing in blocks with two predecessors
This reduces the number of moves in JetNews by 0.4%.
Bug: b/376627459
Change-Id: Iae944c8a068a33f4921762f8a8ffd7c3f12952b2
diff --git a/src/main/java/com/android/tools/r8/ir/code/FixedRegisterValue.java b/src/main/java/com/android/tools/r8/ir/code/FixedRegisterValue.java
index c1424b7..8e393fc 100644
--- a/src/main/java/com/android/tools/r8/ir/code/FixedRegisterValue.java
+++ b/src/main/java/com/android/tools/r8/ir/code/FixedRegisterValue.java
@@ -50,6 +50,19 @@
return register;
}
+ public boolean usesRegister(FixedRegisterValue other) {
+ if (register == other.getRegister()) {
+ return true;
+ }
+ if (getType().isWidePrimitive() && register + 1 == other.getRegister()) {
+ return true;
+ }
+ if (other.getType().isWidePrimitive() && register == other.getRegister() + 1) {
+ return true;
+ }
+ return false;
+ }
+
@Override
public boolean isDefinedByInstructionSatisfying(Predicate<Instruction> predicate) {
return false;
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 2773951..7a67608 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -85,6 +85,10 @@
return position != null;
}
+ public boolean hasPrev() {
+ return prev != null;
+ }
+
public Instruction getPrev() {
return prev;
}
@@ -995,6 +999,10 @@
return null;
}
+ public boolean isExit() {
+ return isJumpInstruction();
+ }
+
public boolean isJumpInstruction() {
return false;
}
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 592b6bc..e40f0d7 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
@@ -970,6 +970,8 @@
assert !result.is8Bit() || highestUsedRegister() <= Constants.U8BIT_MAX;
assert !result.is16Bit() || highestUsedRegister() <= Constants.U16BIT_MAX;
+ new MoveSorter(code).sortMovesForSuffixSharing();
+
return result;
}
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/MoveSorter.java b/src/main/java/com/android/tools/r8/ir/regalloc/MoveSorter.java
new file mode 100644
index 0000000..dacade5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/MoveSorter.java
@@ -0,0 +1,275 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.regalloc;
+
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.ConstNumber;
+import com.android.tools.r8.ir.code.FixedRegisterValue;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.Move;
+import com.android.tools.r8.utils.ListUtils;
+import com.google.common.base.Equivalence;
+import com.google.common.base.Equivalence.Wrapper;
+import com.google.common.collect.Iterables;
+import java.util.ArrayDeque;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+public class MoveSorter {
+
+ private static final Equivalence<Instruction> equivalence = new MoveEquivalence();
+
+ private final IRCode code;
+
+ public MoveSorter(IRCode code) {
+ this.code = code;
+ }
+
+ public void sortMovesForSuffixSharing() {
+ for (BasicBlock block : code.blocks(this::hasTwoPredecessorsWithUniqueSuccessor)) {
+ // First compute the set of moves that are in the suffix of both predecessor blocks. These are
+ // the candidates for suffix sharing.
+ Set<Wrapper<Instruction>> suffixSharingCandidates = getSuffixSharingCandidates(block);
+ if (suffixSharingCandidates.isEmpty()) {
+ continue;
+ }
+ // Compute a one-to-many map where an edge x -> y means that the instruction x cannot be moved
+ // past instruction y.
+ Map<Wrapper<Instruction>, Set<Instruction>> blockedBy =
+ computeBlockedBy(block, suffixSharingCandidates);
+ // Compute which instructions can be shared in the successor.
+ Set<Wrapper<Instruction>> sharedSuffix =
+ blockedBy.isEmpty()
+ ? suffixSharingCandidates
+ : computeSharedSuffix(suffixSharingCandidates, blockedBy);
+ if (!sharedSuffix.isEmpty()) {
+ for (BasicBlock predecessor : block.getPredecessors()) {
+ sortSuffix(predecessor, sharedSuffix);
+ }
+ }
+ }
+ }
+
+ Set<Wrapper<Instruction>> getSuffixSharingCandidates(BasicBlock block) {
+ Set<Wrapper<Instruction>> firstSuffix = new HashSet<>();
+ for (Instruction instruction = block.getPredecessor(0).exit().getPrev();
+ instruction != null && isMove(instruction);
+ instruction = instruction.getPrev()) {
+ firstSuffix.add(equivalence.wrap(instruction));
+ }
+ if (firstSuffix.isEmpty()) {
+ return firstSuffix;
+ }
+ Set<Wrapper<Instruction>> suffixSharingCandidates = new HashSet<>();
+ for (Instruction instruction = block.getPredecessor(1).exit().getPrev();
+ instruction != null && isMove(instruction);
+ instruction = instruction.getPrev()) {
+ Wrapper<Instruction> wrapper = equivalence.wrap(instruction);
+ if (firstSuffix.contains(wrapper)) {
+ suffixSharingCandidates.add(wrapper);
+ }
+ }
+ return suffixSharingCandidates;
+ }
+
+ Map<Wrapper<Instruction>, Set<Instruction>> computeBlockedBy(
+ BasicBlock block, Set<Wrapper<Instruction>> suffixSharingCandidates) {
+ Map<Wrapper<Instruction>, Set<Instruction>> blockedBy = new HashMap<>();
+ for (BasicBlock predecessor : block.getPredecessors()) {
+ addBlockedBy(predecessor, suffixSharingCandidates, blockedBy);
+ }
+ return blockedBy;
+ }
+
+ void addBlockedBy(
+ BasicBlock block,
+ Set<Wrapper<Instruction>> suffixSharingCandidates,
+ Map<Wrapper<Instruction>, Set<Instruction>> blockedBy) {
+ // Find the first move.
+ Instruction instruction = block.exit().getPrev();
+ assert instruction != null;
+ assert isMove(instruction);
+ while (instruction.hasPrev() && isMove(instruction.getPrev())) {
+ instruction = instruction.getPrev();
+ }
+
+ List<Instruction> seen = ListUtils.newArrayList(instruction);
+ for (instruction = instruction.getNext();
+ !instruction.isExit();
+ instruction = instruction.getNext()) {
+ // Record if the current instruction is blocking any of the previously seen instructions.
+ assert isMove(instruction);
+ for (Instruction previous : seen) {
+ if (isBlockedBy(previous, instruction)) {
+ blockedBy
+ .computeIfAbsent(equivalence.wrap(previous), ignoreKey(HashSet::new))
+ .add(instruction);
+ }
+ }
+ // Only add the current instruction to the seen set if it is a candidate for suffix sharing.
+ // Otherwise, we can't move it to the successor so we don't need worry about whether it is
+ // blocked.
+ if (suffixSharingCandidates.contains(equivalence.wrap(instruction))) {
+ seen.add(instruction);
+ }
+ }
+ }
+
+ Set<Wrapper<Instruction>> computeSharedSuffix(
+ Set<Wrapper<Instruction>> suffixSharingCandidates,
+ Map<Wrapper<Instruction>, Set<Instruction>> blockedBy) {
+ Set<Wrapper<Instruction>> sharedSuffix = new HashSet<>();
+ while (true) {
+ Wrapper<Instruction> unblockedMove =
+ removeUnblockedMove(suffixSharingCandidates, blockedBy, sharedSuffix);
+ if (unblockedMove == null) {
+ break;
+ }
+ sharedSuffix.add(unblockedMove);
+ }
+ return sharedSuffix;
+ }
+
+ Wrapper<Instruction> removeUnblockedMove(
+ Set<Wrapper<Instruction>> suffixSharingCandidates,
+ Map<Wrapper<Instruction>, Set<Instruction>> blockedBy,
+ Set<Wrapper<Instruction>> sharedSuffix) {
+ Iterator<Wrapper<Instruction>> iterator = suffixSharingCandidates.iterator();
+ while (iterator.hasNext()) {
+ Wrapper<Instruction> candidate = iterator.next();
+ Set<Instruction> blockingSet = blockedBy.getOrDefault(candidate, Collections.emptySet());
+ // Disregard blocking moves that have already been moved to the successor.
+ blockingSet.removeIf(blockingMove -> sharedSuffix.contains(equivalence.wrap(blockingMove)));
+ if (blockingSet.isEmpty()) {
+ iterator.remove();
+ return candidate;
+ }
+ }
+ return null;
+ }
+
+ void sortSuffix(BasicBlock predecessor, Set<Wrapper<Instruction>> sharedSuffix) {
+ InstructionListIterator iterator =
+ predecessor.listIterator(code, predecessor.getInstructions().size() - 1);
+ Deque<Instruction> removedInstructions = new ArrayDeque<>();
+ while (iterator.hasPrevious()) {
+ Instruction instruction = iterator.previous();
+ if (isMove(instruction)) {
+ if (sharedSuffix.contains(equivalence.wrap(instruction))) {
+ removedInstructions.addFirst(instruction);
+ iterator.removeInstructionIgnoreOutValue();
+ }
+ } else {
+ break;
+ }
+ }
+ assert removedInstructions.size() == sharedSuffix.size();
+ predecessor
+ .listIterator(code, predecessor.getInstructions().size() - 1)
+ .addAll(removedInstructions);
+ }
+
+ private boolean hasTwoPredecessorsWithUniqueSuccessor(BasicBlock block) {
+ return block.getPredecessors().size() == 2
+ && Iterables.all(
+ block.getPredecessors(),
+ predecessor -> predecessor.hasUniqueSuccessor() && predecessor.exit().isGoto());
+ }
+
+ private boolean isBlockedBy(Instruction instruction, Instruction laterInstruction) {
+ // Check if either of the two instructions write the operand of the other instruction.
+ if (instruction.isMove()) {
+ FixedRegisterValue inValue = instruction.getFirstOperand().asFixedRegisterValue();
+ FixedRegisterValue laterOutValue = laterInstruction.outValue().asFixedRegisterValue();
+ if (laterOutValue.usesRegister(inValue)) {
+ return true;
+ }
+ }
+ if (laterInstruction.isMove()) {
+ FixedRegisterValue outValue = instruction.outValue().asFixedRegisterValue();
+ FixedRegisterValue laterInValue = laterInstruction.getFirstOperand().asFixedRegisterValue();
+ if (outValue.usesRegister(laterInValue)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean isMove(Instruction instruction) {
+ if (instruction.isConstNumber()) {
+ return instruction.outValue().isFixedRegisterValue();
+ }
+ if (instruction.isMove() && !instruction.isDebugLocalWrite()) {
+ if (instruction.getFirstOperand().isFixedRegisterValue()) {
+ assert instruction.outValue().isFixedRegisterValue();
+ return true;
+ }
+ // This Move instruction has been inserted for dealing with invoke/range. It is not inserted
+ // by the move scheduler.
+ }
+ return false;
+ }
+
+ private static class MoveEquivalence extends Equivalence<Instruction> {
+
+ @Override
+ protected boolean doEquivalent(Instruction a, Instruction b) {
+ if (a.isConstNumber()) {
+ if (!b.isConstNumber()) {
+ return false;
+ }
+ ConstNumber constNumber = a.asConstNumber();
+ ConstNumber other = b.asConstNumber();
+ return constNumber.getOutType().equals(other.getOutType())
+ && constNumber.getRawValue() == other.getRawValue()
+ && constNumber.outValue().asFixedRegisterValue().getRegister()
+ == other.outValue().asFixedRegisterValue().getRegister();
+ } else {
+ assert a.isMove();
+ if (!b.isMove()) {
+ return false;
+ }
+ Move move = a.asMove();
+ Move other = b.asMove();
+ return move.src()
+ .asFixedRegisterValue()
+ .outType()
+ .equals(other.src().asFixedRegisterValue().outType())
+ && move.src().asFixedRegisterValue().getRegister()
+ == other.src().asFixedRegisterValue().getRegister()
+ && move.outValue().asFixedRegisterValue().getRegister()
+ == other.outValue().asFixedRegisterValue().getRegister();
+ }
+ }
+
+ @Override
+ protected int doHash(Instruction instruction) {
+ if (instruction.isConstNumber()) {
+ ConstNumber constNumber = instruction.asConstNumber();
+ return Objects.hash(
+ constNumber.getClass(),
+ constNumber.outValue().asFixedRegisterValue().getRegister(),
+ constNumber.getRawValue());
+ } else {
+ assert instruction.isMove();
+ Move move = instruction.asMove();
+ return Objects.hash(
+ move.getClass(),
+ move.outValue().asFixedRegisterValue().getRegister(),
+ move.src().asFixedRegisterValue().getRegister());
+ }
+ }
+ }
+}