Skip to content

Commit 6f53541

Browse files
authored
Fix minimal-bundle splitting to avoid infinite loops. (#220)
This PR reworks minimal-bundle logic to be consistent between property computation and splitting, and overall simpler. To recap: a "minimal bundle" is an allocation bundle that is as small as possible. Because RA2 does backtracking, i.e., can kick a bundle out of an allocation to make way for another, and because it can split, i.e. create more work for itself, we need to ensure the algorithm "bottoms out" somewhere to ensure termination. The way we do this is by defining a "minimal bundle": this is a bundle that cannot be split further. A minimal bundle is never evicted, and any allocatable input (set of uses/defs with constraints), when split into minimal bundles, should result in no conflicts. We thus want to define minimal bundles as *minimally* as possible so that we have the maximal solving capacity. For a long time, we defined minimal bundles as those spanning a single instruction (before- and after- progpoints) -- because we cannot insert moves in the middle of an instruction. In #214, we updated minimal-bundle splitting to avoid putting two different unrelated uses in a single-instruction bundle, beacuse doing so and calling it "minimal" (unsplittable) can artificially extend the liverange of a use with a fixed-reg constraint at the before-point into the after-point, causing an unsolveable conflict. This was triggered by new and tighter constraints on callsites in Cranelift after bytecodealliance/wasmtime#10502 (merging retval defs into calls) landed. Unfortunately this also resulted in an infinite allocation loop, because the definition of "minimal bundle" did not agree between the split-into-minimal-bundles fallback/last-ditch code, and the bundle property computation. The splitter was splitting as far as it was willing to go, but our predicate didn't consider those bundles minimal, so we continued to re-attempt splitting indefinitely. While investigating this, I found that the minimal-bundle concept had accumulated significant cruft ("the detritus of dead fuzzbugs") and this tech-debt was making things more confusing than not -- so I started by clearly defining what a minimal bundle *is*. Precisely: - A single use, within a single LiveRange; - With that LiveRange having a program-point span consistent with the use: - Early def: whole instruction (must live past Late point so it can reach its uses; moves not possible within inst); - Late def: Late point only; - Early use: Early point only; - Late use: whole instruction (must be live starting at Early so the value can reach this use; moves not possible within inst). This is made easier and simpler than what we have before largely because the minimal-bundle splitter aggressively puts spans of LiveRange without uses into the spill bundle, and because we support overlapping LiveRanges for a vreg now (thanks Trevor!), so we can rely on having *some* connectivity between the def and its uses even if we aggressively trim LiveRanges in the minimal bundles down to just their defs/uses. The split-at-program-point splitter (i.e., not the fallback split-into-minimal-bundles splitter) also got a small fix related to this: it has a mode that was intended to "split off one use" if we enter with a split-point at the start of the bundle, but this was really splitting off all uses at the program point (if there are multiple of the same vreg at the same program point). In the case that we still need to split these apart, this just falls back to the minimal-bundle splitter now. Fixes #218, #219.
1 parent f90a03b commit 6f53541

File tree

2 files changed

+82
-84
lines changed

2 files changed

+82
-84
lines changed

src/ion/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ impl<'a, F: Function> Env<'a, F> {
9797
self.process_bundles()?;
9898
self.try_allocating_regs_for_spilled_bundles();
9999
self.allocate_spillslots();
100+
if trace_enabled!() {
101+
self.dump_state();
102+
}
100103
let moves = self.apply_allocations_and_insert_moves();
101104
Ok(self.resolve_inserted_moves(moves))
102105
}

src/ion/process.rs

Lines changed: 79 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ use super::{
1919
};
2020
use crate::{
2121
ion::data_structures::{
22-
CodeRange, BUNDLE_MAX_NORMAL_SPILL_WEIGHT, MAX_SPLITS_PER_SPILLSET,
22+
CodeRange, Use, BUNDLE_MAX_NORMAL_SPILL_WEIGHT, MAX_SPLITS_PER_SPILLSET,
2323
MINIMAL_BUNDLE_SPILL_WEIGHT, MINIMAL_FIXED_BUNDLE_SPILL_WEIGHT,
2424
},
2525
Allocation, Function, Inst, InstPosition, OperandConstraint, OperandKind, PReg, ProgPoint,
@@ -292,15 +292,23 @@ impl<'a, F: Function> Env<'a, F> {
292292
break;
293293
}
294294
}
295-
// Minimal if there is only one LR and the ProgPoint range
296-
// covers only one instruction. Note that it could cover
297-
// just one ProgPoint,
298-
// i.e. X.Before..X.After, or two ProgPoints,
299-
// i.e. X.Before..X+1.Before.
300-
trace!(" -> first range has range {:?}", first_range_data.range);
301-
let bundle_start = self.ctx.bundles[bundle].ranges.first().unwrap().range.from;
302-
let bundle_end = self.ctx.bundles[bundle].ranges.last().unwrap().range.to;
303-
minimal = bundle_start.inst() == bundle_end.prev().inst();
295+
// Minimal if there is only one LR and only up to one use
296+
// in that LR, and extent of range is consistent with the
297+
// "minimal range for use" definition.
298+
minimal = match &first_range_data.uses[..] {
299+
[] => true,
300+
[only_use] => {
301+
// We use a "contains" rather than "exact" range
302+
// check because a smaller-than-min range is also
303+
// OK, if some logic produced it for a valid
304+
// reason -- for example, a dead def. We don't
305+
// want to livelock on "smaller than minimal"
306+
// ranges.
307+
let min_range = minimal_range_for_use(only_use);
308+
min_range.contains(&first_range_data.range)
309+
}
310+
_ => false,
311+
};
304312
trace!(" -> minimal: {}", minimal);
305313
} else {
306314
minimal = false;
@@ -430,9 +438,26 @@ impl<'a, F: Function> Env<'a, F> {
430438
let bundle_end = self.ctx.bundles[bundle].ranges.last().unwrap().range.to;
431439
debug_assert!(split_at < bundle_end);
432440

441+
trace!(
442+
"bundle_start = {:?} bundle_end = {:?}",
443+
bundle_start,
444+
bundle_end
445+
);
446+
447+
// Is the bundle at most spanning one instruction? Then
448+
// there's nothing we can do with this logic (we will not
449+
// split in the middle of the instruction). Fall back to the
450+
// minimal-bundle splitting in this case as well.
451+
if bundle_end.prev().inst() == bundle_start.inst() {
452+
trace!(" -> spans only one inst; splitting into minimal bundles");
453+
self.split_into_minimal_bundles(bundle, reg_hint);
454+
return;
455+
}
456+
433457
// Is the split point *at* the start? If so, peel off the
434-
// first use: set the split point just after it, or just
435-
// before it if it comes after the start of the bundle.
458+
// program point with the first use: set the split point just
459+
// after it, or just before it if it comes after the start of
460+
// the bundle.
436461
if split_at == bundle_start {
437462
// Find any uses; if none, just chop off one instruction.
438463
let mut first_use = None;
@@ -762,7 +787,7 @@ impl<'a, F: Function> Env<'a, F> {
762787
///
763788
/// This is meant to solve a quadratic-cost problem that exists
764789
/// with "normal" splitting as implemented above. With that
765-
/// procedure, , splitting a bundle produces two
790+
/// procedure, splitting a bundle produces two
766791
/// halves. Furthermore, it has cost linear in the length of the
767792
/// bundle, because the resulting half-bundles have their
768793
/// requirements recomputed with a new scan, and because we copy
@@ -804,17 +829,10 @@ impl<'a, F: Function> Env<'a, F> {
804829
reg_hint
805830
);
806831

807-
let mut last_lr: Option<LiveRangeIndex> = None;
808-
let mut last_bundle: Option<LiveBundleIndex> = None;
809-
let mut last_inst: Option<Inst> = None;
810-
let mut last_vreg: Option<VRegIndex> = None;
811-
812832
let mut spill_uses = UseList::new_in(self.ctx.bump());
813833

814834
let empty_vec = LiveRangeList::new_in(self.ctx.bump());
815835
for entry in core::mem::replace(&mut self.ctx.bundles[bundle].ranges, empty_vec) {
816-
let lr_from = entry.range.from;
817-
let lr_to = entry.range.to;
818836
let vreg = self.ctx.ranges[entry.index].vreg;
819837

820838
self.ctx.scratch_removed_lrs.insert(entry.index);
@@ -825,10 +843,9 @@ impl<'a, F: Function> Env<'a, F> {
825843
let mut spill_range = entry.range;
826844
let mut spill_starts_def = false;
827845

828-
let mut last_live_pos = entry.range.from;
829846
let empty_vec = UseList::new_in(self.ctx.bump());
830847
for u in core::mem::replace(&mut self.ctx.ranges[entry.index].uses, empty_vec) {
831-
trace!(" -> use {:?} (last_live_pos {:?})", u, last_live_pos);
848+
trace!(" -> use {:?}", u);
832849

833850
let is_def = u.operand.kind() == OperandKind::Def;
834851

@@ -848,66 +865,17 @@ impl<'a, F: Function> Env<'a, F> {
848865
continue;
849866
}
850867

851-
// If this is a def of the vreg the entry cares about, make sure that the spill
852-
// range starts right before the next instruction so that the value is available.
868+
// If this is a def, make sure that the spill range
869+
// starts before the next instruction rather than at
870+
// this position: the value is available only *after*
871+
// this position.
853872
if is_def {
854873
trace!(" -> moving the spill range forward by one");
855874
spill_range.from = ProgPoint::before(u.pos.inst().next());
856875
}
857876

858-
// If we just created a LR for this inst at the last
859-
// pos, add this use to the same LR.
860-
if Some(u.pos.inst()) == last_inst && Some(vreg) == last_vreg {
861-
self.ctx.ranges[last_lr.unwrap()].uses.push(u);
862-
trace!(" -> appended to last LR {:?}", last_lr.unwrap());
863-
continue;
864-
}
865-
866-
// The minimal bundle runs through the whole inst
867-
// (up to the Before of the next inst), *unless*
868-
// the original LR was only over the Before (up to
869-
// the After) of this inst.
870-
let to = core::cmp::min(ProgPoint::before(u.pos.inst().next()), lr_to);
871-
872-
// If the last bundle was at the same inst, add a new
873-
// LR to the same bundle; otherwise, create a LR and a
874-
// new bundle.
875-
if Some(u.pos.inst()) == last_inst {
876-
let cr = CodeRange { from: u.pos, to };
877-
let lr = self.ctx.ranges.add(cr, self.ctx.bump());
878-
new_lrs.push((vreg, lr));
879-
self.ctx.ranges[lr].uses.push(u);
880-
self.ctx.ranges[lr].vreg = vreg;
881-
882-
trace!(
883-
" -> created new LR {:?} but adding to existing bundle {:?}",
884-
lr,
885-
last_bundle.unwrap()
886-
);
887-
// Edit the previous LR to end mid-inst.
888-
self.ctx.bundles[last_bundle.unwrap()]
889-
.ranges
890-
.last_mut()
891-
.unwrap()
892-
.range
893-
.to = u.pos;
894-
self.ctx.ranges[last_lr.unwrap()].range.to = u.pos;
895-
// Add this LR to the bundle.
896-
self.ctx.bundles[last_bundle.unwrap()]
897-
.ranges
898-
.push(LiveRangeListEntry {
899-
range: cr,
900-
index: lr,
901-
});
902-
self.ctx.ranges[lr].bundle = last_bundle.unwrap();
903-
last_live_pos = ProgPoint::before(u.pos.inst().next());
904-
continue;
905-
}
906-
907-
// Otherwise, create a new LR.
908-
let pos = ProgPoint::before(u.pos.inst());
909-
let pos = core::cmp::max(lr_from, pos);
910-
let cr = CodeRange { from: pos, to };
877+
// Create a new LR.
878+
let cr = minimal_range_for_use(&u);
911879
let lr = self.ctx.ranges.add(cr, self.ctx.bump());
912880
new_lrs.push((vreg, lr));
913881
self.ctx.ranges[lr].uses.push(u);
@@ -936,13 +904,6 @@ impl<'a, F: Function> Env<'a, F> {
936904
cr,
937905
new_bundle
938906
);
939-
940-
last_live_pos = ProgPoint::before(u.pos.inst().next());
941-
942-
last_lr = Some(lr);
943-
last_bundle = Some(new_bundle);
944-
last_inst = Some(u.pos.inst());
945-
last_vreg = Some(vreg);
946907
}
947908

948909
if !spill_range.is_empty() {
@@ -1363,3 +1324,37 @@ impl<'a, F: Function> Env<'a, F> {
13631324
return Ok(());
13641325
}
13651326
}
1327+
1328+
/// Compute the minimal range that covers a given use in a minimal
1329+
/// bundle. This definition needs to be consistent between bundle
1330+
/// property computation and minimal-range splitting (fallback
1331+
/// splitting).
1332+
///
1333+
/// The cases are:
1334+
/// - early def: whole instruction
1335+
/// - late def: Late only
1336+
/// - early use: Early only
1337+
/// - late use: whole instruction
1338+
fn minimal_range_for_use(u: &Use) -> CodeRange {
1339+
let early = ProgPoint::before(u.pos.inst());
1340+
let late = ProgPoint::after(u.pos.inst());
1341+
let next_early = ProgPoint::before(u.pos.inst().next());
1342+
match (u.pos.pos(), u.operand.kind()) {
1343+
(InstPosition::Before, OperandKind::Def) => CodeRange {
1344+
from: early,
1345+
to: next_early,
1346+
},
1347+
(InstPosition::Before, OperandKind::Use) => CodeRange {
1348+
from: early,
1349+
to: late,
1350+
},
1351+
(InstPosition::After, OperandKind::Def) => CodeRange {
1352+
from: late,
1353+
to: next_early,
1354+
},
1355+
(InstPosition::After, OperandKind::Use) => CodeRange {
1356+
from: early,
1357+
to: next_early,
1358+
},
1359+
}
1360+
}

0 commit comments

Comments
 (0)