Skip to content

Commit a01056e

Browse files
committed
Implement lifetime holes in linear_scan
1 parent 897dbab commit a01056e

1 file changed

Lines changed: 144 additions & 69 deletions

File tree

zjit/src/backend/lir.rs

Lines changed: 144 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::cell::RefCell;
2-
use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};
2+
use std::collections::{HashMap, HashSet, VecDeque};
33
use std::fmt;
44
use std::mem::take;
55
use std::rc::Rc;
@@ -1372,7 +1372,7 @@ impl LiveRange {
13721372
}
13731373
}
13741374

1375-
#[derive(Clone)]
1375+
#[derive(Clone, PartialEq)]
13761376
pub enum State {
13771377
Unhandled,
13781378
Active,
@@ -1410,13 +1410,40 @@ impl Interval {
14101410
self.ranges.last().unwrap().to
14111411
}
14121412

1413+
pub fn next_intersection(&self, other: &Interval) -> Option<usize> {
1414+
let (mut i, mut j) = (0, 0);
1415+
1416+
// Both range lists are sorted and disjoint, so advancing whichever
1417+
// range ends first can never skip past an overlap. That makes the
1418+
// first overlap found the earliest one.
1419+
while let (Some(a), Some(b)) = (self.ranges.get(i), other.ranges.get(j)) {
1420+
let lo = a.from.max(b.from);
1421+
if lo < a.to.min(b.to) {
1422+
return Some(lo);
1423+
}
1424+
if a.to < b.to { i += 1; } else { j += 1; }
1425+
}
1426+
1427+
None
1428+
}
1429+
1430+
pub fn ends_before(&self, pos: usize) -> bool {
1431+
self.end() <= pos
1432+
}
1433+
14131434
/// Check if the interval is alive at position
14141435
/// Panics if the range is not set
14151436
pub fn survives(&self, position: usize) -> bool {
14161437
assert!(self.ranges.len() > 0, "survives called on interval with no range");
14171438
self.ranges.iter().any(|range| range.from < position && position < range.to)
14181439
}
14191440

1441+
/// Returns true if position falls inside one of the ranges in this
1442+
/// interval.
1443+
pub fn covers(&self, position: usize) -> bool {
1444+
self.ranges.iter().any(|range| range.from <= position && position < range.to)
1445+
}
1446+
14201447
pub fn born_at(&self, x: usize) -> bool {
14211448
self.start() == x
14221449
}
@@ -2126,42 +2153,26 @@ impl Assembler
21262153
}
21272154
}
21282155

