From 9d383288d2dcfede923d1fe702695a914319b3bb Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 2 Jun 2026 00:08:52 +0900 Subject: [PATCH] lib/ww: hash-index the tinfo cache, kills O(n2) compile (perf) tinfocachelookup walked a flat prepend-only association list on every cache miss -> O(N) scan x O(N) calls = O(N2) (91% of all wwstage instructions on a 5k-line input; w6c_ww ~265x slower than its C twin). Replace the single list head with a node-ptr hash index, mirroring sym.ww scope.buckets (rule-12): NBUCKETS_TINFO=8192 power-of-two buckets, ptr hashed via (key>>4)&(N-1) (>>4 drops the always-zero aligned low bits so buckets don't cluster), cnext now chains within a bucket. First-match-in-bucket preserves the old most-recent-bind-wins order -> identical *tinfo per node -> byte-identical asm. cstage (cmd/wcc C) has no such cache, so this is wwstage-internal: no emitted-asm change, no cstage-symmetry obligation. Verified byte-identical output (baseline vs new binary, same 32k-line input) and 52.6s -> 0.54s (~97x). combined.ww regenerated for w6c + wwdump (only tools embedding typ.ww). test-unit (235) + smoke green. --- lib/ww/typ.ww | 35 +++++++++++++++++++++------- selfhost/cmd/w6c/main.combined.ww | 35 +++++++++++++++++++++------- selfhost/cmd/wwdump/main.combined.ww | 35 +++++++++++++++++++++------- 3 files changed, 81 insertions(+), 24 deletions(-) diff --git a/lib/ww/typ.ww b/lib/ww/typ.ww index 0676bbaf..09412079 100644 --- a/lib/ww/typ.ww +++ b/lib/ww/typ.ww @@ -139,16 +139,25 @@ type tinfo = struct { }; // #61 audit §1.8 / Rob+Drew convergence 2026-05-20: memoizes -// tinfofornode lookups keyed by AST pointer. Linked-list shape mirrors -// other wwstage-side caches (cgen.aliases, cgen.structs) — sea-of-stars -// over hash-table cleverness, and Sym/Scope already pay the FNV cost -// for the resolver pass. +// tinfofornode lookups keyed by AST pointer. perf #18: the original +// flat prepend-only list made the cache-MISS scan O(N) per call -> +// O(N2) over a compile (91% of all wwstage instructions on a 5k-line +// input). Now a node-ptr hash index, mirroring sym.ww scope.buckets +// (rule-12): cnext chains WITHIN a bucket; lookup/bind hash then touch +// only one bucket -> O(1) amortized. Identical lookup results (same +// *tinfo for the same node), so emitted asm is byte-identical. type tinfocacheent = struct { key: *node, val: *tinfo, cnext: *tinfocacheent, }; +// Tuning knob, NOT a type size (rule-13 N/A): power-of-two so the +// bucket index is a MASK, not a mod. ~5400 nodes on a big input -> +// well under one entry/bucket. Mirror of sym.ww:38 NBUCKETS (16), +// scaled up — sym's 16 would give ~340-deep chains here. +def NBUCKETS_TINFO: u64 = 8192u64; + // ---- tctx — the box of primitive types ------------------------------- type tctx = struct { @@ -179,7 +188,7 @@ type tctx = struct { tyuntypedrune: *tinfo, tyuntypedbool: *tinfo, tyuntypednil: *tinfo, - tinfocache: *tinfocacheent, + tinfobuckets: **tinfocacheent, // length NBUCKETS_TINFO; node-ptr hash index }; // ---- constructors ----------------------------------------------------- @@ -244,6 +253,8 @@ export fn typesinit(c: *tctx) void = { c.tyuntypedrune = prim(tykind.TY_UNTYPED_RUNE, "untyped_rune", 0u64, 1u64); c.tyuntypedbool = prim(tykind.TY_UNTYPED_BOOL, "untyped_bool", 0u64, 1u64); c.tyuntypednil = prim(tykind.TY_UNTYPED_NIL, "untyped_nil", 0u64, 1u64); + let tib: []*tinfocacheent = alloc([], NBUCKETS_TINFO)!; // mirror sym.ww:63 + c.tinfobuckets = tib.ptr; }; export fn typeptr(sub: *tinfo) *tinfo = { @@ -308,8 +319,15 @@ export fn typenamed(name: str, under: *tinfo) *tinfo = { // independent entries that both resolve to c.tyi32. Used by // tinfofornode in check.ww; cgen still reads sizes via primtypesize // until A.2+ graduates each walker family. +// Node ptrs are 8+-aligned, so the low 3-4 bits are always zero — +// shift right 4 before masking or every 16th bucket would cluster. +fn tinfobucket(key: *node) u64 = { + return ((key: u64) >> 4u64) & (NBUCKETS_TINFO - 1u64); +}; + export fn tinfocachelookup(c: *tctx, key: *node) *tinfo = { - let e: *tinfocacheent = c.tinfocache; + let bi: u64 = tinfobucket(key); + let e: *tinfocacheent = c.tinfobuckets[bi]; for (e != nil) { if (e.key == key) { return e.val; }; e = e.cnext; @@ -318,8 +336,9 @@ export fn tinfocachelookup(c: *tctx, key: *node) *tinfo = { }; export fn tinfocachebind(c: *tctx, key: *node, val: *tinfo) void = { - let e: *tinfocacheent = alloc(tinfocacheent{key=key, val=val, cnext=c.tinfocache})!; - c.tinfocache = e; + let bi: u64 = tinfobucket(key); + let e: *tinfocacheent = alloc(tinfocacheent{key=key, val=val, cnext=c.tinfobuckets[bi]})!; + c.tinfobuckets[bi] = e; }; // ---- predicates ------------------------------------------------------- diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 4d69f4c7..c300308c 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -9667,16 +9667,25 @@ type tinfo = struct { }; // #61 audit §1.8 / Rob+Drew convergence 2026-05-20: memoizes -// tinfofornode lookups keyed by AST pointer. Linked-list shape mirrors -// other wwstage-side caches (cgen.aliases, cgen.structs) — sea-of-stars -// over hash-table cleverness, and Sym/Scope already pay the FNV cost -// for the resolver pass. +// tinfofornode lookups keyed by AST pointer. perf #18: the original +// flat prepend-only list made the cache-MISS scan O(N) per call -> +// O(N2) over a compile (91% of all wwstage instructions on a 5k-line +// input). Now a node-ptr hash index, mirroring sym.ww scope.buckets +// (rule-12): cnext chains WITHIN a bucket; lookup/bind hash then touch +// only one bucket -> O(1) amortized. Identical lookup results (same +// *tinfo for the same node), so emitted asm is byte-identical. type tinfocacheent = struct { key: *node, val: *tinfo, cnext: *tinfocacheent, }; +// Tuning knob, NOT a type size (rule-13 N/A): power-of-two so the +// bucket index is a MASK, not a mod. ~5400 nodes on a big input -> +// well under one entry/bucket. Mirror of sym.ww:38 NBUCKETS (16), +// scaled up — sym's 16 would give ~340-deep chains here. +def NBUCKETS_TINFO: u64 = 8192u64; + // ---- tctx — the box of primitive types ------------------------------- type tctx = struct { @@ -9707,7 +9716,7 @@ type tctx = struct { tyuntypedrune: *tinfo, tyuntypedbool: *tinfo, tyuntypednil: *tinfo, - tinfocache: *tinfocacheent, + tinfobuckets: **tinfocacheent, // length NBUCKETS_TINFO; node-ptr hash index }; // ---- constructors ----------------------------------------------------- @@ -9772,6 +9781,8 @@ export fn typesinit(c: *tctx) void = { c.tyuntypedrune = prim(tykind.TY_UNTYPED_RUNE, "untyped_rune", 0u64, 1u64); c.tyuntypedbool = prim(tykind.TY_UNTYPED_BOOL, "untyped_bool", 0u64, 1u64); c.tyuntypednil = prim(tykind.TY_UNTYPED_NIL, "untyped_nil", 0u64, 1u64); + let tib: []*tinfocacheent = alloc([], NBUCKETS_TINFO)!; // mirror sym.ww:63 + c.tinfobuckets = tib.ptr; }; export fn typeptr(sub: *tinfo) *tinfo = { @@ -9836,8 +9847,15 @@ export fn typenamed(name: str, under: *tinfo) *tinfo = { // independent entries that both resolve to c.tyi32. Used by // tinfofornode in check.ww; cgen still reads sizes via primtypesize // until A.2+ graduates each walker family. +// Node ptrs are 8+-aligned, so the low 3-4 bits are always zero — +// shift right 4 before masking or every 16th bucket would cluster. +fn tinfobucket(key: *node) u64 = { + return ((key: u64) >> 4u64) & (NBUCKETS_TINFO - 1u64); +}; + export fn tinfocachelookup(c: *tctx, key: *node) *tinfo = { - let e: *tinfocacheent = c.tinfocache; + let bi: u64 = tinfobucket(key); + let e: *tinfocacheent = c.tinfobuckets[bi]; for (e != nil) { if (e.key == key) { return e.val; }; e = e.cnext; @@ -9846,8 +9864,9 @@ export fn tinfocachelookup(c: *tctx, key: *node) *tinfo = { }; export fn tinfocachebind(c: *tctx, key: *node, val: *tinfo) void = { - let e: *tinfocacheent = alloc(tinfocacheent{key=key, val=val, cnext=c.tinfocache})!; - c.tinfocache = e; + let bi: u64 = tinfobucket(key); + let e: *tinfocacheent = alloc(tinfocacheent{key=key, val=val, cnext=c.tinfobuckets[bi]})!; + c.tinfobuckets[bi] = e; }; // ---- predicates ------------------------------------------------------- diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index b871160b..e5014765 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -9667,16 +9667,25 @@ type tinfo = struct { }; // #61 audit §1.8 / Rob+Drew convergence 2026-05-20: memoizes -// tinfofornode lookups keyed by AST pointer. Linked-list shape mirrors -// other wwstage-side caches (cgen.aliases, cgen.structs) — sea-of-stars -// over hash-table cleverness, and Sym/Scope already pay the FNV cost -// for the resolver pass. +// tinfofornode lookups keyed by AST pointer. perf #18: the original +// flat prepend-only list made the cache-MISS scan O(N) per call -> +// O(N2) over a compile (91% of all wwstage instructions on a 5k-line +// input). Now a node-ptr hash index, mirroring sym.ww scope.buckets +// (rule-12): cnext chains WITHIN a bucket; lookup/bind hash then touch +// only one bucket -> O(1) amortized. Identical lookup results (same +// *tinfo for the same node), so emitted asm is byte-identical. type tinfocacheent = struct { key: *node, val: *tinfo, cnext: *tinfocacheent, }; +// Tuning knob, NOT a type size (rule-13 N/A): power-of-two so the +// bucket index is a MASK, not a mod. ~5400 nodes on a big input -> +// well under one entry/bucket. Mirror of sym.ww:38 NBUCKETS (16), +// scaled up — sym's 16 would give ~340-deep chains here. +def NBUCKETS_TINFO: u64 = 8192u64; + // ---- tctx — the box of primitive types ------------------------------- type tctx = struct { @@ -9707,7 +9716,7 @@ type tctx = struct { tyuntypedrune: *tinfo, tyuntypedbool: *tinfo, tyuntypednil: *tinfo, - tinfocache: *tinfocacheent, + tinfobuckets: **tinfocacheent, // length NBUCKETS_TINFO; node-ptr hash index }; // ---- constructors ----------------------------------------------------- @@ -9772,6 +9781,8 @@ export fn typesinit(c: *tctx) void = { c.tyuntypedrune = prim(tykind.TY_UNTYPED_RUNE, "untyped_rune", 0u64, 1u64); c.tyuntypedbool = prim(tykind.TY_UNTYPED_BOOL, "untyped_bool", 0u64, 1u64); c.tyuntypednil = prim(tykind.TY_UNTYPED_NIL, "untyped_nil", 0u64, 1u64); + let tib: []*tinfocacheent = alloc([], NBUCKETS_TINFO)!; // mirror sym.ww:63 + c.tinfobuckets = tib.ptr; }; export fn typeptr(sub: *tinfo) *tinfo = { @@ -9836,8 +9847,15 @@ export fn typenamed(name: str, under: *tinfo) *tinfo = { // independent entries that both resolve to c.tyi32. Used by // tinfofornode in check.ww; cgen still reads sizes via primtypesize // until A.2+ graduates each walker family. +// Node ptrs are 8+-aligned, so the low 3-4 bits are always zero — +// shift right 4 before masking or every 16th bucket would cluster. +fn tinfobucket(key: *node) u64 = { + return ((key: u64) >> 4u64) & (NBUCKETS_TINFO - 1u64); +}; + export fn tinfocachelookup(c: *tctx, key: *node) *tinfo = { - let e: *tinfocacheent = c.tinfocache; + let bi: u64 = tinfobucket(key); + let e: *tinfocacheent = c.tinfobuckets[bi]; for (e != nil) { if (e.key == key) { return e.val; }; e = e.cnext; @@ -9846,8 +9864,9 @@ export fn tinfocachelookup(c: *tctx, key: *node) *tinfo = { }; export fn tinfocachebind(c: *tctx, key: *node, val: *tinfo) void = { - let e: *tinfocacheent = alloc(tinfocacheent{key=key, val=val, cnext=c.tinfocache})!; - c.tinfocache = e; + let bi: u64 = tinfobucket(key); + let e: *tinfocacheent = alloc(tinfocacheent{key=key, val=val, cnext=c.tinfobuckets[bi]})!; + c.tinfobuckets[bi] = e; }; // ---- predicates -------------------------------------------------------