census by owner (25.5c): which state holds what, and how much it grew

The escape analysis now follows which state a kept value is stored into (a state parameter, a state
global - each its own class - through the flows to and from it), and every heap site in the fence's
table carries that owner. The census writes, per owning state, what its sites hold and how much that
grew since judging began: 'owner NotesState holds 6400 (+5600 since judging began)'. A keep() or
intern() is a site of its own for this, never scratch and never reported as a keep or a birth. It
needs the analysis, so the arena's (or --escape-report's) build; this is what a soak watches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 17:05:53 +03:00
parent b87ee96805
commit a6364199da
5 changed files with 71792 additions and 68451 deletions

View file

@ -20,6 +20,11 @@ const ES_FREED: int = 4 # given back with free(): no
var g_es_flag: []int = new []int
var g_es_why: []Node = new []Node # per class: the statement that made it escape
var g_es_own: []pointer = new []pointer # per class: the state that holds it (25.5c's census)
var g_es_gname: []pointer = new []pointer # a class per state global, so each state is its own
var g_es_gcls: []int = new []int
var g_es_ok: []pointer = new []pointer # file:line of a kept site -> the state that holds it
var g_es_ov: []Node = new []Node
var g_es_swhy: []Node = new []Node # per store edge: its statement
var g_es_stmt: Node = null # the statement being walked
var g_es_aok: int = 0 # inside a statement under @alloc_ok
@ -48,7 +53,7 @@ var g_es_lt: []pointer = new []pointer # ... and their types, where
var g_es_cur: int = -1
var g_es_heap: int = -1 # the one class for globals, states and the unknown
function es_new(flags: int) -> int { push(g_es_flag, flags); push(g_es_why, null); return len(g_es_flag) - 1 }
function es_new(flags: int) -> int { push(g_es_flag, flags); push(g_es_why, null); push(g_es_own, null); return len(g_es_flag) - 1 }
function es_flow(a: int, b: int) -> void { if a >= 0 and b >= 0 { push(g_es_ff, a); push(g_es_ft, b) } }
function es_store(v: int, t: int) -> void {
if v < 0 { return }
@ -174,6 +179,16 @@ function es_val(e: Node) -> int {
if k == E_ID {
let c = es_local(e.s)
if c >= 0 { return c }
let g = find_global(e.s)
if g != null and is_state_ty(g.ty) {
var i = 0
while i < len(g_es_gname) { if (g_es_gname[i] == e.s) { return g_es_gcls[i] }; i += 1 }
let sc = es_new(ES_ESC | ES_HEAP)
g_es_own[sc] = g.ty
push(g_es_gname, e.s)
push(g_es_gcls, sc)
return sc
}
return g_es_heap
}
if k == E_MEMBER { return es_val(e.a) }
@ -248,9 +263,12 @@ function es_call(e: Node) -> int {
es_args_walk(e)
return es_site(e)
}
if (nm == "keep") or (nm == "intern") {
if (nm == "keep") or (nm == "intern") { # a heap copy on purpose: a site for the census's owner, never scratch
es_args_walk(e)
return g_es_heap
let kc = es_site(e)
g_es_flag[kc] = g_es_flag[kc] | ES_ESC
g_es_site_aok[len(g_es_site_aok) - 1] = true # deliberate: not a keep to report, nor a birth
return kc
}
if (nm == "push") and len(e.kids) >= 2 {
let t = es_val(e.kids[0])
@ -443,6 +461,27 @@ function es_solve() -> void {
i += 1
}
}
# 25.5c: which state holds what - a container's owner flows forward, a stored value takes its
# target's, and what flowed into a value takes the value's; the first found stays
changed = true
while changed {
changed = false
var o = 0
while o < len(g_es_ff) {
let a = g_es_ff[o]
let b = g_es_ft[o]
if g_es_own[a] != null and g_es_own[b] == null { g_es_own[b] = g_es_own[a]; changed = true }
if g_es_own[b] != null and g_es_own[a] == null and (g_es_flag[a] & ES_ESC) != 0 { g_es_own[a] = g_es_own[b]; changed = true }
o += 1
}
o = 0
while o < len(g_es_sv) {
let v = g_es_sv[o]
let t = g_es_st[o]
if g_es_own[t] != null and g_es_own[v] == null { g_es_own[v] = g_es_own[t]; changed = true }
o += 1
}
}
# a function that reaches Mem.frame spans frames: so does every caller
changed = true
while changed {
@ -463,6 +502,7 @@ var g_es_esc_n: int = 0
function escape_analyse() -> void {
if g_es_done { return }
g_es_done = true
ck_tab_init(g_es_ok, g_es_ov)
g_es_heap = es_new(ES_ESC | ES_HEAP)
ck_tab_init(g_es_fk, g_es_fv)
var i = 0
@ -476,7 +516,11 @@ function escape_analyse() -> void {
push(g_es_fspans, d.kind == N_MAIN)
push(g_es_fparam, len(g_es_flag))
var p = 0
while p < len(d.kids) { es_new(ES_HEAP); p += 1 }
while p < len(d.kids) {
let pc = es_new(ES_HEAP)
if d.kids[p].kind == N_PARAM and is_state_ty(d.kids[p].ty) { g_es_own[pc] = d.kids[p].ty }
p += 1
}
push(g_es_fret, es_new(0))
}
i += 1
@ -516,7 +560,14 @@ function escape_analyse() -> void {
if c >= 0 and (g_es_flag[c] & ES_ESC) == 0 and not spans {
n.uns = ES_SCRATCH
g_es_local_n += 1
} else { g_es_esc_n += 1 }
} else {
g_es_esc_n += 1
if c >= 0 and g_es_own[c] != null and n.file != null {
let h = new Node
h.s = g_es_own[c]
ck_tab_put(g_es_ok, g_es_ov, `{n.file}:{itoa(n.line)}`, h)
}
}
s += 1
}
}
@ -578,3 +629,11 @@ function emit_keep(a: Val) -> Val {
}
return a
}
# the state that holds what a site makes, when the analysis ran and found one
function es_owner_at(file: pointer, line: int) -> pointer {
if not g_es_done or file == null { return null }
let h = ck_tab_get(g_es_ok, g_es_ov, `{file}:{itoa(line)}`)
if h == null { return null }
return h.s
}

View file

@ -11,6 +11,8 @@ var g_site_fn: []pointer = new []pointer # per site: the function name's c
var g_site_file: []pointer = new []pointer # the file name's constant
var g_site_line: []int = new []int
var g_site_kind: []pointer = new []pointer # the kind's constant
var g_site_owner: []int = new []int # 25.5c: the state that holds it (index + 1 into g_owner_names, 0: none)
var g_owner_names: []pointer = new []pointer
var g_cur_fn_name: pointer = "?" # the function being emitted
var g_site_fnc: pointer = null # its name's constant, made at its first site
var g_site_filen: pointer = null # the last file a site named, and its constant
@ -38,6 +40,7 @@ var g_es_node: Node = null # the expression being emitted, w
function fence_reset() -> void {
g_site_fn = new []pointer; g_site_file = new []pointer; g_site_line = new []int; g_site_kind = new []pointer
g_site_kinds = new []pointer; g_site_kindc = new []pointer
g_site_owner = new []int; g_owner_names = new []pointer
g_atexit_declared = false
g_mzs_declared = false
g_mmap_declared = false
@ -45,6 +48,7 @@ function fence_reset() -> void {
g_scan_roots = new []pointer
g_cur_fn_name = "?"; g_site_fnc = null; g_site_filen = null; g_site_filec = null; g_site_next = null
push(g_site_fn, null); push(g_site_file, null); push(g_site_line, 0); push(g_site_kind, null) # site 0, named at the end
push(g_site_owner, 0)
}
# the function the next sites belong to (emit_fn, emit_system_fn)
@ -53,6 +57,14 @@ function fence_enter(name: pointer) -> void {
g_site_fnc = null
}
function fence_owner_index(own: pointer) -> int {
if own == null { return 0 }
var i = 0
while i < len(g_owner_names) { if (g_owner_names[i] == own) { return i + 1 }; i += 1 }
push(g_owner_names, own)
return len(g_owner_names)
}
function fence_kind_const(kind: pointer) -> pointer {
var i = 0
while i < len(g_site_kinds) { if (g_site_kinds[i] == kind) { return g_site_kindc[i] }; i += 1 }
@ -77,6 +89,7 @@ function emit_site(kind: pointer) -> void {
push(g_site_file, g_site_filec)
push(g_site_line, g_err_line)
push(g_site_kind, fence_kind_const(k))
push(g_site_owner, fence_owner_index(es_owner_at(file, g_err_line)))
emit(` store i32 {itoa(id)}, ptr @lp_site\n`)
}
@ -234,6 +247,16 @@ function emit_fence_runtime() -> void {
emith(`@lp_site_stamp = global [{sn} x i64] zeroinitializer\n`)
emith(`@lp_site_base = global [{sn} x i64] zeroinitializer\n`)
emith(`@lp_site_scan = global [{sn} x i64] zeroinitializer\n`)
let owns = new []pointer
var w = 0
while w < n { push(owns, itoa(g_site_owner[w])); w += 1 }
fence_table("@lp_site_owner", "i32", owns)
let onames = new []pointer
var q = 0
while q < len(g_owner_names) { push(onames, emit_str_const(g_owner_names[q])); q += 1 }
if len(onames) == 0 { push(onames, "null") }
fence_table("@lp_owner_name", "ptr", onames)
emith(`@lp_nowners = global i32 {itoa(len(g_owner_names))}\n`)
let roots = new []pointer
var r = 0
while r < len(g_scan_roots) { push(roots, `@g_{g_scan_roots[r]}`); r += 1 }

View file

@ -93,6 +93,7 @@ function emit_fence_globals() -> void {
emith("@.lpf_exitscan = private unnamed_addr constant [15 x i8] c\"R3D_ALLOC_EXIT\\00\"\n")
emith("@.lpf_shdr = private unnamed_addr constant [116 x i8] c\"alloc-scan: frame %lld - %lld bytes in %lld blocks made since tracking began are reachable from no global or state\\0A\\00\"\n")
emith("@.lpf_srow = private unnamed_addr constant [25 x i8] c\" %lld B %s %s:%d %s\\0A\\00\"\n")
emith("@.lpf_cown = private unnamed_addr constant [49 x i8] c\"owner %s holds %lld (%+lld since judging began)\\0A\\00\"\n")
emith("@.lpf_failing = private unnamed_addr constant [53 x i8] c\"alloc-fence: failing the run (R3D_ALLOC_FENCE=fail)\\0A\\00\"\n")
emith("@.lpf_census = private unnamed_addr constant [17 x i8] c\"R3D_ALLOC_CENSUS\\00\"\n")
emith("@.lpf_warmmax = private unnamed_addr constant [19 x i8] c\"R3D_ALLOC_WARM_MAX\\00\"\n")
@ -1097,6 +1098,7 @@ function emit_fence_report() -> void {
emith(" call i32 (ptr, ptr, ...) @fprintf(ptr %fp, ptr @.lpf_cvk, i64 %vd1, i64 %vpk)\n")
emith(" %sn = load i32, ptr @lp_site_n\n")
emith(" call void @lp_franked(ptr %fp, ptr @lp_site_base, i1 1, i32 %sn, i32 40)\n")
emith(" call void @lp_fowners(ptr %fp)\n")
emith(" call i32 @fclose(ptr %fp)\n")
emith(" br label %out\n")
emith("out:\n")
@ -1429,6 +1431,56 @@ function emit_fence_frame() -> void {
emith(" call void @lp_funlock()\n")
emith(" ret i64 %lost\n")
emith("}\n")
emith("; 25.5c: the census by owner - what each state holds of what its sites made, and how much it grew\n")
emith("define internal void @lp_fowners(ptr %fp) {\n")
emith("entry:\n")
emith(" %no = load i32, ptr @lp_nowners\n")
emith(" %sn = load i32, ptr @lp_site_n\n")
emith(" br label %ol\n")
emith("ol:\n")
emith(" %o = phi i32 [ 1, %entry ], [ %o1, %onext ]\n")
emith(" %ogo = icmp ule i32 %o, %no\n")
emith(" br i1 %ogo, label %sl0, label %done\n")
emith("sl0:\n")
emith(" br label %sl\n")
emith("sl:\n")
emith(" %s = phi i32 [ 0, %sl0 ], [ %s1, %snext ]\n")
emith(" %live = phi i64 [ 0, %sl0 ], [ %live2, %snext ]\n")
emith(" %grew = phi i64 [ 0, %sl0 ], [ %grew2, %snext ]\n")
emith(" %sgo = icmp ult i32 %s, %sn\n")
emith(" br i1 %sgo, label %sb, label %say\n")
emith("sb:\n")
emith(" %op = getelementptr inbounds i32, ptr @lp_site_owner, i32 %s\n")
emith(" %ow = load i32, ptr %op\n")
emith(" %mine = icmp eq i32 %ow, %o\n")
emith(" %lp = getelementptr inbounds i64, ptr @lp_site_live, i32 %s\n")
emith(" %lv = load i64, ptr %lp\n")
emith(" %bp = getelementptr inbounds i64, ptr @lp_site_base, i32 %s\n")
emith(" %bv = load i64, ptr %bp\n")
emith(" %d = sub i64 %lv, %bv\n")
emith(" %la = select i1 %mine, i64 %lv, i64 0\n")
emith(" %da = select i1 %mine, i64 %d, i64 0\n")
emith(" %live2 = add i64 %live, %la\n")
emith(" %grew2 = add i64 %grew, %da\n")
emith(" br label %snext\n")
emith("snext:\n")
emith(" %s1 = add i32 %s, 1\n")
emith(" br label %sl\n")
emith("say:\n")
emith(" %any = icmp ne i64 %live, 0\n")
emith(" br i1 %any, label %print, label %onext\n")
emith("print:\n")
emith(" %oi = sub i32 %o, 1\n")
emith(" %np = getelementptr inbounds ptr, ptr @lp_owner_name, i32 %oi\n")
emith(" %nm = load ptr, ptr %np\n")
emith(" call i32 (ptr, ptr, ...) @fprintf(ptr %fp, ptr @.lpf_cown, ptr %nm, i64 %live, i64 %grew)\n")
emith(" br label %onext\n")
emith("onext:\n")
emith(" %o1 = add i32 %o, 1\n")
emith(" br label %ol\n")
emith("done:\n")
emith(" ret void\n")
emith("}\n")
emith("define internal void @lp_fsettle_locked() {\n")
emith("entry:\n")
emith(" %n = load i32, ptr @lp_site_n\n")

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff