Files
regalloc2/src/ion/moves.rs
Chris Fallin 9733cb2227 Clobbers: use a more efficient bitmask representation in API. (#58)
* Clobbers: use a more efficient bitmask representation in API.

Currently, the `Function` trait requires a `&[PReg]` for the
clobber-list for a given instruction. In most cases where clobbers are
used, the list may be long: e.g., ABIs specify a fixed set of registers
that are clobbered and there may be ~half of all registers in this list.
What's more, the list can't be shared for e.g. all calls of a given ABI,
because actual return-values (defs) can't be clobbers. So we need to
allocate space for long, sometimes-slightly-different lists; this is
inefficient for the embedder and for us.

It's much more efficient to use a bitmask to represent a set of physical
registers. With current data structure bitpacking limitations, we can
support at most 128 physical registers; this means we can use a `u128`
bitmask. This also allows e.g. an embedder to start with a constant for
a given ABI, and mask out bits for actual return-value registers on call
instructions.

This PR makes that change, for minor but positive performance impact.

* Review comments.
2022-06-27 12:27:19 -07:00

1126 lines
49 KiB
Rust

/*
* This file was initially derived from the files
* `js/src/jit/BacktrackingAllocator.h` and
* `js/src/jit/BacktrackingAllocator.cpp` in Mozilla Firefox, and was
* originally licensed under the Mozilla Public License 2.0. We
* subsequently relicensed it to Apache-2.0 WITH LLVM-exception (see
* https://github.com/bytecodealliance/regalloc2/issues/7).
*
* Since the initial port, the design has been substantially evolved
* and optimized.
*/
//! Move resolution.
use super::{
Env, InsertMovePrio, InsertedMove, LiveRangeFlag, LiveRangeIndex, RedundantMoveEliminator,
VRegIndex, SLOT_NONE,
};
use crate::ion::data_structures::{
BlockparamIn, BlockparamOut, CodeRange, FixedRegFixupLevel, LiveRangeKey, PosWithPrio,
};
use crate::ion::reg_traversal::RegTraversalIter;
use crate::moves::{MoveAndScratchResolver, ParallelMoves};
use crate::{
Allocation, Block, Edit, Function, Inst, InstPosition, OperandConstraint, OperandKind,
OperandPos, PReg, ProgPoint, RegClass, SpillSlot, VReg,
};
use fxhash::FxHashMap;
use smallvec::{smallvec, SmallVec};
use std::fmt::Debug;
impl<'a, F: Function> Env<'a, F> {
pub fn is_start_of_block(&self, pos: ProgPoint) -> bool {
let block = self.cfginfo.insn_block[pos.inst().index()];
pos == self.cfginfo.block_entry[block.index()]
}
pub fn is_end_of_block(&self, pos: ProgPoint) -> bool {
let block = self.cfginfo.insn_block[pos.inst().index()];
pos == self.cfginfo.block_exit[block.index()]
}
fn allocation_is_stack(&self, alloc: Allocation) -> bool {
if alloc.is_stack() {
true
} else if let Some(preg) = alloc.as_reg() {
self.pregs[preg.index()].is_stack
} else {
false
}
}
pub fn insert_move(
&mut self,
pos: ProgPoint,
prio: InsertMovePrio,
from_alloc: Allocation,
to_alloc: Allocation,
to_vreg: Option<VReg>,
) {
trace!(
"insert_move: pos {:?} prio {:?} from_alloc {:?} to_alloc {:?}",
pos,
prio,
from_alloc,
to_alloc
);
match (from_alloc.as_reg(), to_alloc.as_reg()) {
(Some(from), Some(to)) => {
debug_assert_eq!(from.class(), to.class());
}
_ => {}
}
self.inserted_moves.push(InsertedMove {
pos_prio: PosWithPrio {
pos,
prio: prio as u32,
},
from_alloc,
to_alloc,
to_vreg,
});
}
pub fn get_alloc(&self, inst: Inst, slot: usize) -> Allocation {
let inst_allocs = &self.allocs[self.inst_alloc_offsets[inst.index()] as usize..];
inst_allocs[slot]
}
pub fn set_alloc(&mut self, inst: Inst, slot: usize, alloc: Allocation) {
let inst_allocs = &mut self.allocs[self.inst_alloc_offsets[inst.index()] as usize..];
inst_allocs[slot] = alloc;
}
pub fn get_alloc_for_range(&self, range: LiveRangeIndex) -> Allocation {
trace!("get_alloc_for_range: {:?}", range);
let bundle = self.ranges[range.index()].bundle;
trace!(" -> bundle: {:?}", bundle);
let bundledata = &self.bundles[bundle.index()];
trace!(" -> allocation {:?}", bundledata.allocation);
if bundledata.allocation != Allocation::none() {
bundledata.allocation
} else {
trace!(" -> spillset {:?}", bundledata.spillset);
trace!(
" -> spill slot {:?}",
self.spillsets[bundledata.spillset.index()].slot
);
self.spillslots[self.spillsets[bundledata.spillset.index()].slot.index()].alloc
}
}
pub fn apply_allocations_and_insert_moves(&mut self) {
trace!("apply_allocations_and_insert_moves");
trace!("blockparam_ins: {:?}", self.blockparam_ins);
trace!("blockparam_outs: {:?}", self.blockparam_outs);
// Now that all splits are done, we can pay the cost once to
// sort VReg range lists and update with the final ranges.
for vreg in &mut self.vregs {
for entry in &mut vreg.ranges {
entry.range = self.ranges[entry.index.index()].range;
}
vreg.ranges.sort_unstable_by_key(|entry| entry.range.from);
}
/// We create "half-moves" in order to allow a single-scan
/// strategy with a subsequent sort. Basically, the key idea
/// is that as our single scan through a range for a vreg hits
/// upon the source or destination of an edge-move, we emit a
/// "half-move". These half-moves are carefully keyed in a
/// particular sort order (the field order below is
/// significant!) so that all half-moves on a given (from, to)
/// block-edge appear contiguously, and then all moves from a
/// given vreg appear contiguously. Within a given from-vreg,
/// pick the first `Source` (there should only be one, but
/// imprecision in liveranges due to loop handling sometimes
/// means that a blockparam-out is also recognized as a normal-out),
/// and then for each `Dest`, copy the source-alloc to that
/// dest-alloc.
#[derive(Clone, Debug, PartialEq, Eq)]
struct HalfMove {
key: u64,
alloc: Allocation,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
enum HalfMoveKind {
Source = 0,
Dest = 1,
}
fn half_move_key(
from_block: Block,
to_block: Block,
to_vreg: VRegIndex,
kind: HalfMoveKind,
) -> u64 {
debug_assert!(from_block.index() < 1 << 21);
debug_assert!(to_block.index() < 1 << 21);
debug_assert!(to_vreg.index() < 1 << 21);
((from_block.index() as u64) << 43)
| ((to_block.index() as u64) << 22)
| ((to_vreg.index() as u64) << 1)
| (kind as u8 as u64)
}
impl HalfMove {
fn from_block(&self) -> Block {
Block::new(((self.key >> 43) & ((1 << 21) - 1)) as usize)
}
fn to_block(&self) -> Block {
Block::new(((self.key >> 22) & ((1 << 21) - 1)) as usize)
}
fn to_vreg(&self) -> VRegIndex {
VRegIndex::new(((self.key >> 1) & ((1 << 21) - 1)) as usize)
}
fn kind(&self) -> HalfMoveKind {
if self.key & 1 == 1 {
HalfMoveKind::Dest
} else {
HalfMoveKind::Source
}
}
}
let debug_labels = self.func.debug_value_labels();
let mut half_moves: Vec<HalfMove> = Vec::with_capacity(6 * self.func.num_insts());
let mut reuse_input_insts = Vec::with_capacity(self.func.num_insts() / 2);
let mut blockparam_in_idx = 0;
let mut blockparam_out_idx = 0;
let mut prog_move_src_idx = 0;
let mut prog_move_dst_idx = 0;
for vreg in 0..self.vregs.len() {
let vreg = VRegIndex::new(vreg);
if !self.is_vreg_used(vreg) {
continue;
}
let pinned_alloc = self.func.is_pinned_vreg(self.vreg(vreg));
// For each range in each vreg, insert moves or
// half-moves. We also scan over `blockparam_ins` and
// `blockparam_outs`, which are sorted by (block, vreg),
// and over program-move srcs/dsts to fill in allocations.
let mut prev = LiveRangeIndex::invalid();
for range_idx in 0..self.vregs[vreg.index()].ranges.len() {
let entry = self.vregs[vreg.index()].ranges[range_idx];
let alloc = pinned_alloc
.map(|preg| Allocation::reg(preg))
.unwrap_or_else(|| self.get_alloc_for_range(entry.index));
let range = entry.range;
trace!(
"apply_allocations: vreg {:?} LR {:?} with range {:?} has alloc {:?} (pinned {:?})",
vreg,
entry.index,
range,
alloc,
pinned_alloc,
);
debug_assert!(alloc != Allocation::none());
if self.annotations_enabled {
self.annotate(
range.from,
format!(
" <<< start v{} in {} (range{}) (bundle{})",
vreg.index(),
alloc,
entry.index.index(),
self.ranges[entry.index.index()].bundle.raw_u32(),
),
);
self.annotate(
range.to,
format!(
" end v{} in {} (range{}) (bundle{}) >>>",
vreg.index(),
alloc,
entry.index.index(),
self.ranges[entry.index.index()].bundle.raw_u32(),
),
);
}
// Does this range follow immediately after a prior
// range in the same block? If so, insert a move (if
// the allocs differ). We do this directly rather than
// with half-moves because we eagerly know both sides
// already (and also, half-moves are specific to
// inter-block transfers).
//
// Note that we do *not* do this if there is also a
// def as the first use in the new range: it's
// possible that an old liverange covers the Before
// pos of an inst, a new liverange covers the After
// pos, and the def also happens at After. In this
// case we don't want to an insert a move after the
// instruction copying the old liverange.
//
// Note also that we assert that the new range has to
// start at the Before-point of an instruction; we
// can't insert a move that logically happens just
// before After (i.e. in the middle of a single
// instruction).
//
// Also note that this case is not applicable to
// pinned vregs (because they are always in one PReg).
if pinned_alloc.is_none() && prev.is_valid() {
let prev_alloc = self.get_alloc_for_range(prev);
let prev_range = self.ranges[prev.index()].range;
let first_is_def =
self.ranges[entry.index.index()].has_flag(LiveRangeFlag::StartsAtDef);
debug_assert!(prev_alloc != Allocation::none());
if prev_range.to == range.from
&& !self.is_start_of_block(range.from)
&& !first_is_def
{
trace!(
"prev LR {} abuts LR {} in same block; moving {} -> {} for v{}",
prev.index(),
entry.index.index(),
prev_alloc,
alloc,
vreg.index()
);
debug_assert_eq!(range.from.pos(), InstPosition::Before);
self.insert_move(
range.from,
InsertMovePrio::Regular,
prev_alloc,
alloc,
Some(self.vreg(vreg)),
);
}
}
// The block-to-block edge-move logic is not
// applicable to pinned vregs, which are always in one
// PReg (so never need moves within their own vreg
// ranges).
if pinned_alloc.is_none() {
// Scan over blocks whose ends are covered by this
// range. For each, for each successor that is not
// already in this range (hence guaranteed to have the
// same allocation) and if the vreg is live, add a
// Source half-move.
let mut block = self.cfginfo.insn_block[range.from.inst().index()];
while block.is_valid() && block.index() < self.func.num_blocks() {
if range.to < self.cfginfo.block_exit[block.index()].next() {
break;
}
trace!("examining block with end in range: block{}", block.index());
for &succ in self.func.block_succs(block) {
trace!(
" -> has succ block {} with entry {:?}",
succ.index(),
self.cfginfo.block_entry[succ.index()]
);
if range.contains_point(self.cfginfo.block_entry[succ.index()]) {
continue;
}
trace!(" -> out of this range, requires half-move if live");
if self.is_live_in(succ, vreg) {
trace!(" -> live at input to succ, adding halfmove");
half_moves.push(HalfMove {
key: half_move_key(block, succ, vreg, HalfMoveKind::Source),
alloc,
});
}
}
// Scan forward in `blockparam_outs`, adding all
// half-moves for outgoing values to blockparams
// in succs.
trace!(
"scanning blockparam_outs for v{} block{}: blockparam_out_idx = {}",
vreg.index(),
block.index(),
blockparam_out_idx,
);
while blockparam_out_idx < self.blockparam_outs.len() {
let BlockparamOut {
from_vreg,
from_block,
to_block,
to_vreg,
} = self.blockparam_outs[blockparam_out_idx];
if (from_vreg, from_block) > (vreg, block) {
break;
}
if (from_vreg, from_block) == (vreg, block) {
trace!(
" -> found: from v{} block{} to v{} block{}",
from_vreg.index(),
from_block.index(),
to_vreg.index(),
to_vreg.index()
);
half_moves.push(HalfMove {
key: half_move_key(
from_block,
to_block,
to_vreg,
HalfMoveKind::Source,
),
alloc,
});
if self.annotations_enabled {
self.annotate(
self.cfginfo.block_exit[block.index()],
format!(
"blockparam-out: block{} to block{}: v{} to v{} in {}",
from_block.index(),
to_block.index(),
from_vreg.index(),
to_vreg.index(),
alloc
),
);
}
}
blockparam_out_idx += 1;
}
block = block.next();
}
// Scan over blocks whose beginnings are covered by
// this range and for which the vreg is live at the
// start of the block. For each, for each predecessor,
// add a Dest half-move.
let mut block = self.cfginfo.insn_block[range.from.inst().index()];
if self.cfginfo.block_entry[block.index()] < range.from {
block = block.next();
}
while block.is_valid() && block.index() < self.func.num_blocks() {
if self.cfginfo.block_entry[block.index()] >= range.to {
break;
}
// Add half-moves for blockparam inputs.
trace!(
"scanning blockparam_ins at vreg {} block {}: blockparam_in_idx = {}",
vreg.index(),
block.index(),
blockparam_in_idx
);
while blockparam_in_idx < self.blockparam_ins.len() {
let BlockparamIn {
from_block,
to_block,
to_vreg,
} = self.blockparam_ins[blockparam_in_idx];
if (to_vreg, to_block) > (vreg, block) {
break;
}
if (to_vreg, to_block) == (vreg, block) {
half_moves.push(HalfMove {
key: half_move_key(
from_block,
to_block,
to_vreg,
HalfMoveKind::Dest,
),
alloc,
});
trace!(
"match: blockparam_in: v{} in block{} from block{} into {}",
to_vreg.index(),
to_block.index(),
from_block.index(),
alloc,
);
#[cfg(debug_assertions)]
{
if log::log_enabled!(log::Level::Trace) {
self.annotate(
self.cfginfo.block_entry[block.index()],
format!(
"blockparam-in: block{} to block{}:into v{} in {}",
from_block.index(),
to_block.index(),
to_vreg.index(),
alloc
),
);
}
}
}
blockparam_in_idx += 1;
}
if !self.is_live_in(block, vreg) {
block = block.next();
continue;
}
trace!(
"scanning preds at vreg {} block {} for ends outside the range",
vreg.index(),
block.index()
);
// Now find any preds whose ends are not in the
// same range, and insert appropriate moves.
for &pred in self.func.block_preds(block) {
trace!(
"pred block {} has exit {:?}",
pred.index(),
self.cfginfo.block_exit[pred.index()]
);
if range.contains_point(self.cfginfo.block_exit[pred.index()]) {
continue;
}
trace!(" -> requires half-move");
half_moves.push(HalfMove {
key: half_move_key(pred, block, vreg, HalfMoveKind::Dest),
alloc,
});
}
block = block.next();
}
}
// Scan over def/uses and apply allocations.
for use_idx in 0..self.ranges[entry.index.index()].uses.len() {
let usedata = self.ranges[entry.index.index()].uses[use_idx];
trace!("applying to use: {:?}", usedata);
debug_assert!(range.contains_point(usedata.pos));
let inst = usedata.pos.inst();
let slot = usedata.slot;
let operand = usedata.operand;
// Safepoints add virtual uses with no slots;
// avoid these.
if slot != SLOT_NONE {
self.set_alloc(inst, slot as usize, alloc);
}
if let OperandConstraint::Reuse(_) = operand.constraint() {
reuse_input_insts.push(inst);
}
}
// Scan debug-labels on this vreg that overlap with
// this range, producing a debug-info output record
// giving the allocation location for each label.
if !debug_labels.is_empty() {
// Do a binary search to find the start of any
// labels for this vreg. Recall that we require
// debug-label requests to be sorted by vreg as a
// precondition (which we verified above).
let start = debug_labels
.binary_search_by(|&(label_vreg, _label_from, _label_to, _label)| {
// Search for the point just before the first
// tuple that could be for `vreg` overlapping
// with `range`. Never return
// `Ordering::Equal`; `binary_search_by` in
// this case returns the index of the first
// entry that is greater as an `Err`.
if label_vreg.vreg() < vreg.index() {
std::cmp::Ordering::Less
} else {
std::cmp::Ordering::Greater
}
})
.unwrap_err();
for &(label_vreg, label_from, label_to, label) in &debug_labels[start..] {
let label_from = ProgPoint::before(label_from);
let label_to = ProgPoint::before(label_to);
let label_range = CodeRange {
from: label_from,
to: label_to,
};
if label_vreg.vreg() != vreg.index() {
break;
}
if !range.overlaps(&label_range) {
continue;
}
let from = std::cmp::max(label_from, range.from);
let to = std::cmp::min(label_to, range.to);
self.debug_locations.push((label, from, to, alloc));
}
}
// Scan over program move srcs/dsts to fill in allocations.
// Move srcs happen at `After` of a given
// inst. Compute [from, to) semi-inclusive range of
// inst indices for which we should fill in the source
// with this LR's allocation.
//
// range from inst-Before or inst-After covers cur
// inst's After; so includes move srcs from inst.
let move_src_start = (vreg, range.from.inst());
// range to (exclusive) inst-Before or inst-After
// covers only prev inst's After; so includes move
// srcs to (exclusive) inst.
let move_src_end = (vreg, range.to.inst());
trace!(
"vreg {:?} range {:?}: looking for program-move sources from {:?} to {:?}",
vreg,
range,
move_src_start,
move_src_end
);
while prog_move_src_idx < self.prog_move_srcs.len()
&& self.prog_move_srcs[prog_move_src_idx].0 < move_src_start
{
trace!(" -> skipping idx {}", prog_move_src_idx);
prog_move_src_idx += 1;
}
while prog_move_src_idx < self.prog_move_srcs.len()
&& self.prog_move_srcs[prog_move_src_idx].0 < move_src_end
{
trace!(
" -> setting idx {} ({:?}) to alloc {:?}",
prog_move_src_idx,
self.prog_move_srcs[prog_move_src_idx].0,
alloc
);
self.prog_move_srcs[prog_move_src_idx].1 = alloc;
prog_move_src_idx += 1;
}
// move dsts happen at Before point.
//
// Range from inst-Before includes cur inst, while inst-After includes only next inst.
let move_dst_start = if range.from.pos() == InstPosition::Before {
(vreg, range.from.inst())
} else {
(vreg, range.from.inst().next())
};
// Range to (exclusive) inst-Before includes prev
// inst, so to (exclusive) cur inst; range to
// (exclusive) inst-After includes cur inst, so to
// (exclusive) next inst.
let move_dst_end = if range.to.pos() == InstPosition::Before {
(vreg, range.to.inst())
} else {
(vreg, range.to.inst().next())
};
trace!(
"vreg {:?} range {:?}: looking for program-move dests from {:?} to {:?}",
vreg,
range,
move_dst_start,
move_dst_end
);
while prog_move_dst_idx < self.prog_move_dsts.len()
&& self.prog_move_dsts[prog_move_dst_idx].0 < move_dst_start
{
trace!(" -> skipping idx {}", prog_move_dst_idx);
prog_move_dst_idx += 1;
}
while prog_move_dst_idx < self.prog_move_dsts.len()
&& self.prog_move_dsts[prog_move_dst_idx].0 < move_dst_end
{
trace!(
" -> setting idx {} ({:?}) to alloc {:?}",
prog_move_dst_idx,
self.prog_move_dsts[prog_move_dst_idx].0,
alloc
);
self.prog_move_dsts[prog_move_dst_idx].1 = alloc;
prog_move_dst_idx += 1;
}
prev = entry.index;
}
}
// Sort the half-moves list. For each (from, to,
// from-vreg) tuple, find the from-alloc and all the
// to-allocs, and insert moves on the block edge.
half_moves.sort_unstable_by_key(|h| h.key);
trace!("halfmoves: {:?}", half_moves);
self.stats.halfmoves_count = half_moves.len();
let mut i = 0;
while i < half_moves.len() {
// Find a Source.
while i < half_moves.len() && half_moves[i].kind() != HalfMoveKind::Source {
i += 1;
}
if i >= half_moves.len() {
break;
}
let src = &half_moves[i];
i += 1;
// Find all Dests.
let dest_key = src.key | 1;
let first_dest = i;
while i < half_moves.len() && half_moves[i].key == dest_key {
i += 1;
}
let last_dest = i;
trace!(
"halfmove match: src {:?} dests {:?}",
src,
&half_moves[first_dest..last_dest]
);
// Determine the ProgPoint where moves on this (from, to)
// edge should go:
// - If there is more than one in-edge to `to`, then
// `from` must have only one out-edge; moves go at tail of
// `from` just before last Branch/Ret.
// - Otherwise, there must be at most one in-edge to `to`,
// and moves go at start of `to`.
let from_last_insn = self.func.block_insns(src.from_block()).last();
let to_first_insn = self.func.block_insns(src.to_block()).first();
let from_is_ret = self.func.is_ret(from_last_insn);
let to_is_entry = self.func.entry_block() == src.to_block();
let from_outs =
self.func.block_succs(src.from_block()).len() + if from_is_ret { 1 } else { 0 };
let to_ins =
self.func.block_preds(src.to_block()).len() + if to_is_entry { 1 } else { 0 };
let (insertion_point, prio) = if to_ins > 1 && from_outs <= 1 {
(
// N.B.: though semantically the edge moves happen
// after the branch, we must insert them before
// the branch because otherwise, of course, they
// would never execute. This is correct even in
// the presence of branches that read register
// inputs (e.g. conditional branches on some RISCs
// that branch on reg zero/not-zero, or any
// indirect branch), but for a very subtle reason:
// all cases of such branches will (or should)
// have multiple successors, and thus due to
// critical-edge splitting, their successors will
// have only the single predecessor, and we prefer
// to insert at the head of the successor in that
// case (rather than here). We make this a
// requirement, in fact: the user of this library
// shall not read registers in a branch
// instruction of there is only one successor per
// the given CFG information.
ProgPoint::before(from_last_insn),
InsertMovePrio::OutEdgeMoves,
)
} else if to_ins <= 1 {
(
ProgPoint::before(to_first_insn),
InsertMovePrio::InEdgeMoves,
)
} else {
panic!(
"Critical edge: can't insert moves between blocks {:?} and {:?}",
src.from_block(),
src.to_block()
);
};
let mut last = None;
for dest in first_dest..last_dest {
let dest = &half_moves[dest];
if last == Some(dest.alloc) {
continue;
}
self.insert_move(
insertion_point,
prio,
src.alloc,
dest.alloc,
Some(self.vreg(dest.to_vreg())),
);
last = Some(dest.alloc);
}
}
// Handle multi-fixed-reg constraints by copying.
for fixup in std::mem::replace(&mut self.multi_fixed_reg_fixups, vec![]) {
let from_alloc = self.get_alloc(fixup.pos.inst(), fixup.from_slot as usize);
let to_alloc = Allocation::reg(PReg::from_index(fixup.to_preg.index()));
trace!(
"multi-fixed-move constraint at {:?} from {} to {} for v{}",
fixup.pos,
from_alloc,
to_alloc,
fixup.vreg.index(),
);
let prio = match fixup.level {
FixedRegFixupLevel::Initial => InsertMovePrio::MultiFixedRegInitial,
FixedRegFixupLevel::Secondary => InsertMovePrio::MultiFixedRegSecondary,
};
self.insert_move(
fixup.pos,
prio,
from_alloc,
to_alloc,
Some(self.vreg(fixup.vreg)),
);
self.set_alloc(
fixup.pos.inst(),
fixup.to_slot as usize,
Allocation::reg(PReg::from_index(fixup.to_preg.index())),
);
}
// Handle outputs that reuse inputs: copy beforehand, then set
// input's alloc to output's.
//
// Note that the output's allocation may not *actually* be
// valid until InstPosition::After, but the reused input may
// occur at InstPosition::Before. This may appear incorrect,
// but we make it work by ensuring that all *other* inputs are
// extended to InstPosition::After so that the def will not
// interfere. (The liveness computation code does this -- we
// do not require the user to do so.)
//
// One might ask: why not insist that input-reusing defs occur
// at InstPosition::Before? this would be correct, but would
// mean that the reused input and the reusing output
// interfere, *guaranteeing* that every such case would
// require a move. This is really bad on ISAs (like x86) where
// reused inputs are ubiquitous.
//
// Another approach might be to put the def at Before, and
// trim the reused input's liverange back to the previous
// instruction's After. This is kind of OK until (i) a block
// boundary occurs between the prior inst and this one, or
// (ii) any moves/spills/reloads occur between the two
// instructions. We really do need the input to be live at
// this inst's Before.
//
// In principle what we really need is a "BeforeBefore"
// program point, but we don't want to introduce that
// everywhere and pay the cost of twice as many ProgPoints
// throughout the allocator.
//
// Or we could introduce a separate move instruction -- this
// is the approach that regalloc.rs takes with "mod" operands
// -- but that is also costly.
//
// So we take this approach (invented by IonMonkey -- somewhat
// hard to discern, though see [0] for a comment that makes
// this slightly less unclear) to avoid interference between
// the actual reused input and reusing output, ensure
// interference (hence no incorrectness) between other inputs
// and the reusing output, and not require a separate explicit
// move instruction.
//
// [0] https://searchfox.org/mozilla-central/rev/3a798ef9252896fb389679f06dd3203169565af0/js/src/jit/shared/Lowering-shared-inl.h#108-110
for inst in reuse_input_insts {
let mut input_reused: SmallVec<[usize; 4]> = smallvec![];
for output_idx in 0..self.func.inst_operands(inst).len() {
let operand = self.func.inst_operands(inst)[output_idx];
if let OperandConstraint::Reuse(input_idx) = operand.constraint() {
debug_assert!(!input_reused.contains(&input_idx));
debug_assert_eq!(operand.pos(), OperandPos::Late);
input_reused.push(input_idx);
let input_alloc = self.get_alloc(inst, input_idx);
let output_alloc = self.get_alloc(inst, output_idx);
trace!(
"reuse-input inst {:?}: output {} has alloc {:?}, input {} has alloc {:?}",
inst,
output_idx,
output_alloc,
input_idx,
input_alloc
);
if input_alloc != output_alloc {
#[cfg(debug_assertions)]
{
if log::log_enabled!(log::Level::Trace) {
self.annotate(
ProgPoint::before(inst),
format!(
" reuse-input-copy: {} -> {}",
input_alloc, output_alloc
),
);
}
}
let input_operand = self.func.inst_operands(inst)[input_idx];
self.insert_move(
ProgPoint::before(inst),
InsertMovePrio::ReusedInput,
input_alloc,
output_alloc,
Some(input_operand.vreg()),
);
self.set_alloc(inst, input_idx, output_alloc);
}
}
}
}
// Sort the prog-moves lists and insert moves to reify the
// input program's move operations.
self.prog_move_srcs
.sort_unstable_by_key(|((_, inst), _)| *inst);
self.prog_move_dsts
.sort_unstable_by_key(|((_, inst), _)| inst.prev());
let prog_move_srcs = std::mem::replace(&mut self.prog_move_srcs, vec![]);
let prog_move_dsts = std::mem::replace(&mut self.prog_move_dsts, vec![]);
debug_assert_eq!(prog_move_srcs.len(), prog_move_dsts.len());
for (&((_, from_inst), from_alloc), &((to_vreg, to_inst), to_alloc)) in
prog_move_srcs.iter().zip(prog_move_dsts.iter())
{
trace!(
"program move at inst {:?}: alloc {:?} -> {:?} (v{})",
from_inst,
from_alloc,
to_alloc,
to_vreg.index(),
);
debug_assert!(from_alloc.is_some());
debug_assert!(to_alloc.is_some());
debug_assert_eq!(from_inst, to_inst.prev());
// N.B.: these moves happen with the *same* priority as
// LR-to-LR moves, because they work just like them: they
// connect a use at one progpoint (move-After) with a def
// at an adjacent progpoint (move+1-Before), so they must
// happen in parallel with all other LR-to-LR moves.
self.insert_move(
ProgPoint::before(to_inst),
InsertMovePrio::Regular,
from_alloc,
to_alloc,
Some(self.vreg(to_vreg)),
);
}
// Sort the debug-locations vector; we provide this
// invariant to the client.
self.debug_locations.sort_unstable();
}
pub fn resolve_inserted_moves(&mut self) {
// For each program point, gather all moves together. Then
// resolve (see cases below).
let mut i = 0;
self.inserted_moves
.sort_unstable_by_key(|m| m.pos_prio.key());
// Redundant-move elimination state tracker.
let mut redundant_moves = RedundantMoveEliminator::default();
fn redundant_move_process_side_effects<'a, F: Function>(
this: &Env<'a, F>,
redundant_moves: &mut RedundantMoveEliminator,
from: ProgPoint,
to: ProgPoint,
) {
// If any safepoints in range, clear and return.
// Also, if we cross a block boundary, clear and return.
if this.cfginfo.insn_block[from.inst().index()]
!= this.cfginfo.insn_block[to.inst().index()]
{
redundant_moves.clear();
return;
}
for inst in from.inst().index()..=to.inst().index() {
if this.func.requires_refs_on_stack(Inst::new(inst)) {
redundant_moves.clear();
return;
}
}
let start_inst = if from.pos() == InstPosition::Before {
from.inst()
} else {
from.inst().next()
};
let end_inst = if to.pos() == InstPosition::Before {
to.inst()
} else {
to.inst().next()
};
for inst in start_inst.index()..end_inst.index() {
let inst = Inst::new(inst);
for (i, op) in this.func.inst_operands(inst).iter().enumerate() {
match op.kind() {
OperandKind::Def | OperandKind::Mod => {
let alloc = this.get_alloc(inst, i);
redundant_moves.clear_alloc(alloc);
}
_ => {}
}
}
for reg in this.func.inst_clobbers(inst) {
redundant_moves.clear_alloc(Allocation::reg(reg));
}
}
}
let mut last_pos = ProgPoint::before(Inst::new(0));
while i < self.inserted_moves.len() {
let start = i;
let pos_prio = self.inserted_moves[i].pos_prio;
while i < self.inserted_moves.len() && self.inserted_moves[i].pos_prio == pos_prio {
i += 1;
}
let moves = &self.inserted_moves[start..i];
redundant_move_process_side_effects(self, &mut redundant_moves, last_pos, pos_prio.pos);
last_pos = pos_prio.pos;
// Gather all the moves with Int class and Float class
// separately. These cannot interact, so it is safe to
// have two separate ParallelMove instances. They need to
// be separate because moves between the two classes are
// impossible. (We could enhance ParallelMoves to
// understand register classes, but this seems simpler.)
let mut int_moves: SmallVec<[InsertedMove; 8]> = smallvec![];
let mut float_moves: SmallVec<[InsertedMove; 8]> = smallvec![];
for m in moves {
if m.from_alloc.is_reg() && m.to_alloc.is_reg() {
debug_assert_eq!(m.from_alloc.class(), m.to_alloc.class());
}
if m.from_alloc == m.to_alloc {
continue;
}
match m.from_alloc.class() {
RegClass::Int => {
int_moves.push(m.clone());
}
RegClass::Float => {
float_moves.push(m.clone());
}
}
}
for &(regclass, moves) in
&[(RegClass::Int, &int_moves), (RegClass::Float, &float_moves)]
{
// All moves in `moves` semantically happen in
// parallel. Let's resolve these to a sequence of moves
// that can be done one at a time.
let mut parallel_moves = ParallelMoves::new();
trace!(
"parallel moves at pos {:?} prio {:?}",
pos_prio.pos,
pos_prio.prio
);
for m in moves {
if (m.from_alloc != m.to_alloc) || m.to_vreg.is_some() {
trace!(" {} -> {}", m.from_alloc, m.to_alloc,);
parallel_moves.add(m.from_alloc, m.to_alloc, m.to_vreg);
}
}
let resolved = parallel_moves.resolve();
let mut scratch_iter = RegTraversalIter::new(
self.env,
regclass,
PReg::invalid(),
PReg::invalid(),
0,
None,
);
let key = LiveRangeKey::from_range(&CodeRange {
from: pos_prio.pos,
to: pos_prio.pos.next(),
});
let get_reg = || {
while let Some(preg) = scratch_iter.next() {
if !self.pregs[preg.index()]
.allocations
.btree
.contains_key(&key)
{
let alloc = Allocation::reg(preg);
if moves
.iter()
.any(|m| m.from_alloc == alloc || m.to_alloc == alloc)
{
// Skip pregs used by moves in this
// parallel move set, even if not
// marked used at progpoint: edge move
// liveranges meet but don't overlap
// so otherwise we may incorrectly
// overwrite a source reg.
continue;
}
return Some(alloc);
}
}
None
};
let mut stackslot_idx = 0;
let get_stackslot = || {
let idx = stackslot_idx;
stackslot_idx += 1;
// We can't borrow `self` as mutable, so we create
// these placeholders then allocate the actual
// slots if needed with `self.allocate_spillslot`
// below.
Allocation::stack(SpillSlot::new(SpillSlot::MAX - idx, regclass))
};
let preferred_victim = self.preferred_victim_by_class[regclass as usize];
let scratch_resolver =
MoveAndScratchResolver::new(get_reg, get_stackslot, preferred_victim);
let resolved = scratch_resolver.compute(resolved);
let mut rewrites = FxHashMap::default();
for i in 0..stackslot_idx {
if i >= self.extra_spillslots_by_class[regclass as usize].len() {
let slot = self.allocate_spillslot(regclass);
self.extra_spillslots_by_class[regclass as usize].push(slot);
}
rewrites.insert(
Allocation::stack(SpillSlot::new(SpillSlot::MAX - i, regclass)),
self.extra_spillslots_by_class[regclass as usize][i],
);
}
for (src, dst, to_vreg) in resolved {
let src = rewrites.get(&src).cloned().unwrap_or(src);
let dst = rewrites.get(&dst).cloned().unwrap_or(dst);
trace!(" resolved: {} -> {} ({:?})", src, dst, to_vreg);
let action = redundant_moves.process_move(src, dst, to_vreg);
if !action.elide {
self.add_move_edit(pos_prio, src, dst);
} else {
trace!(" -> redundant move elided");
}
}
}
}
// Ensure edits are in sorted ProgPoint order. N.B.: this must
// be a stable sort! We have to keep the order produced by the
// parallel-move resolver for all moves within a single sort
// key.
self.edits.sort_by_key(|&(pos_prio, _)| pos_prio.key());
self.stats.edits_count = self.edits.len();
// Add debug annotations.
if self.annotations_enabled {
for i in 0..self.edits.len() {
let &(pos_prio, ref edit) = &self.edits[i];
match edit {
&Edit::Move { from, to } => {
self.annotate(pos_prio.pos, format!("move {} -> {}", from, to));
}
}
}
}
}
pub fn add_move_edit(&mut self, pos_prio: PosWithPrio, from: Allocation, to: Allocation) {
if from != to {
if from.is_reg() && to.is_reg() {
debug_assert_eq!(from.as_reg().unwrap().class(), to.as_reg().unwrap().class());
}
self.edits.push((pos_prio, Edit::Move { from, to }));
}
}
}