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