compiler/
lib/
examples/
std/
arch/
char/
collections/
lang/
alloc/
ast/
gen/
bitset/
regalloc/
liveness/
assign.rad
11.8 KiB
liveness.rad
10.0 KiB
spill.rad
12.5 KiB
bitset.rad
4.6 KiB
data.rad
8.6 KiB
labels.rad
2.4 KiB
regalloc.rad
2.4 KiB
types.rad
594 B
il/
module/
parser/
resolver/
scanner/
alloc.rad
7.1 KiB
ast.rad
26.7 KiB
gen.rad
513 B
il.rad
20.3 KiB
lower.rad
315.0 KiB
module.rad
14.9 KiB
package.rad
1.3 KiB
parser.rad
91.2 KiB
resolver.rad
452.9 KiB
scanner.rad
17.9 KiB
sexpr.rad
6.7 KiB
strings.rad
2.2 KiB
types.rad
1.6 KiB
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
5.4 KiB
README
2.5 KiB
STYLE
2.5 KiB
std.lib
1.5 KiB
std.lib.test
662 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 | /// Largest spill frame representable by signed byte offsets. |
| 43 | constant MAX_FRAME_SIZE: u32 = 0x7fffffff; |
| 44 | |
| 45 | /// Spill cost for a single SSA register. |
| 46 | record SpillCost: Copy { |
| 47 | /// Number of definitions (weighted by loop depth). |
| 48 | defs: u32, |
| 49 | /// Number of uses (weighted by loop depth). |
| 50 | uses: u32, |
| 51 | } |
| 52 | |
| 53 | /// Spill decision for a function. |
| 54 | export record SpillInfo: 'scratch + Copy { |
| 55 | /// SSA register mapped to stack slot offset. `-1` means not spilled. |
| 56 | slots: &'scratch [i32], |
| 57 | /// Total spill frame size needed in bytes. |
| 58 | frameSize: i32, |
| 59 | /// Values that must be allocated in callee-saved registers. |
| 60 | calleeClass: &'scratch [u32], |
| 61 | /// Maximum SSA register number. |
| 62 | maxReg: u32, |
| 63 | } |
| 64 | |
| 65 | /// Candidate buffer for spill decisions. |
| 66 | record Candidates: Copy { |
| 67 | entries: [CostEntry; MAX_CANDIDATES], |
| 68 | n: u32, |
| 69 | } |
| 70 | |
| 71 | /// Entry for cost sorting. |
| 72 | record CostEntry: Copy { |
| 73 | reg: u32, |
| 74 | cost: u32, |
| 75 | } |
| 76 | |
| 77 | /// Analyze a function and determine which values need spill slots. |
| 78 | export unsafe fn analyze 'scratch ( |
| 79 | func: &il::Fn, |
| 80 | live: &liveness::LiveInfo 'scratch, |
| 81 | numRegs: u32, |
| 82 | numCalleeSaved: u32, |
| 83 | slotSize: u32, |
| 84 | storage: &Session 'scratch |
| 85 | ) -> SpillInfo 'scratch throws (alloc::AllocError) { |
| 86 | return try analyzeBlocks(func.blocks, live, numRegs, numCalleeSaved, slotSize, storage); |
| 87 | } |
| 88 | |
| 89 | /// Allocate spill storage and analyze pressure over a borrowed block table. |
| 90 | fn analyzeBlocks 'scratch ( |
| 91 | blocks: &[il::Block], |
| 92 | live: &liveness::LiveInfo 'scratch, |
| 93 | numRegs: u32, |
| 94 | numCalleeSaved: u32, |
| 95 | slotSize: u32, |
| 96 | storage: &Session 'scratch, |
| 97 | ) -> SpillInfo 'scratch throws (alloc::AllocError) { |
| 98 | let maxReg = live.maxReg; |
| 99 | if maxReg == 0 { |
| 100 | let calleeClass = try bitset::allocate(storage, 0); |
| 101 | return SpillInfo 'scratch { |
| 102 | slots: try storage.fill(-1 as i32, 0), |
| 103 | frameSize: 0, |
| 104 | calleeClass: &calleeClass[..], |
| 105 | maxReg: 0, |
| 106 | }; |
| 107 | } |
| 108 | // Allocate spill slots array. |
| 109 | let slots = try storage.fill(-1 as i32, maxReg); |
| 110 | // Allocate cost array. |
| 111 | let costs = try storage.fill(SpillCost { defs: 0, uses: 0 }, maxReg); |
| 112 | // Phase 1: Calculate spill costs. |
| 113 | for b in 0..blocks.len { |
| 114 | let block = &blocks[b]; |
| 115 | unsafe { |
| 116 | fillCosts(block.params, block.instrs, block.loopDepth, costs); |
| 117 | } |
| 118 | } |
| 119 | |
| 120 | // Phase 2: Find values that exceed register pressure. |
| 121 | let spilled = try bitset::allocate(storage, maxReg); |
| 122 | let calleeClass = try bitset::allocate(storage, maxReg); |
| 123 | let scratch = try bitset::allocate(storage, maxReg); |
| 124 | |
| 125 | for b in 0..blocks.len { |
| 126 | let block = &blocks[b]; |
| 127 | |
| 128 | // Start with live-out set. |
| 129 | bitset::copy(scratch, liveness::liveOutRow(live, b)); |
| 130 | |
| 131 | unsafe { |
| 132 | try limitBlockPressure(block.instrs, scratch, spilled, costs, calleeClass, numRegs, numCalleeSaved); |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | let frameSize = try finishSpills(slots, spilled, calleeClass, numCalleeSaved, slotSize); |
| 137 | return SpillInfo 'scratch { slots: &slots[..], frameSize, calleeClass: &calleeClass[..], maxReg }; |
| 138 | } |
| 139 | |
| 140 | /// Limit register pressure from the block's live-out set to its entry. |
| 141 | fn limitBlockPressure( |
| 142 | instructions: &[il::Instr], |
| 143 | live: &mut [u32], |
| 144 | spilled: &mut [u32], |
| 145 | costs: &[SpillCost], |
| 146 | calleeClass: &mut [u32], |
| 147 | numRegs: u32, |
| 148 | numCalleeSaved: u32, |
| 149 | ) throws (alloc::AllocError) { |
| 150 | // Walk instructions backwards. |
| 151 | let mut i = instructions.len; |
| 152 | while i > 0 { |
| 153 | set i -= 1; |
| 154 | let instr = &instructions[i]; |
| 155 | |
| 156 | // Limit register pressure before processing this instruction. |
| 157 | try limitPressure(live, spilled, costs, numRegs); |
| 158 | |
| 159 | // Enforce cross-call pressure at call sites. |
| 160 | if il::isCall(*instr) { |
| 161 | try limitCrossCallPressure( |
| 162 | live, spilled, costs, |
| 163 | calleeClass, numCalleeSaved, il::instrDst(*instr) |
| 164 | ); |
| 165 | } |
| 166 | // Remove definition from live set. |
| 167 | if let dst = il::instrDst(*instr) { |
| 168 | bitset::clear(live, dst.n); |
| 169 | } |
| 170 | // Add uses to live set. |
| 171 | let mut registers = il::registers(instr); |
| 172 | // Argument groups contain raw views into the function's IL storage. |
| 173 | unsafe { |
| 174 | while let reg = il::nextReg(&mut registers, instr) { |
| 175 | bitset::put(live, reg.n); |
| 176 | } |
| 177 | } |
| 178 | } |
| 179 | // Also limit pressure at block entry. |
| 180 | try limitPressure(live, spilled, costs, numRegs); |
| 181 | } |
| 182 | |
| 183 | /// Enforce the global register-class limit and assign bounded stack offsets. |
| 184 | fn finishSpills( |
| 185 | slots: &mut [i32], |
| 186 | spilled: &mut [u32], |
| 187 | calleeClass: &mut [u32], |
| 188 | numCalleeSaved: u32, |
| 189 | slotSize: u32, |
| 190 | ) -> i32 throws (alloc::AllocError) { |
| 191 | // Phase 3: Enforce global callee-class limit. |
| 192 | // The per-call-site limit may leave the callee-class set larger than |
| 193 | // `numCalleeSaved` when different call sites keep different subsets. |
| 194 | // Spill excess values to guarantee the assignment phase always finds |
| 195 | // a callee-saved register for cross-call values. |
| 196 | let calleeCount = bitset::count(calleeClass); |
| 197 | if calleeCount > numCalleeSaved { |
| 198 | let mut it = bitset::iter(calleeClass); |
| 199 | for _ in 0..(calleeCount - numCalleeSaved) { |
| 200 | if let reg = bitset::iterNext(&mut it, calleeClass) { |
| 201 | bitset::clear(calleeClass, reg); |
| 202 | bitset::put(spilled, reg); |
| 203 | } else { |
| 204 | panic "spill: count > 0 but no set bits found"; |
| 205 | } |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | // Phase 4: Assign stack slots to spilled values. |
| 210 | let mut frameSize: i32 = 0; |
| 211 | let mut it = bitset::iter(spilled); |
| 212 | |
| 213 | while let n = bitset::iterNext(&mut it, spilled) { |
| 214 | if slotSize > MAX_FRAME_SIZE - frameSize as u32 { |
| 215 | throw alloc::AllocError::OutOfMemory; |
| 216 | } |
| 217 | set slots[n] = frameSize; |
| 218 | set frameSize += slotSize as i32; |
| 219 | } |
| 220 | return frameSize; |
| 221 | } |
| 222 | |
| 223 | /// Add one block's register spill costs, weighted by loop depth. |
| 224 | fn fillCosts(params: &[il::Param], instructions: &[il::Instr], loopDepth: u32, costs: &mut [SpillCost]) { |
| 225 | // Exponential weight for loop depth, capped to avoid overflow. |
| 226 | let depth = MAX_LOOP_WEIGHT if loopDepth > MAX_LOOP_WEIGHT else loopDepth; |
| 227 | let weight: u32 = 1 << depth; |
| 228 | |
| 229 | // Count block parameter definitions. |
| 230 | for p in params { |
| 231 | if p.value.n < costs.len { |
| 232 | set costs[p.value.n].defs = costs[p.value.n].defs + weight; |
| 233 | } |
| 234 | } |
| 235 | // Count instruction defs and uses. |
| 236 | for i in 0..instructions.len { |
| 237 | let instr = &instructions[i]; |
| 238 | |
| 239 | // Count definition. |
| 240 | if let dst = il::instrDst(*instr) { |
| 241 | if dst.n < costs.len { |
| 242 | set costs[dst.n].defs = costs[dst.n].defs + weight; |
| 243 | } |
| 244 | } |
| 245 | // Count uses. |
| 246 | let mut registers = il::registers(instr); |
| 247 | // Argument groups contain raw views into the function's IL storage. |
| 248 | unsafe { |
| 249 | while let reg = il::nextReg(&mut registers, instr) { |
| 250 | countRegUse(reg, &mut costs[..], weight); |
| 251 | } |
| 252 | } |
| 253 | } |
| 254 | } |
| 255 | |
| 256 | /// Sort candidates by cost (ascending) using insertion sort, then spill |
| 257 | /// the cheapest `excess` values: set in `spilled`, clear in `source`. |
| 258 | fn spillCheapest( |
| 259 | c: &mut Candidates, |
| 260 | excess: u32, |
| 261 | source: &mut [u32], |
| 262 | spilled: &mut [u32] |
| 263 | ) { |
| 264 | // Insertion sort ascending by cost. |
| 265 | for i in 1..c.n { |
| 266 | let key = c.entries[i]; |
| 267 | let mut j: u32 = i; |
| 268 | while j > 0 and c.entries[j - 1].cost > key.cost { |
| 269 | set c.entries[j] = c.entries[j - 1]; |
| 270 | set j -= 1; |
| 271 | } |
| 272 | set c.entries[j] = key; |
| 273 | } |
| 274 | let toSpill = c.n if excess > c.n else excess; |
| 275 | for i in 0..toSpill { |
| 276 | bitset::put(spilled, c.entries[i].reg); |
| 277 | bitset::clear(source, c.entries[i].reg); |
| 278 | } |
| 279 | } |
| 280 | |
| 281 | /// Collect all values from a bitset into a candidates buffer with their costs. |
| 282 | fn collectCandidates(bs: &[u32], costs: &[SpillCost]) -> Candidates throws (alloc::AllocError) { |
| 283 | let mut c = Candidates { entries: [CostEntry { reg: 0, cost: 0 }; MAX_CANDIDATES], n: 0 }; |
| 284 | let mut it = bitset::iter(bs); |
| 285 | while let reg = bitset::iterNext(&mut it, bs) { |
| 286 | if c.n == MAX_CANDIDATES { |
| 287 | throw alloc::AllocError::OutOfMemory; |
| 288 | } |
| 289 | if reg < costs.len { |
| 290 | set c.entries[c.n] = CostEntry { reg, cost: costs[reg].defs + costs[reg].uses }; |
| 291 | set c.n += 1; |
| 292 | } |
| 293 | } |
| 294 | return c; |
| 295 | } |
| 296 | |
| 297 | /// Limit register pressure by marking low-cost values as spilled. |
| 298 | fn limitPressure( |
| 299 | live: &mut [u32], |
| 300 | spilled: &mut [u32], |
| 301 | costs: &[SpillCost], |
| 302 | numRegs: u32 |
| 303 | ) throws (alloc::AllocError) { |
| 304 | let liveCount = bitset::count(live); |
| 305 | if liveCount <= numRegs { |
| 306 | return; |
| 307 | } |
| 308 | let mut c = try collectCandidates(live, costs); |
| 309 | spillCheapest(&mut c, liveCount - numRegs, live, spilled); |
| 310 | } |
| 311 | |
| 312 | /// Check whether an SSA register is the destination of a call instruction. |
| 313 | fn isCallDst(callDst: ?il::Reg, n: u32) -> bool { |
| 314 | if let d = callDst { |
| 315 | return d.n == n; |
| 316 | } |
| 317 | return false; |
| 318 | } |
| 319 | |
| 320 | /// Limit cross-call pressure by spilling values that exceed callee-saved capacity. |
| 321 | /// |
| 322 | /// At a call site, every live value must survive the call in a callee-saved |
| 323 | /// register. If the count exceeds `numCalleeSaved`, spill the cheapest |
| 324 | /// crossing values. |
| 325 | fn limitCrossCallPressure( |
| 326 | live: &mut [u32], |
| 327 | spilled: &mut [u32], |
| 328 | costs: &[SpillCost], |
| 329 | calleeClass: &mut [u32], |
| 330 | numCalleeSaved: u32, |
| 331 | callDst: ?il::Reg |
| 332 | ) throws (alloc::AllocError) { |
| 333 | // Collect crossing candidates: live values excluding the call destination. |
| 334 | let mut candidates: [CostEntry; MAX_CANDIDATES] = [CostEntry { reg: 0, cost: 0 }; MAX_CANDIDATES]; |
| 335 | let mut numCandidates: u32 = 0; |
| 336 | let mut it = bitset::iter(live); |
| 337 | while let n = bitset::iterNext(&mut it, live) { |
| 338 | if not isCallDst(callDst, n) and n < costs.len { |
| 339 | if numCandidates == MAX_CANDIDATES { |
| 340 | throw alloc::AllocError::OutOfMemory; |
| 341 | } |
| 342 | set candidates[numCandidates] = CostEntry { |
| 343 | reg: n, |
| 344 | cost: costs[n].defs + costs[n].uses, |
| 345 | }; |
| 346 | set numCandidates += 1; |
| 347 | } |
| 348 | } |
| 349 | // Spill cheapest candidates if crossing count exceeds callee-saved capacity. |
| 350 | if numCandidates > numCalleeSaved { |
| 351 | let mut c = Candidates { entries: candidates, n: numCandidates }; |
| 352 | spillCheapest(&mut c, numCandidates - numCalleeSaved, live, spilled); |
| 353 | } |
| 354 | // Mark crossing values that remain after spilling as callee-saved class. |
| 355 | let mut it2 = bitset::iter(live); |
| 356 | while let n = bitset::iterNext(&mut it2, live) { |
| 357 | if not isCallDst(callDst, n) { |
| 358 | bitset::put(calleeClass, n); |
| 359 | } |
| 360 | } |
| 361 | } |
| 362 | |
| 363 | /// Add the block weight to the register use count. |
| 364 | fn countRegUse(reg: il::Reg, costs: &mut [SpillCost], weight: u32) { |
| 365 | assert reg.n < costs.len, "countRegUse: register out of bounds"; |
| 366 | set costs[reg.n].uses += weight; |
| 367 | } |
| 368 | |
| 369 | /// Check if a register is spilled. |
| 370 | export fn isSpilled 'scratch (info: &SpillInfo 'scratch, reg: il::Reg) -> bool { |
| 371 | if reg.n >= info.maxReg { |
| 372 | return false; |
| 373 | } |
| 374 | return info.slots[reg.n] >= 0; |
| 375 | } |
| 376 | |
| 377 | /// Get spill slot offset for a register, or `nil` if not spilled. |
| 378 | export fn spillSlot 'scratch (info: &SpillInfo 'scratch, reg: il::Reg) -> ?i32 { |
| 379 | if isSpilled(info, reg) { |
| 380 | return info.slots[reg.n]; |
| 381 | } |
| 382 | return nil; |
| 383 | } |