code wiki / _hdl_build / nx_regalloc_linscan.nx
nx_regalloc_linscan.nx source
↩ module page · 143 lines · 5403 B
1// nx_regalloc_linscan.nx -- linear-scan register allocation, bits-up + PROVABLE.
2//
3// This is the keystone lever the ONGOING race vs C keeps pointing at: Nishi's
4// stack/memory-resident codegen loads every operand and stores every result (the
5// ~2x speed + ~6x code-density tax the race measured). Register allocation keeps
6// live values in registers; only SPILLS touch memory. Built STANDALONE + proven
7// first (NOT hand-patched into the protected self-hosting compiler -- a prior
8// "g1 regalloc" attempt silently miscompiled high-pressure functions; we prove
9// soundness here, then the loop absorbs it into codegen under governance).
10//
11// Algorithm: Poletto & Sarkar, "Linear Scan Register Allocation," ACM TOPLAS 1999
12// (the alternative is Chaitin graph-coloring, SIGPLAN 1982; linear scan is faster
13// and good enough -- the right fit for a spore that re-derives its stack on deploy).
14//
15// Model IR: a straight-line block of n instructions; instruction i DEFINES virtual
16// register i and USES up to two prior vregs (u0[i], u1[i]; -1 = none). The live
17// interval of vreg v is [v (its def), last_use[v]]. Linear scan allocates nreg
18// physical registers across the intervals, spilling the farthest-future interval
19// when it runs out (the standard heuristic).
20
21import "nx_syscalls.nx"
22
23const RA_SPILL: i64 = 0 - 1
24
25// last_use[v] = the latest instruction that reads v (>= v's own def point).
26func ra_compute_last_use(n: i64, u0: *i64, u1: *i64, last_use: *i64) -> i64 {
27 var i: i64 = 0
28 while i < n { last_use[i] = i; i = i + 1 }
29 i = 0
30 while i < n {
31 if u0[i] >= 0 { if i > last_use[u0[i]] { last_use[u0[i]] = i } }
32 if u1[i] >= 0 { if i > last_use[u1[i]] { last_use[u1[i]] = i } }
33 i = i + 1
34 }
35 return 0
36}
37
38// Linear scan: fills alloc[v] with a physical reg 0..nreg-1, or RA_SPILL. Returns
39// the spill count. active set kept as parallel (vreg,end) arrays (no sort needed --
40// we scan; correctness does not depend on order, only the spill pick does).
41func ra_linscan(n: i64, nreg: i64, last_use: *i64, alloc: *i64) -> i64 {
42 let reg_busy: *i64 = sys_mmap(8 * nreg)
43 let act_v: *i64 = sys_mmap(8 * (n + 1))
44 let act_e: *i64 = sys_mmap(8 * (n + 1))
45 var act_n: i64 = 0
46 var spills: i64 = 0
47 var i: i64 = 0
48 while i < n {
49 // expire intervals whose end < i (their registers become free)
50 var k: i64 = 0
51 var w: i64 = 0
52 while k < act_n {
53 if act_e[k] < i {
54 reg_busy[alloc[act_v[k]]] = 0
55 } else {
56 act_v[w] = act_v[k]; act_e[w] = act_e[k]; w = w + 1
57 }
58 k = k + 1
59 }
60 act_n = w
61 // find a free physical register
62 var r: i64 = 0 - 1
63 var j: i64 = 0
64 while j < nreg { if reg_busy[j] == 0 { r = j; j = nreg } else { j = j + 1 } }
65 if r >= 0 {
66 alloc[i] = r
67 reg_busy[r] = 1
68 act_v[act_n] = i; act_e[act_n] = last_use[i]; act_n = act_n + 1
69 } else {
70 // no free reg: spill the active interval with the farthest end
71 var mx: i64 = 0 - 1
72 var mxi: i64 = 0 - 1
73 var t: i64 = 0
74 while t < act_n {
75 if act_e[t] > mx { mx = act_e[t]; mxi = t }
76 t = t + 1
77 }
78 if mx > last_use[i] {
79 let sv: i64 = act_v[mxi] // steal its register
80 alloc[i] = alloc[sv]
81 alloc[sv] = RA_SPILL
82 act_v[mxi] = i; act_e[mxi] = last_use[i]
83 } else {
84 alloc[i] = RA_SPILL // spill the new one instead
85 }
86 spills = spills + 1
87 }
88 i = i + 1
89 }
90 return spills
91}
92
93// INDEPENDENT soundness check (the proof): at EVERY program point, every pair of
94// simultaneously-live, non-spilled vregs must occupy DISTINCT physical registers.
95// This is the invariant the prior g1 attempt violated. Returns 0 if valid, else
96// 0-(p+1) for the first colliding point p.
97func ra_validate(n: i64, last_use: *i64, alloc: *i64, nreg: i64) -> i64 {
98 let seen: *i64 = sys_mmap(8 * nreg)
99 var p: i64 = 0
100 while p < n {
101 var j: i64 = 0
102 while j < nreg { seen[j] = 0; j = j + 1 }
103 var v: i64 = 0
104 while v <= p {
105 if last_use[v] >= p { // v live at p
106 let r: i64 = alloc[v]
107 if r >= 0 {
108 if seen[r] == 1 { return 0 - (p + 1) } // COLLISION
109 seen[r] = 1
110 }
111 }
112 v = v + 1
113 }
114 p = p + 1
115 }
116 return 0
117}
118
119// stack/memory-resident baseline: load each operand + store each result.
120func ra_baseline_mem(n: i64, u0: *i64, u1: *i64) -> i64 {
121 var m: i64 = 0
122 var i: i64 = 0
123 while i < n {
124 if u0[i] >= 0 { m = m + 1 }
125 if u1[i] >= 0 { m = m + 1 }
126 m = m + 1
127 i = i + 1
128 }
129 return m
130}
131
132// register-allocated traffic: only SPILLED operands load, only SPILLED results store.
133func ra_regalloc_mem(n: i64, u0: *i64, u1: *i64, alloc: *i64) -> i64 {
134 var m: i64 = 0
135 var i: i64 = 0
136 while i < n {
137 if u0[i] >= 0 { if alloc[u0[i]] == RA_SPILL { m = m + 1 } }
138 if u1[i] >= 0 { if alloc[u1[i]] == RA_SPILL { m = m + 1 } }
139 if alloc[i] == RA_SPILL { m = m + 1 }
140 i = i + 1
141 }
142 return m
143}