2129-
fn release_assignment(it: &Interval, num_registers: usize, regs: &[Reg], free_registers: &mut BTreeSet<usize>) {
2130-
if let Some(allocation) = it.assigned {
2131-
if let Some(reg) = allocation.alloc_pool_index(num_registers) {
2132-
let was_not_there_before = free_registers.insert(reg);
2133-
assert!(
2134-
was_not_there_before,
2135-
"attempted to return allocator register {:?} to the free pool more than once",
2136-
allocation.assigned_reg(regs).unwrap(),
2137-
);
2138-
} else {
2139-
assert!(
2140-
allocation.assigned_reg(regs).is_none_or(|reg| {
2141-
regs.iter()
2142-
.take(num_registers)
2143-
.all(|candidate| candidate.reg_no != reg.reg_no)
2144-
}),
2145-
"attempted to return non-allocatable register {:?} to the allocator pool",
2146-
allocation.assigned_reg(regs).unwrap(),
2147-
);
2148-
}
2149-
}
2150-
}
2151-
21522156
// TODO: We want to make the following refactoring so that we DON'T have
21532157
// to parcopy in to entry blocks
21542158
//
21552159
// * Pre-allocate pinned regs
21562160
// * Update linear scan to handle pinned LRs
21572161
//
2162+
// num_registers is the number of allocatable registers. `regs` is a
2163+
// list of _all_ registers partitioned by allocatable and non-allocatable.
2164+
// The registers in `regs` from 0 to num_registers is allocatable,
2165+
// num_registers to the end are only allocatable via "preferred" register
2166+
// support.
21582167
pub fn linear_scan(
21592168
&self,
21602169
intervals: Vec<Interval>,
2161-
num_registers: usize,
2162-
regs: &[Reg],
2170+
num_registers: usize, // number of allocatable regs
2171+
regs: &[Reg], // list of all registers used, partitioned by allocatability
21632172
) -> (Vec<Option<Allocation>>, usize) {
2164-
let mut free_registers: BTreeSet<usize> = (0..num_registers).collect();
2173+
debug_assert!(num_registers <= regs.len());
2174+
2175+
let mut free_registers: Vec<usize> = vec![0; regs.len()];
21652176
let mut active: Vec<Interval> = Vec::new(); // sorted by increasing end point
21662177
let mut inactive: Vec<Interval> = Vec::new(); // intervals with lifetime holes
21672178
let mut num_stack_slots: usize = 0;
@@ -2176,46 +2187,100 @@ impl Assembler
21762187
let mut unhandled: VecDeque<Interval> = sorted_intervals.into();
21772188

21782189
while let Some(mut interval) = unhandled.pop_front() {
2190+
let position = interval.start();
2191+
21792192
// Expire old intervals.
21802193
for mut it in std::mem::take(&mut active) {
2181-
if it.end() > interval.start() {
2182-
active.push(it);
2183-
} else {
2184-
Self::release_assignment(&it, num_registers, regs, &mut free_registers);
2194+
assert!(it.state == State::Active);
2195+
// If the interval ends before the current position, then we're
2196+
// done with it and can mark it handled.
2197+
if it.ends_before(position) {
21852198
it.state = State::Handled;
21862199
handled.push(it);
2200+
} else if !it.covers(position) {
2201+
// If it doesn't cover the current position (there's a hole
2202+
// in the ranges), then move it to inactive
2203+
it.state = State::Inactive;
2204+
inactive.push(it);
2205+
} else {
2206+
// Otherwise it's still live here and stays active.
2207+
active.push(it);
21872208
}
21882209
}
21892210

2190-
let preferred_alloc = interval.preferred;
2191-
let preferred_taken = preferred_alloc
2192-
.is_some_and(|alloc|
2193-
active.iter().any(|active_interval| active_interval.assigned == Some(alloc))
2194-
);
2211+
// Check for intervals in inactive that are handled or active
2212+
for mut it in std::mem::take(&mut inactive) {
2213+
assert!(it.state == State::Inactive);
2214+
// If the inactive interval ends before the current position
2215+
if it.ends_before(position) {
2216+
// Move it to handled
2217+
it.state = State::Handled;
2218+
handled.push(it);
21952219

2196-
if let Some(preferred_alloc) = preferred_alloc.filter(|_| !preferred_taken) {
2197-
if let Some(reg_idx) = preferred_alloc.alloc_pool_index(num_registers) {
2198-
if free_registers.remove(&reg_idx) {
2199-
interval.assigned = Some(preferred_alloc);
2200-
let insert_idx = active.partition_point(|i| i.end() < interval.end());
2201-
active.insert(insert_idx, interval);
2202-
continue;
2203-
}
2220+
} else if it.covers(position) {
2221+
// If the current position falls inside one of the
2222+
// interval's ranges, then make it active
2223+
it.state = State::Active;
2224+
active.push(it);
22042225
} else {
2205-
interval.assigned = Some(preferred_alloc);
2226+
// It's still inactive
2227+
inactive.push(it);
2228+
}
2229+
}
2230+
2231+
// Mark all registers as "free". In other words, they aren't
2232+
// in use until instruction number usize::MAX.
2233+
free_registers.fill(usize::MAX);
2234+
2235+
// Mark all active intervals with assignments as "unavailable". They are available
2236+
// again at instruction 0 which effectively means they can't be used (as all
2237+
// intervals will end after 0)
2238+
for it in &active {
2239+
debug_assert!(it.state == State::Active);
2240+
match it.assigned.expect("should have assignment") {
2241+
Allocation::Reg(idx) => free_registers[idx] = 0,
2242+
_ => {},
2243+
}
2244+
}
2245+
2246+
// Inactive intervals are intervals that have gaps in them, and
2247+
// we're currently inside one of those gaps. We'll mark in the
2248+
// "free_registers" list when each interval will want to use its
2249+
// assigned reg again.
2250+
for it in &inactive {
2251+
debug_assert!(it.state == State::Inactive);
2252+
let Some(pos) = it.next_intersection(&interval) else { continue };
2253+
match it.assigned.expect("should have assignment") {
2254+
Allocation::Reg(idx) => free_registers[idx] = free_registers[idx].min(pos),
2255+
_ => {},
2256+
}
2257+
}
2258+
2259+
// If the current interval has a preferred allocation, use it if
2260+
// that register is available until the interval end.
2261+
if let Some(Allocation::Reg(idx)) = interval.preferred {
2262+
if free_registers[idx] >= interval.end() {
2263+
interval.assigned = Some(Allocation::Reg(idx));
2264+
interval.state = State::Active;
22062265
let insert_idx = active.partition_point(|i| i.end() < interval.end());
22072266
active.insert(insert_idx, interval);
22082267
continue;
22092268
}
22102269
}
22112270

2212-
if free_registers.is_empty() {
2271+
// Find a reg with the highest "free until use" point, then check if that "free until
2272+
// use" point is greater than or eq to than the current interval's end. If it is, then
2273+
// we know the current interval can fit in the window this register is available.
2274+
let best_reg = (0..num_registers).rev()
2275+
.max_by_key(|&reg| free_registers[reg])
2276+
.filter(|&reg| free_registers[reg] >= interval.end());
2277+
2278+
// We couldn't find a best register, so we need to spill
2279+
if best_reg.is_none() {
22132280
// Spill: pick the longest-lived active interval (last in sorted active)
22142281
// but only from the allocatable partition of the pool. An index
22152282
// at or past `num_registers` is a pinned physical register (for
22162283
// example SP), which is not ours to hand to someone else.
2217-
// Take the id and end point rather than a reference, so that `active`
2218-
// can be mutated below.
22192284
let spill = active.iter().rev()
22202285
.find(|active_interval| {
22212286
active_interval.assigned
@@ -2231,28 +2296,38 @@ impl Assembler
22312296
let mut spilled = active.remove(spill_idx);
22322297
interval.assigned = spilled.assigned;
22332298
spilled.assigned = Some(slot);
2299+
spilled.state = State::Handled;
22342300
handled.push(spilled);
22352301
// Insert current into sorted active
22362302
let insert_idx = active.partition_point(|i| i.end() < interval.end());
2303+
interval.state = State::Active;
22372304
active.insert(insert_idx, interval);
22382305
} else {
22392306
// Spill the current interval
22402307
interval.assigned = Some(slot);
2308+
interval.state = State::Handled;
22412309
handled.push(interval);
22422310
}
22432311
} else {
2244-
// Allocate lowest free register
2245-
let reg = *free_registers.iter().min().unwrap();
2246-
free_registers.remove(&reg);
2312+
let reg = best_reg.unwrap();
22472313
interval.assigned = Some(Allocation::Reg(reg));
2314+
interval.state = State::Active;
22482315
// Insert into sorted active
22492316
let insert_idx = active.partition_point(|i| i.end() < interval.end());
22502317
active.insert(insert_idx, interval);
22512318
}
22522319
}
22532320

2321+
// Drain active and inactive and mark everything as handled.
2322+
for mut it in active.drain(..).chain(inactive.drain(..)) {
2323+
it.state = State::Handled;
2324+
handled.push(it);
2325+
}
2326+
debug_assert!(active.is_empty() && inactive.is_empty());
2327+
22542328
let mut assignment: Vec<Option<Allocation>> = vec![None; num_intervals];
2255-
for it in active.into_iter().chain(handled) {
2329+
for it in handled {
2330+
debug_assert!(it.state == State::Handled);
22562331
assignment[it.id] = it.assigned;
22572332
}
22582333

@@ -3343,7 +3418,7 @@ pub fn lir_intervals_string(asm: &Assembler, intervals: &[Interval]) -> String {
33433418
}
33443419
output.push_str(&format!("{param}"));
33453420
}
3346-
output.push_str("):\n");
3421+
output.push_str("):\n"); // :)
33473422
}
33483423

33493424
for (insn, insn_id) in block.insns.iter().zip(&block.insn_ids) {
@@ -3362,7 +3437,7 @@ pub fn lir_intervals_string(asm: &Assembler, intervals: &[Interval]) -> String {
33623437
output.push_str(" v ");
33633438
} else if interval.dies_at(insn_id.0) {
33643439
output.push_str(" ^ ");
3365-
} else if interval.survives(insn_id.0) {
3440+
} else if interval.covers(insn_id.0) {
33663441
output.push_str(" █ ");
33673442
} else {
33683443
output.push_str(" . ");
@@ -4685,7 +4760,7 @@ mod tests {
46854760
assert_eq!(assignments[r11_idx], Some(Allocation::Reg(1)));
46864761
assert_eq!(assignments[r12_idx], Some(Allocation::Reg(1)));
46874762
assert_eq!(assignments[r13_idx], Some(Allocation::Reg(2)));
4688-
assert_eq!(assignments[r14_idx], Some(Allocation::Reg(3)));
4763+
assert_eq!(assignments[r14_idx], Some(Allocation::Reg(1)));
46894764
assert_eq!(assignments[r15_idx], Some(Allocation::Reg(2)));
46904765
}
46914766

@@ -4697,7 +4772,7 @@ mod tests {
46974772
asm.number_instructions(16);
46984773
let mut intervals = asm.build_intervals(live_in);
46994774

4700-
// 3 registers -- only r10 needs to spill
4775+
// 3 registers -- enough for every interval once holes are reused
47014776
let mut regs = crate::backend::current::ALLOC_REGS.to_vec();
47024777
let allocatable_regs = regs.len();
47034778
asm.preferred_register_assignments(&mut intervals, &mut regs);
@@ -4710,12 +4785,14 @@ mod tests {
47104785
let r14_idx = if let Opnd::VReg { idx, .. } = r14 { idx } else { panic!() };
47114786
let r15_idx = if let Opnd::VReg { idx, .. } = r15 { idx } else { panic!() };
47124787

4713-
assert_eq!(num_stack_slots, 1);
4714-
assert_eq!(assignments[r10_idx], Some(Allocation::Stack(0)));
4788+
// Lifetime holes make three registers enough: nothing spills, and the
4789+
// assignment is the same one five registers produce.
4790+
assert_eq!(num_stack_slots, 0);
4791+
assert_eq!(assignments[r10_idx], Some(Allocation::Reg(0)));
47154792
assert_eq!(assignments[r11_idx], Some(Allocation::Reg(1)));
47164793
assert_eq!(assignments[r12_idx], Some(Allocation::Reg(1)));
47174794
assert_eq!(assignments[r13_idx], Some(Allocation::Reg(2)));
4718-
assert_eq!(assignments[r14_idx], Some(Allocation::Reg(0)));
4795+
assert_eq!(assignments[r14_idx], Some(Allocation::Reg(1)));
47194796
assert_eq!(assignments[r15_idx], Some(Allocation::Reg(2)));
47204797
}
47214798

@@ -4832,20 +4909,18 @@ mod tests {
48324909
if *dest == Opnd::Reg(regs[1]) && *src == Opnd::UImm(1)));
48334910

48344911
// Edge b3->b2 (single succ): args=[v4, v5], params=[v2, v3]
4835-
// v4->Reg(3), v5->Reg(2), v2->Reg(1), v3->Reg(2)
4836-
// Reg copy: Reg(3)->Reg(1) -> Mov(regs[1], regs[3])
4837-
// Reg(2)->Reg(2) is self-move, filtered
4838-
// Inserted in b3 before Jmp: [Label, Mul, Sub, Mov, Jmp]
4912+
// v4->Reg(1), v5->Reg(2), v2->Reg(1), v3->Reg(2)
4913+
// Reg(1)->Reg(1) and Reg(2)->Reg(2) are both self-moves, filtered, so
4914+
// this edge needs no moves at all: [Label, Mul, Sub, Jmp]
48394915
let b3_insns = &asm.basic_blocks[b3.0].insns;
4840-
assert_eq!(b3_insns.len(), 5);
4841-
assert!(matches!(&b3_insns[3], Insn::Mov { dest, src }
4842-
if *dest == Opnd::Reg(regs[1]) && *src == Opnd::Reg(regs[3])));
4916+
assert_eq!(b3_insns.len(), 4);
4917+
assert!(matches!(&b3_insns[3], Insn::Jmp(..)));
48434918

48444919
// Verify original instructions in b3 are rewritten to physical registers.
48454920
// b3: Mul { left: r12, right: r13, out: r14 }, Sub { left: r13, right: UImm(1), out: r15 }
4846-
// r12->Reg(1), r13->Reg(2), r14->Reg(3), r15->Reg(2)
4921+
// r12->Reg(1), r13->Reg(2), r14->Reg(1), r15->Reg(2)
48474922
assert!(matches!(&b3_insns[1], Insn::Mul { left, right, out }
4848-
if *left == Opnd::Reg(regs[1]) && *right == Opnd::Reg(regs[2]) && *out == Opnd::Reg(regs[3])));
4923+
if *left == Opnd::Reg(regs[1]) && *right == Opnd::Reg(regs[2]) && *out == Opnd::Reg(regs[1])));
48494924
assert!(matches!(&b3_insns[2], Insn::Sub { left, right, out }
48504925
if *left == Opnd::Reg(regs[2]) && *right == Opnd::UImm(1) && *out == Opnd::Reg(regs[2])));
48514926
}

0 commit comments

Comments
 (0)