1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
//! Backtracking allocator: the as-yet-unallocated VirtualReg LR prio queue.
use std::cmp::Ordering;
use std::collections::BinaryHeap;
use crate::data_structures::{TypedIxVec, VirtualRange, VirtualRangeIx};
//=============================================================================
// The as-yet-unallocated VirtualReg LR prio queue, `VirtualRangePrioQ`.
//
// Relevant methods are parameterised by a VirtualRange env.
// What we seek to do with `VirtualRangePrioQ` is to implement a priority
// queue of as-yet unallocated virtual live ranges. For each iteration of the
// main allocation loop, we pull out the highest-priority unallocated
// VirtualRange, and either allocate it (somehow), or spill it.
//
// The Rust standard type BinaryHeap gives us an efficient way to implement
// the priority queue. However, it requires that the queue items supply the
// necessary cost-comparisons by implementing `Ord` on that type. Hence we
// have to wrap up the items we fundamentally want in the queue, viz,
// `VirtualRangeIx`, into a new type `VirtualRangeIxAndSize` that also carries
// the relevant cost field, `size`. Then we implement `Ord` for
// `VirtualRangeIxAndSize` so as to only look at the `size` fields.
//
// There is a small twist, however. Most virtual ranges are small and so will
// have a small `size` field (less than 20, let's say). For such cases,
// `BinaryHeap` will presumably choose between contenders with the same `size`
// field in some arbitrary way. This has two disadvantages:
//
// * it makes the exact allocation order, and hence allocation results,
// dependent on `BinaryHeap`'s arbitrary-choice scheme. This seems
// undesirable, and given recent shenanigans resulting from `HashMap` being
// nondeterministic even in a single-threaded scenario, I don't entirely
// trust `BinaryHeap` even to be deterministic.
//
// * experimentation with the "qsort" test case shows that breaking ties by
// selecting the entry that has been in the queue the longest, rather than
// choosing arbitrarily, gives slightly better allocations (slightly less
// spilling) in spill-heavy situations (where there are few registers).
// When there is not much spilling, it makes no difference.
//
// For these reasons, `VirtualRangeIxAndSize` also contains a `tiebreaker`
// field. The `VirtualRangePrioQ` logic gives a different value of this for
// each `VirtualRangeIxAndSize` it creates. These numbers start off at 2^32-1
// and decrease towards zero. They are used as a secondary comparison key in
// the case where the `size` fields are equal. The effect is that (1)
// tiebreaking is made completely deterministic, and (2) it breaks ties in
// favour of the oldest entry (since that will have the highest `tiebreaker`
// field).
//
// The tiebreaker field will wrap around when it hits zero, but that can only
// happen after processing 2^32-1 virtual live ranges. In such cases I would
// expect that the allocator would have run out of memory long before, so it's
// academic in practice. Even if it does wrap around there is no danger to
// the correctness of the allocations.
// Wrap up a VirtualRangeIx and its size, so that we can implement Ord for it
// on the basis of the `size` and `tiebreaker` fields.
//
// NB! Do not derive {,Partial}{Eq,Ord} for this. It has its own custom
// implementations.
struct VirtualRangeIxAndSize {
vlrix: VirtualRangeIx,
size: u16,
tiebreaker: u32,
}
impl VirtualRangeIxAndSize {
fn new(vlrix: VirtualRangeIx, size: u16, tiebreaker: u32) -> Self {
assert!(size > 0);
Self {
vlrix,
size,
tiebreaker,
}
}
}
impl PartialEq for VirtualRangeIxAndSize {
fn eq(&self, other: &Self) -> bool {
self.size == other.size && self.tiebreaker == other.tiebreaker
}
}
impl Eq for VirtualRangeIxAndSize {}
impl PartialOrd for VirtualRangeIxAndSize {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for VirtualRangeIxAndSize {
fn cmp(&self, other: &Self) -> Ordering {
match self.size.cmp(&other.size) {
Ordering::Less => Ordering::Less,
Ordering::Greater => Ordering::Greater,
Ordering::Equal => self.tiebreaker.cmp(&other.tiebreaker),
}
}
}
//=============================================================================
// VirtualRangePrioQ: public interface
pub struct VirtualRangePrioQ {
// The set of as-yet unallocated VirtualRangeIxs. These are indexes into a
// VirtualRange env that is not stored here. The VirtualRangeIxs are tagged
// with the VirtualRange size and a tiebreaker field.
heap: BinaryHeap<VirtualRangeIxAndSize>,
tiebreaker_ctr: u32,
}
impl VirtualRangePrioQ {
pub fn new(vlr_env: &TypedIxVec<VirtualRangeIx, VirtualRange>) -> Self {
let mut res = Self {
heap: BinaryHeap::new(),
tiebreaker_ctr: 0xFFFF_FFFFu32,
};
for vlrix in VirtualRangeIx::new(0).dotdot(VirtualRangeIx::new(vlr_env.len())) {
let to_add = VirtualRangeIxAndSize::new(vlrix, vlr_env[vlrix].size, res.tiebreaker_ctr);
res.heap.push(to_add);
res.tiebreaker_ctr -= 1;
}
res
}
#[inline(never)]
pub fn is_empty(&self) -> bool {
self.heap.is_empty()
}
#[inline(never)]
pub fn len(&self) -> usize {
self.heap.len()
}
// Add a VirtualRange index.
#[inline(never)]
pub fn add_VirtualRange(
&mut self,
vlr_env: &TypedIxVec<VirtualRangeIx, VirtualRange>,
vlrix: VirtualRangeIx,
) {
let to_add = VirtualRangeIxAndSize::new(vlrix, vlr_env[vlrix].size, self.tiebreaker_ctr);
self.tiebreaker_ctr -= 1;
self.heap.push(to_add);
}
// Look in `unallocated` to locate the entry referencing the VirtualRange
// with the largest `size` value. Remove the ref from `unallocated` and
// return the VLRIx for said entry.
#[inline(never)]
pub fn get_longest_VirtualRange(&mut self) -> Option<VirtualRangeIx> {
match self.heap.pop() {
None => None,
Some(VirtualRangeIxAndSize { vlrix, .. }) => Some(vlrix),
}
}
#[inline(never)]
pub fn show_with_envs(
&self,
vlr_env: &TypedIxVec<VirtualRangeIx, VirtualRange>,
) -> Vec<String> {
let mut resV = vec![];
for VirtualRangeIxAndSize { vlrix, .. } in self.heap.iter() {
let mut res = "TODO ".to_string();
res += &format!("{:?} = {:?}", vlrix, &vlr_env[*vlrix]);
resV.push(res);
}
resV
}
}