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