lib/std/lang/gen/regalloc/spill.rad 12.5 KiB 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
}