Lesson 23.8 — Scheduling and register allocation: phase ordering and integration¶
Techniques: prepass vs postpass scheduling (the phase-ordering problem); integrated prepass scheduling (Goodman–Hsu IPS, pressure-sensitive schedulers); scheduler-sensitive register allocation (Pinter's parallelizable interference graph, Norris–Pollock) and post-allocation renaming · Prerequisites: Lesson 23.3, Ch 22 (interference graphs, coloring, spilling) · Time: 3 hours
Scheduling wants values in flight at the same time; register allocation wants as few values live at once as possible. Each phase changes the other's problem. Schedule first, and the allocator faces longer live ranges and may spill. Allocate first, and every register the allocator reuses adds an anti or output dependence that pins two computations in their original order, so the scheduler cannot overlap them. The running block of Lesson 23.2 shows the cost directly. Its g: r1 = load B[1] reuses r1, the register of a's value, as an allocator would. Rename g to a fresh register r8 and every list scheduler of Lesson 23.3 finds a schedule of length 9, the lower bound. With the reuse, the best is 10 and bottom-up gets 11. This lesson presents the three answers compilers use: order the phases and live with it, make the scheduler watch registers, or make the allocator watch the schedule.
1. Problem and motivation¶
Prepass vs postpass scheduling (phase ordering)¶
The first RISC compilers scheduled after register allocation (postpass). Hennessy and Gross's pipeline scheduler for MIPS worked on allocated code, because only then are all loads, stores and spills known [HG83]. Gibbons and Muchnick did the same on the HP Precision Architecture [GM86]. The price is the false dependences of allocated code. Scheduling before allocation (prepass) works on virtual registers without false dependences, but it cannot see spill code and may create it. Most compilers today do both. LLVM's MachineScheduler is prepass, with an optional post-RA scheduler (Lesson 23.7). GCC has sched1 (prepass, off by default on x86, where registers are scarce) and sched2 (postpass, on at -O2).
Integrated prepass scheduling¶
Goodman and Hsu proposed to keep scheduling before allocation but make the scheduler count registers. Their Integrated Prepass Scheduling (IPS) uses a latency-oriented priority while enough registers are free, and switches to a register-reducing priority when the count of available registers falls below a threshold [GH88]. The same paper gives DAG-driven register allocation, which allocates during scheduling and reuses the register freed longest ago to avoid creating false dependences. Every production prepass scheduler descends from IPS: LLVM's GenericScheduler with its excess/critical pressure criteria (Lesson 23.7), SelectionDAG's list-hybrid, and GCC's -fsched-pressure.
Scheduler-sensitive register allocation¶
The other direction changes the allocator. Pinter defined the parallelizable interference graph: the interference graph plus an edge between any two values whose defining operations could execute in parallel. A coloring of it introduces no false dependence that restricts the schedule [Pin93]. Norris and Pollock made a Chaitin-style allocator scheduler-sensitive: it adds such edges optimistically and removes them first when coloring would otherwise spill [NP93]. Bradlee, Eggers and Henry compared integrated strategies (RASE) on RISC machines and found that a loosely coupled prepass scheduler with pressure feedback gets most of the benefit [BEH91]. Compilers also use a cheap after-the-fact variant: renaming registers after allocation to remove the false dependences it created (GCC's regrename, LLVM's anti-dependence breakers of Lesson 23.7).
2. Definitions and algorithms¶
Definition 23.8.1 (Allocated DAG, false dependences introduced by allocation)
Let \(G\) be the dependence DAG of a block over virtual registers (each value has its own name) and let \(\rho\) assign a physical register to every value. The allocated DAG \(G_\rho\) is the DAG of the block with every value \(v\) renamed to \(\rho(v)\) (Algorithm 23.2.6). Its edges not in \(G\) are the false dependences introduced by \(\rho\): anti and output edges between values sharing a register.
Proposition 23.8.2 (Allocation can only remove schedules)
Every valid schedule of \(G_\rho\) is a valid schedule of \(G\), so \(\mathrm{CP}(G_\rho) \ge \mathrm{CP}(G)\) and the optimal length of \(G_\rho\) is at least that of \(G\).
Proof
Renaming preserves the true dependences: a use still reads the definition that reaches it, because \(\rho\) is a valid allocation (values that are live at the same time get different registers), so reaching definitions are unchanged. Memory edges do not depend on registers. Hence \(G \subseteq G_\rho\) with the same latencies on common edges, every constraint of \(G\) is a constraint of \(G_\rho\), and the claims follow from Theorem 23.2.12 and the definition of the optimum.
Prepass vs postpass scheduling (phase ordering)¶
Algorithm 23.8.3 (Prepass and postpass pipelines)
- Input: a function in SSA machine code.
- Output: allocated, scheduled code.
- Precondition: a list scheduler (Lesson 23.3) and a register allocator (Ch 22).
- Postcondition: valid code; the schedule respects \(G\) (prepass) or \(G_\rho\) (postpass).
- Invariant: each phase preserves semantics (Theorem 23.2.11 for scheduling, the allocator's correctness for allocation).
Integrated prepass scheduling¶
Algorithm 23.8.4 (IPS, after Goodman and Hsu)
- Input: a block's DAG over virtual registers; the number \(k\) of allocatable registers; a threshold \(\tau\).
- Output: a schedule.
- Precondition: \(k\) is at least the MaxLive of some order of the block.
- Postcondition: a valid schedule; while \(\mathrm{AVLREG} \ge \tau\) the order follows latency (CSP), otherwise it reduces live registers (CSR).
- Invariant: \(\mathrm{AVLREG} = k - (\text{number of live values at the current point})\).
function IPS(G, k, τ):
AVLREG ← k − |live-in values|; ready ← roots
while ready ≠ ∅:
if AVLREG > τ: # CSP: code scheduling for pipelines
v ← the ready op with the largest height that does not stall;
if every ready op stalls, the one with the largest height
else: # CSR: code scheduling to reduce registers
v ← a ready op that frees registers (kills ≥ defines), preferring the most kills;
else the ready op that starts the chain freeing a register soonest
emit v; AVLREG ← AVLREG − defines(v) + kills(v); update ready
Algorithm 23.3.10 (the lab's pressure algorithm) is a variant with a hard cap \(K\) and the switch at \(K - 1\).
Scheduler-sensitive register allocation¶
Definition 23.8.5 (Parallelizable interference graph)
For the virtual DAG \(G\), two values \(u, v\) are parallelizable if neither's defining operation reaches the other's in \(G\) (no path either way). The parallelizable interference graph \(I_P\) has an edge between \(u\) and \(v\) if they interfere (both live at some point of the current order) or they are parallelizable.
Theorem 23.8.6 (Coloring \(I_P\) introduces no restricting false dependence)
If \(\rho\) is a proper coloring of \(I_P\), then two values share a register only if their definitions are ordered by a path of \(G\). In particular, every false dependence that \(\rho\) introduces lies between operations already ordered by \(G\), and no pair of operations that \(G\) allows to run in parallel is serialized by the allocation.
Proof sketch (full proof: [Pin93])
Values sharing a register are not adjacent in \(I_P\), so they are not parallelizable, so one definition reaches the other. A false edge introduced by the sharing (anti from the uses of the first value to the second definition, output between the definitions) connects operations on the two sides of that path. Pinter additionally shows that the anti edges from the uses are harmless whenever the uses are themselves ordered before the second definition in \(G\), and restricts the parallelizable relation to make that hold. The full statement and its proof are in [Pin93].
Algorithm 23.8.7 (Scheduler-sensitive graph coloring, after Norris and Pollock)
- Input: the virtual DAG \(G\); the interference graph \(I\); \(k\) registers.
- Output: an allocation \(\rho\) (with spills if needed).
- Precondition: a Chaitin–Briggs allocator (Ch 22).
- Postcondition: a valid allocation; parallelizable pairs share a register only when needed to avoid spilling.
- Invariant: the working graph always contains \(I\); only "scheduler edges" are ever removed.
function SchedulerSensitiveColor(G, I, k):
W ← I ∪ { (u, v) | u, v parallelizable in G } # scheduler edges, marked
loop:
if Simplify(W, k) succeeds: return Select(W) # Chaitin–Briggs on W
if W has scheduler edges:
remove the scheduler edges of the node blocking simplification with the fewest
lost parallelism (for example, the pair whose operations have the least slack)
else:
spill as Chaitin–Briggs would; rebuild I and W
Algorithm 23.8.8 (Post-allocation renaming, as in GCC's regrename)
- Input: allocated code of a block; liveness.
- Output: code where some def-use chains use a different register.
- Precondition: the chain (a definition and all the uses it reaches) is complete within the block and has no tied or fixed operands.
- Postcondition: semantics preserved (Proposition 23.7.10); fewer anti and output edges.
- Invariant: a replacement register is free (not live, not referenced) over the whole chain.
3. Worked examples¶
Prepass vs postpass scheduling (phase ordering)¶
The running block with and without the reuse of r1 (lab machine, lab algorithms; the renamed block has g: r8 = load B[1] and i: r7 = sub r6, r8):
| algorithm | length, with r1 reused (\(G_\rho\)) |
length, renamed (\(G\)) | MaxLive renamed |
|---|---|---|---|
td-cp |
10 | 9 | 6 |
td-succ |
11 | 9 | 6 |
bu-cp |
11 | 9 | 6 |
bu-succ |
11 | 9 | 6 |
Without the reuse, \(G\) lacks the anti edge d→g and the output edge a→g, so the load of B[1] can issue at cycle 3 (td-cp puts g at 3 instead of 5), and every algorithm reaches the lower bound \(\max(\mathrm{CP}, \mathrm{RB}) = 9\) (Proposition 23.8.2 in reverse: the extra edges cost one or two cycles). The in-order MaxLive of the renamed block is 5, so an allocator with five registers can color it only in an order close to in-order. That is the dilemma: a prepass schedule of length 9 needs 6 registers.
Integrated prepass scheduling¶
IPS on the renamed block with \(k = 5\) and \(\tau = 1\) behaves like Algorithm 23.3.10 with \(K = 5\) (Lesson 23.3 §3, which traces it on the original block): it follows heights until only one register is free, then prefers operations that kill values (d, which kills a's value, and the store h, which defines nothing) and delays the load of B[1] until registers die. The result trades one cycle for one register, exactly as in Lesson 23.3.
Scheduler-sensitive register allocation¶
In the renamed block the values of a (r1) and g (r8) do not interfere (a's last use d is before g's definition in program order). A Chaitin allocator may therefore give them the same register, which recreates the false edges. Their definitions are parallelizable (no path between a and g in \(G\)), so \(I_P\) has an edge a–g, and coloring \(I_P\) forces different registers (Theorem 23.8.6). If only five registers are available, Algorithm 23.8.7 removes that scheduler edge first, because a has slack 1, and accepts the serialization rather than spill.
4. Invariants and correctness¶
Prepass vs postpass scheduling (phase ordering)¶
Each phase preserves semantics on its own (Theorem 23.2.11 and the allocator's correctness), so any composition does. The quality question is formal only in one direction: Proposition 23.8.2 says postpass scheduling can never beat prepass scheduling on the same code. The prepass order can, however, force spills, which add operations. The two phase orders are therefore incomparable in general, as the §7 box shows.
Post-RA scheduling cannot undo the allocator's choices
A post-RA scheduler sees \(G_\rho\). If the allocator reused a register between two independent chains, the anti edge is as real to the scheduler as a true dependence, and no priority function helps. Only renaming (Algorithm 23.8.8 or an anti-dependence breaker) or a prepass schedule can recover the overlap. The AArch64 box in §7 shows 12 cycles lost this way.
Integrated prepass scheduling¶
IPS is a list scheduler, so validity is Lemma 23.3.11. The register count is a heuristic in the original IPS (it switches modes and does not cap). The capped variant with a fallback (Proposition 23.3.12) guarantees the limit.
Scheduler-sensitive register allocation¶
Algorithm 23.8.7 never removes an interference edge of \(I\), so the final coloring is a proper coloring of \(I\) and a valid allocation, as in Chaitin–Briggs. Theorem 23.8.6 describes what the scheduler edges buy while they last. Renaming (Algorithm 23.8.8) is correct by Proposition 23.7.10.
5. Complexity¶
Let \(n\) be the operations, \(e\) the DAG edges, \(v\) the values and \(k\) the registers.
| Technique | Time | Notes |
|---|---|---|
| Prepass / postpass | the sum of the phases: list scheduling \(O((n + e)\log n)\), allocation as in Ch 22 | a second (post-RA) scheduling pass doubles scheduling time |
| IPS (Alg. 23.8.4) | \(O((n + e)\log n)\) plus \(O(1)\) amortized register accounting per operation | as list scheduling |
| Parallelizable interference graph | \(O(v^2)\) edges in the worst case; reachability for all pairs \(O(n \cdot e / w)\) with bit sets | the graph can be dense |
| Scheduler-sensitive coloring (Alg. 23.8.7) | Chaitin–Briggs on \(O(v^2)\) edges, repeated after each removal round | removal rounds bounded by the number of scheduler edges |
Justification. Pairwise reachability in a DAG is computed by a reverse-topological sweep, OR-ing successor bit sets of \(n\) bits: \(O(e)\) word operations of \(n/w\) words each.
Pathological family. A block of \(m\) independent chains (no edges between chains) makes every pair of values in different chains parallelizable: \(I_P\) contains a complete multipartite graph with \(\Theta(v^2)\) edges and chromatic number at least \(m\). With \(k < m\) registers every scheduler edge between chains must eventually be removed, so the integrated allocator does \(\Theta(v^2)\) extra work and then produces the same allocation as the plain one. This is why production compilers prefer pressure-aware prepass scheduling (IPS) to scheduler-sensitive allocation.
6. Variants and refinements¶
Prepass vs postpass scheduling (phase ordering)¶
- Schedule twice (LLVM, GCC): prepass with pressure tracking, then a light postpass. This is the common compromise.
- Iterate (reschedule after spilling): repeat allocation and scheduling until stable, as some VLIW compilers do. Better code, more compile time.
Integrated prepass scheduling¶
- Pressure sets per register class (LLVM
RegPressureTracker, GCC's per-class pressure with-fsched-pressure): a separate count and limit per class. Needed with separate integer and vector register files. - Occupancy-driven scheduling (AMDGPU): GPU register counts decide how many waves can run, so the scheduler minimizes registers first and ILP second.
Scheduler-sensitive register allocation¶
- Combined formulations (integer programming for both, e.g. Kästner's work; constraint programming in Unison): optimal for small functions, too slow in general.
- Register renaming in hardware: out-of-order cores rename architectural registers to physical ones, which removes most false dependences at run time. It is a large part of why post-RA scheduling matters less on such cores.
7. In real compilers¶
Prepass vs postpass scheduling (phase ordering)¶
TargetPassConfig::addMachinePasses (llvm/lib/CodeGen/TargetPassConfig.cpp) adds MachineScheduler before register allocation and PostRAScheduler or PostMachineScheduler after it, when the subtarget enables them [LLVM-TPC]. GCC's pass list runs sched1 (pass_sched) before and sched2 (pass_sched2) after reload in gcc/sched-rgn.cc [GCC-sched-rgn].
Postpass scheduling cannot recover what the allocator serialized (AArch64)
Reproduce (llc 23.1.2, llvm-mca 23.1.2; poly.ll from Lesson 23.3 §7):
for f in "-enable-misched=false -misched-postra-direction=topdown" \
"-misched-prera-direction=topdown -enable-post-misched=false"; do
echo "== $f"
llc -O2 -mtriple=aarch64-linux-gnu -mcpu=cortex-a55 $f poly.ll -o - \
| grep -P '^\t(ldp|madd|mul|eor)' | tee q.s
llvm-mca -mtriple=aarch64-linux-gnu -mcpu=cortex-a55 -iterations=1 q.s | grep 'Total Cycles'
done
Output (complete):
== -enable-misched=false -misched-postra-direction=topdown
ldp x8, x9, [x0]
madd x8, x8, x1, x9
ldp x9, x10, [x0, #16]
madd x9, x9, x1, x10
ldp x10, x11, [x0, #32]
madd x10, x10, x1, x11
ldp x11, x12, [x0, #48]
mul x8, x9, x8
madd x11, x11, x1, x12
mul x9, x11, x10
eor x0, x9, x8
Total Cycles: 32
== -misched-prera-direction=topdown -enable-post-misched=false
ldp x8, x9, [x0]
ldp x10, x11, [x0, #16]
ldp x12, x13, [x0, #32]
madd x8, x8, x1, x9
ldp x9, x14, [x0, #48]
madd x10, x10, x1, x11
madd x11, x12, x1, x13
madd x9, x9, x1, x14
mul x8, x10, x8
mul x9, x9, x11
eor x0, x9, x8
Total Cycles: 20
What to notice: the first configuration schedules only after allocation (postpass, top-down).
The allocator has reused x9 and x10: ldp x9, x10, [x0, #16] overwrites x9, which the first
madd reads, an anti dependence of \(G_\rho\) (Definition 23.8.1). So the loads cannot move up, and
32 cycles remain. The second configuration schedules before allocation (prepass, top-down): the
three loads start early and the allocator uses more registers (x12–x14), 20 cycles
(Lesson 23.3's box). Proposition 23.8.2 in action.
Integrated prepass scheduling¶
LLVM's GenericScheduler tracks pressure per pressure set with RegPressureTracker and consults it first in tryCandidate (Lesson 23.7) [LLVM-MISched, LLVM-RegPressure]. GCC's -fsched-pressure (the sched_pressure modes in gcc/haifa-sched.cc, weighted or model-based) adds a pressure cost to each ready instruction [GCC-haifa].
GCC's pressure-aware scheduler at the register limit
Reproduce (gcc 14.2.0 on x86-64 Linux):
{ echo 'long p2(long *restrict a, long *restrict b) {'
for i in $(seq 0 15); do echo " long t$i = a[$i] * b[$i];"; done
printf ' return ((t0 - t15) * (t1 - t14)) ^ ((t2 - t13) * (t3 - t12)) ^ ((t4 - t11) * (t5 - t10)) ^ ((t6 - t9) * (t7 - t8));\n}\n'
} > p2.c
gcc-14 -O2 -fschedule-insns -fsched-pressure -fsched-verbose=5 -fdump-rtl-sched1 -c p2.c -o p2.o
grep -o 'GENERAL_REGS:[0-9]*([-0-9]*)' p2.c.*r.sched1 | sort -t: -k2 -n | uniq -c
grep -c 'cost=12' p2.c.*r.sched1
Output (complete):
4 GENERAL_REGS:2(-7)
11 GENERAL_REGS:3(-6)
3 GENERAL_REGS:4(-5)
5 GENERAL_REGS:5(-4)
4 GENERAL_REGS:6(-3)
15 GENERAL_REGS:7(-2)
20 GENERAL_REGS:8(-1)
18 GENERAL_REGS:9(0)
92
What to notice: before each scheduling decision GCC prints the current pressure of each
register class and, in parentheses, its distance from the class's limit (9 general registers
available to the scheduler here). The pressure climbs as products are computed, and it stops at
the limit (9(0)) and never exceeds it. The 92 ready-list lines with cost=12 are the
moments when an instruction that would raise pressure beyond the limit carries a penalty, so the
scheduler prefers the ones that consume values: IPS's CSR mode.
Scheduler-sensitive register allocation¶
No mainstream allocator is scheduler-sensitive in Pinter's sense. LLVM's greedy allocator uses hints and split costs but no parallelizable graph. Both compilers undo allocation-induced false dependences afterwards: GCC's regrename pass (gcc/regrename.cc, -frename-registers, on by default with -funroll-loops and on some targets) and LLVM's anti-dependence breakers (Lesson 23.7) [GCC-regrename].
GCC renames a register to free the scheduler
Reproduce (gcc 14.2.0; poly.c from Lesson 23.3 §7):
for f in "" "-frename-registers"; do echo "== gcc-14 -O2 $f"
gcc-14 -O2 $f -S poly.c -o - | grep -P '^\t(mov|imul|add|xor)'
done
gcc-14 -O2 -frename-registers -fdump-rtl-rnreg -c poly.c -o poly.o
grep -E 'Creating chain|no available|renamed as' poly.c.*r.rnreg
Output (complete):
== gcc-14 -O2
movq (%rdi), %rax
movq 16(%rdi), %rdx
imulq %rsi, %rax
addq 8(%rdi), %rax
imulq %rsi, %rdx
addq 24(%rdi), %rdx
imulq %rdx, %rax
movq 32(%rdi), %rdx
imulq %rsi, %rdx
addq 40(%rdi), %rdx
imulq 48(%rdi), %rsi
addq 56(%rdi), %rsi
imulq %rsi, %rdx
xorq %rdx, %rax
== gcc-14 -O2 -frename-registers
movq (%rdi), %rax
movq 16(%rdi), %rdx
movq 32(%rdi), %rcx
imulq %rsi, %rax
addq 8(%rdi), %rax
imulq %rsi, %rdx
addq 24(%rdi), %rdx
imulq %rsi, %rcx
addq 40(%rdi), %rcx
imulq 48(%rdi), %rsi
addq 56(%rdi), %rsi
imulq %rdx, %rax
imulq %rsi, %rcx
xorq %rcx, %rax
Creating chain ax (0) at insn 44
Creating chain dx (1) at insn 46
Creating chain dx (2) at insn 48
Register dx in insn 46; no available better choice
, renamed as cx
What to notice: GCC's allocator put the second and third partial results in the same
register %rdx. That creates an anti dependence: movq 32(%rdi), %rdx must wait for
imulq %rdx, %rax to read the old value, so the third chain starts late. regrename finds the
def-use chain of the second %rdx (chain 2, starting at insn 48) and renames it to the free
register %rcx (the dump splits that message over two lines). Then sched2 hoists the third
load next to the other two (Algorithm 23.8.8, Proposition 23.7.10).
8. Comparison¶
| Technique | Power / precision | Speed (asymptotic · practical) | Output / error quality | Implementation effort | Typical use |
|---|---|---|---|---|---|
| Prepass vs postpass scheduling | postpass is limited by false edges (Proposition 23.8.2); prepass may spill | the sum of the phases | incomparable: the A55 box loses 12 cycles postpass; Lesson 23.3's box spills prepass | low (ordering passes) | LLVM and GCC: both, with pressure tracking prepass |
| Integrated prepass scheduling (IPS) | latency until registers run short, then register reduction | list scheduling | fewer spills at small cost in length (lab: MaxLive −12 %, length +4.6 %) | moderate: live-value accounting | LLVM GenericScheduler, SelectionDAG list-hybrid, GCC -fsched-pressure |
| Scheduler-sensitive register allocation | coloring \(I_P\) adds no restricting false dependence (Theorem 23.8.6) | \(\Theta(v^2)\) extra edges | best schedules when registers suffice; degrades to plain coloring otherwise | high: allocator changes | research compilers; renaming passes (GCC regrename, LLVM anti-dep breakers) in production |
Choose prepass scheduling with pressure tracking (IPS) as the default. It is what both LLVM and GCC do. Add a postpass scheduler for in-order and VLIW cores, together with renaming so that the allocator's reuse does not pin the code. Choose scheduler-sensitive allocation only for statically scheduled machines with large register files, where every false dependence costs cycles and registers are rarely short.
9. Assessment¶
- Quiz:
false-deps-running(number),ips-modes(single),parallelizable-pairs(set),regrename-effect(number), andmaxlive-running(Lesson 23.3). Tagsphase-ordering,integrated-prepass,sched-sensitive-ra. - Drills:
./course drill list-schedule --difficulty hardincludes register reuse, so its traces show false dependences;./course drill critical-path --difficulty hardasks for anti and output edges. Integrated allocation is exercised by the lab's ★pressurealgorithm (E3) and the quiz; a drill for coloring \(I_P\) would duplicate Ch 22's interference-graph drills. - Flashcards: tags
phase-ordering,integrated-prepass,sched-sensitive-ra.
References¶
See the chapter references.