compiler/
lib/
examples/
std/
arch/
char/
collections/
lang/
alloc/
ast/
gen/
bitset/
regalloc/
assign.rad
11.1 KiB
liveness.rad
8.9 KiB
spill.rad
11.5 KiB
bitset.rad
6.1 KiB
data.rad
8.6 KiB
labels.rad
2.4 KiB
regalloc.rad
2.3 KiB
types.rad
594 B
il/
module/
parser/
resolver/
scanner/
alloc.rad
5.3 KiB
ast.rad
23.6 KiB
gen.rad
513 B
il.rad
15.5 KiB
lower.rad
277.2 KiB
module.rad
13.5 KiB
package.rad
1.3 KiB
parser.rad
79.6 KiB
resolver.rad
332.7 KiB
scanner.rad
17.6 KiB
sexpr.rad
6.4 KiB
strings.rad
2.2 KiB
types.rad
286 B
sys/
arch.rad
68 B
char.rad
855 B
collections.rad
39 B
fmt.rad
8.3 KiB
intrinsics.rad
467 B
io.rad
1.7 KiB
lang.rad
276 B
mem.rad
2.3 KiB
sys.rad
179 B
testing.rad
2.4 KiB
tests.rad
15.7 KiB
vec.rad
3.2 KiB
std.rad
281 B
scripts/
seed/
sublime/
test/
vim/
.gitignore
336 B
.gitsigners
112 B
CONTRIBUTING
2.1 KiB
LICENSE
1.1 KiB
Makefile
4.1 KiB
README
2.5 KiB
STYLE
2.6 KiB
std.lib
1.2 KiB
std.lib.test
347 B
lib/std/lang/gen/regalloc/spill.rad
raw
| 1 | //! Spilling pass - determines which SSA values need stack slots. |
| 2 | //! |
| 3 | //! This pass analyzes register pressure and determines which values must be |
| 4 | //! spilled to memory. It does not modify the IL. |
| 5 | //! |
| 6 | //! The instruction selector uses this information to emit load/store |
| 7 | //! instructions for spilled values. |
| 8 | //! |
| 9 | //! # Algorithm |
| 10 | //! |
| 11 | //! 1. Cost calculation: for each value, compute spill cost: |
| 12 | //! |
| 13 | //! cost = (defs + uses) * 2^min(loopDepth, 10) |
| 14 | //! |
| 15 | //! Values used in inner loops are expensive to spill. |
| 16 | //! |
| 17 | //! 2. Pressure analysis: walk instructions backwards, tracking live set. |
| 18 | //! |
| 19 | //! When `|live| > numRegs`: |
| 20 | //! |
| 21 | //! * Sort live values by cost (ascending). |
| 22 | //! * Mark lowest-cost values as spilled until pressure is acceptable. |
| 23 | //! |
| 24 | //! 3. Slot assignment: assign stack offsets to spilled values. |
| 25 | //! |
| 26 | //! # Output |
| 27 | //! |
| 28 | //! Spill info containing: |
| 29 | //! |
| 30 | //! * Spill slot assignments: SSA reg -> stack offset, `-1` if not spilled. |
| 31 | //! * Total frame size needed for spills. |
| 32 | |
| 33 | use std::lang::il; |
| 34 | use std::lang::alloc; |
| 35 | use std::lang::gen::bitset; |
| 36 | use std::lang::gen::regalloc::liveness; |
| 37 | |
| 38 | /// Maximum number of candidates for spill sorting. |
| 39 | constant MAX_CANDIDATES: u32 = 256; |
| 40 | /// Maximum loop depth for cost weighting (2^10 = 1024). |
| 41 | constant MAX_LOOP_WEIGHT: u32 = 10; |
| 42 | |
| 43 | /// Spill cost for a single SSA register. |
| 44 | record SpillCost: Copy { |
| 45 | /// Number of definitions (weighted by loop depth). |
| 46 | defs: u32, |
| 47 | /// Number of uses (weighted by loop depth). |
| 48 | uses: u32, |
| 49 | } |
| 50 | |
| 51 | /// Spill decision for a function. |
| 52 | export record SpillInfo: Copy { |
| 53 | /// SSA register mapped to stack slot offset. `-1` means not spilled. |
| 54 | slots: *[i32], |
| 55 | /// Total spill frame size needed in bytes. |
| 56 | frameSize: i32, |
| 57 | /// Values that must be allocated in callee-saved registers. |
| 58 | calleeClass: bitset::Bitset, |
| 59 | /// Maximum SSA register number. |
| 60 | maxReg: u32, |
| 61 | } |
| 62 | |
| 63 | /// Candidate buffer for spill decisions. |
| 64 | record Candidates: Copy { |
| 65 | entries: [CostEntry; 256], |
| 66 | n: u32, |
| 67 | } |
| 68 | |
| 69 | /// Entry for cost sorting. |
| 70 | record CostEntry: Copy { |
| 71 | reg: u32, |
| 72 | cost: u32, |
| 73 | } |
| 74 | |
| 75 | /// Context for counting register uses. |
| 76 | record CountCtx: Copy { |
| 77 | costs: *unsafe mut [SpillCost], |
| 78 | weight: u32, |
| 79 | } |
| 80 | |
| 81 | /// Analyze a function and determine which values need spill slots. |
| 82 | export unsafe fn analyze( |
| 83 | func: *unsafe il::Fn, |
| 84 | live: &liveness::LiveInfo, |
| 85 | numRegs: u32, |
| 86 | numCalleeSaved: u32, |
| 87 | slotSize: u32, |
| 88 | arena: &mut alloc::Arena |
| 89 | ) -> SpillInfo throws (alloc::AllocError) { |
| 90 | let maxReg = live.maxReg; |
| 91 | if maxReg == 0 { |
| 92 | let calleeClass = try bitset::allocate(arena, 0); |
| 93 | return SpillInfo { |
| 94 | slots: &mut [], |
| 95 | frameSize: 0, |
| 96 | calleeClass, |
| 97 | maxReg: 0, |
| 98 | }; |
| 99 | } |
| 100 | // Allocate spill slots array. |
| 101 | let slots = try alloc::allocSlice(arena, @sizeOf(i32), @alignOf(i32), maxReg) as *mut [i32]; |
| 102 | for i in 0..maxReg { |
| 103 | set slots[i] = -1; |
| 104 | } |
| 105 | // Allocate cost array. |
| 106 | let costs = try alloc::allocRawSlice(arena, @sizeOf(SpillCost), @alignOf(SpillCost), maxReg) as *unsafe mut [SpillCost]; |
| 107 | for i in 0..maxReg { |
| 108 | set costs[i] = SpillCost { defs: 0, uses: 0 }; |
| 109 | } |
| 110 | // Phase 1: Calculate spill costs. |
| 111 | fillCosts(func, costs); |
| 112 | |
| 113 | // Phase 2: Find values that exceed register pressure. |
| 114 | let mut spilled = try bitset::allocate(arena, maxReg); |
| 115 | let mut calleeClass = try bitset::allocate(arena, maxReg); |
| 116 | let mut scratch = try bitset::allocate(arena, maxReg); |
| 117 | |
| 118 | for b in 0..func.blocks.len { |
| 119 | let block = &func.blocks[b]; |
| 120 | |
| 121 | // Start with live-out set. |
| 122 | bitset::copy(&mut scratch, &live.liveOut[b]); |
| 123 | |
| 124 | // Walk instructions backwards. |
| 125 | let mut i = block.instrs.len; |
| 126 | while i > 0 { |
| 127 | set i -= 1; |
| 128 | let instr = block.instrs[i]; |
| 129 | |
| 130 | // Limit register pressure before processing this instruction. |
| 131 | try limitPressure(&mut scratch, &mut spilled, costs, numRegs); |
| 132 | |
| 133 | // Enforce cross-call pressure at call sites. |
| 134 | if il::isCall(instr) { |
| 135 | try limitCrossCallPressure( |
| 136 | &mut scratch, &mut spilled, costs, |
| 137 | &mut calleeClass, numCalleeSaved, il::instrDst(instr) |
| 138 | ); |
| 139 | } |
| 140 | // Remove definition from live set. |
| 141 | if let dst = il::instrDst(instr) { |
| 142 | bitset::clear(&mut scratch, dst.n); |
| 143 | } |
| 144 | // Add uses to live set. |
| 145 | il::forEachReg(instr, addRegToSetCallback, &mut scratch as &mut opaque); |
| 146 | } |
| 147 | // Also limit pressure at block entry. |
| 148 | try limitPressure(&mut scratch, &mut spilled, costs, numRegs); |
| 149 | } |
| 150 | |
| 151 | // Phase 3: Enforce global callee-class limit. |
| 152 | // The per-call-site limit may leave the callee-class set larger than |
| 153 | // `numCalleeSaved` when different call sites keep different subsets. |
| 154 | // Spill excess values to guarantee the assignment phase always finds |
| 155 | // a callee-saved register for cross-call values. |
| 156 | let calleeCount = bitset::count(&calleeClass); |
| 157 | if calleeCount > numCalleeSaved { |
| 158 | let mut it = bitset::iter(&calleeClass); |
| 159 | for _ in 0..(calleeCount - numCalleeSaved) { |
| 160 | if let reg = bitset::iterNext(&mut it) { |
| 161 | bitset::clear(&mut calleeClass, reg); |
| 162 | bitset::put(&mut spilled, reg); |
| 163 | } else { |
| 164 | panic "spill: count > 0 but no set bits found"; |
| 165 | } |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | // Phase 4: Assign stack slots to spilled values. |
| 170 | let mut frameSize: i32 = 0; |
| 171 | let mut it = bitset::iter(&spilled); |
| 172 | |
| 173 | while let n = bitset::iterNext(&mut it) { |
| 174 | set slots[n] = frameSize; |
| 175 | set frameSize += slotSize as i32; |
| 176 | } |
| 177 | return SpillInfo { slots, frameSize, calleeClass, maxReg }; |
| 178 | } |
| 179 | |
| 180 | /// Calculate spill costs for all registers, weighted by loop depth. |
| 181 | unsafe fn fillCosts(func: *unsafe il::Fn, costs: *unsafe mut [SpillCost]) { |
| 182 | for b in 0..func.blocks.len { |
| 183 | let block = &func.blocks[b]; |
| 184 | |
| 185 | // Exponential weight for loop depth, capped to avoid overflow. |
| 186 | let depth = MAX_LOOP_WEIGHT if block.loopDepth > MAX_LOOP_WEIGHT else block.loopDepth; |
| 187 | let weight: u32 = 1 << depth; |
| 188 | |
| 189 | // Count block parameter definitions. |
| 190 | for p in block.params { |
| 191 | if p.value.n < costs.len { |
| 192 | set costs[p.value.n].defs = costs[p.value.n].defs + weight; |
| 193 | } |
| 194 | } |
| 195 | // Count instruction defs and uses. |
| 196 | for i in 0..block.instrs.len { |
| 197 | let instr = block.instrs[i]; |
| 198 | |
| 199 | // Count definition. |
| 200 | if let dst = il::instrDst(instr) { |
| 201 | if dst.n < costs.len { |
| 202 | set costs[dst.n].defs = costs[dst.n].defs + weight; |
| 203 | } |
| 204 | } |
| 205 | // Count uses. |
| 206 | let mut ctx = CountCtx { costs, weight }; |
| 207 | il::forEachReg(instr, countRegUseCallback, &mut ctx as &mut opaque); |
| 208 | } |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | /// Sort candidates by cost (ascending) using insertion sort, then spill |
| 213 | /// the cheapest `excess` values: set in `spilled`, clear in `source`. |
| 214 | unsafe fn spillCheapest( |
| 215 | c: &mut Candidates, |
| 216 | excess: u32, |
| 217 | source: &mut bitset::Bitset, |
| 218 | spilled: &mut bitset::Bitset |
| 219 | ) { |
| 220 | // Insertion sort ascending by cost. |
| 221 | for i in 1..c.n { |
| 222 | let key = c.entries[i]; |
| 223 | let mut j: u32 = i; |
| 224 | while j > 0 and c.entries[j - 1].cost > key.cost { |
| 225 | set c.entries[j] = c.entries[j - 1]; |
| 226 | set j -= 1; |
| 227 | } |
| 228 | set c.entries[j] = key; |
| 229 | } |
| 230 | let toSpill = c.n if excess > c.n else excess; |
| 231 | for i in 0..toSpill { |
| 232 | bitset::put(spilled, c.entries[i].reg); |
| 233 | bitset::clear(source, c.entries[i].reg); |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | /// Collect all values from a bitset into a candidates buffer with their costs. |
| 238 | unsafe fn collectCandidates(bs: &bitset::Bitset, costs: *unsafe [SpillCost]) -> Candidates throws (alloc::AllocError) { |
| 239 | let mut c = Candidates { entries: undefined, n: 0 }; |
| 240 | let mut it = bitset::iter(bs); |
| 241 | while let reg = bitset::iterNext(&mut it) { |
| 242 | if c.n == MAX_CANDIDATES { |
| 243 | throw alloc::AllocError::OutOfMemory; |
| 244 | } |
| 245 | if reg < costs.len { |
| 246 | set c.entries[c.n] = CostEntry { reg, cost: costs[reg].defs + costs[reg].uses }; |
| 247 | set c.n += 1; |
| 248 | } |
| 249 | } |
| 250 | return c; |
| 251 | } |
| 252 | |
| 253 | /// Limit register pressure by marking low-cost values as spilled. |
| 254 | unsafe fn limitPressure( |
| 255 | live: &mut bitset::Bitset, |
| 256 | spilled: &mut bitset::Bitset, |
| 257 | costs: *unsafe [SpillCost], |
| 258 | numRegs: u32 |
| 259 | ) throws (alloc::AllocError) { |
| 260 | let liveCount = bitset::count(live); |
| 261 | if liveCount <= numRegs { |
| 262 | return; |
| 263 | } |
| 264 | let mut c = try collectCandidates(live, costs); |
| 265 | spillCheapest(&mut c, liveCount - numRegs, live, spilled); |
| 266 | } |
| 267 | |
| 268 | /// Check whether an SSA register is the destination of a call instruction. |
| 269 | fn isCallDst(callDst: ?il::Reg, n: u32) -> bool { |
| 270 | if let d = callDst { |
| 271 | return d.n == n; |
| 272 | } |
| 273 | return false; |
| 274 | } |
| 275 | |
| 276 | /// Limit cross-call pressure by spilling values that exceed callee-saved capacity. |
| 277 | /// |
| 278 | /// At a call site, every live value must survive the call in a callee-saved |
| 279 | /// register. If the count exceeds `numCalleeSaved`, spill the cheapest |
| 280 | /// crossing values. |
| 281 | unsafe fn limitCrossCallPressure( |
| 282 | live: &mut bitset::Bitset, |
| 283 | spilled: &mut bitset::Bitset, |
| 284 | costs: *unsafe [SpillCost], |
| 285 | calleeClass: &mut bitset::Bitset, |
| 286 | numCalleeSaved: u32, |
| 287 | callDst: ?il::Reg |
| 288 | ) throws (alloc::AllocError) { |
| 289 | // Collect crossing candidates: live values excluding the call destination. |
| 290 | let mut candidates: [CostEntry; 256] = undefined; |
| 291 | let mut numCandidates: u32 = 0; |
| 292 | let mut it = bitset::iter(live); |
| 293 | while let n = bitset::iterNext(&mut it) { |
| 294 | if not isCallDst(callDst, n) and n < costs.len { |
| 295 | if numCandidates == MAX_CANDIDATES { |
| 296 | throw alloc::AllocError::OutOfMemory; |
| 297 | } |
| 298 | set candidates[numCandidates] = CostEntry { |
| 299 | reg: n, |
| 300 | cost: costs[n].defs + costs[n].uses, |
| 301 | }; |
| 302 | set numCandidates += 1; |
| 303 | } |
| 304 | } |
| 305 | // Spill cheapest candidates if crossing count exceeds callee-saved capacity. |
| 306 | if numCandidates > numCalleeSaved { |
| 307 | let mut c = Candidates { entries: candidates, n: numCandidates }; |
| 308 | spillCheapest(&mut c, numCandidates - numCalleeSaved, live, spilled); |
| 309 | } |
| 310 | // Mark crossing values that remain after spilling as callee-saved class. |
| 311 | let mut it2 = bitset::iter(live); |
| 312 | while let n = bitset::iterNext(&mut it2) { |
| 313 | if not isCallDst(callDst, n) { |
| 314 | bitset::put(calleeClass, n); |
| 315 | } |
| 316 | } |
| 317 | } |
| 318 | |
| 319 | /// Callback for [`il::forEachReg`]: increments use count for register. |
| 320 | unsafe fn countRegUseCallback(reg: il::Reg, ctxPtr: &mut opaque) { |
| 321 | let weight = (ctxPtr as &mut CountCtx).weight; |
| 322 | countRegUse(reg, &mut (ctxPtr as &mut CountCtx).costs[..], weight); |
| 323 | } |
| 324 | |
| 325 | /// Add the block weight to the register use count. |
| 326 | fn countRegUse(reg: il::Reg, costs: &mut [SpillCost], weight: u32) { |
| 327 | assert reg.n < costs.len, "countRegUseCallback: register out of bounds"; |
| 328 | set costs[reg.n].uses += weight; |
| 329 | } |
| 330 | |
| 331 | /// Callback for [`il::forEachReg`]: adds register to live set. |
| 332 | unsafe fn addRegToSetCallback(reg: il::Reg, ctx: &mut opaque) { |
| 333 | bitset::put(ctx as &mut bitset::Bitset, reg.n); |
| 334 | } |
| 335 | |
| 336 | /// Check if a register is spilled. |
| 337 | export fn isSpilled(info: &SpillInfo, reg: il::Reg) -> bool { |
| 338 | if reg.n >= info.maxReg { |
| 339 | return false; |
| 340 | } |
| 341 | return info.slots[reg.n] >= 0; |
| 342 | } |
| 343 | |
| 344 | /// Get spill slot offset for a register, or `nil` if not spilled. |
| 345 | export fn spillSlot(info: &SpillInfo, reg: il::Reg) -> ?i32 { |
| 346 | if isSpilled(info, reg) { |
| 347 | return info.slots[reg.n]; |
| 348 | } |
| 349 | return nil; |
| 350 | } |