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.
This commit is contained in:
2026-06-02 00:08:52 +09:00
parent db5c5b6149
commit 9d383288d2
3 changed files with 81 additions and 24 deletions

View File

@@ -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 -------------------------------------------------------

View File

@@ -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 -------------------------------------------------------

View File

@@ -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 -------------------------------------------------------