kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 3e0e8146073711b21af37610484a9ca06dbeffb6
parent 486575cfb77911b5f89ce8f54eb8f3263ff32c96
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 13 Jun 2026 10:09:50 -0700

perf(lex): densify the per-lexer punct-spelling cache โ€” shrink struct Lexer 1208->288B

The punctuator-spelling cache was a Punct-value-indexed Sym[P_HASH_HASH+1]
(~1.1 KB) living inline in struct Lexer. Since lex_open_mem memsets the whole
struct on every opened buffer (~6,000 per sqlite compile, one per #include),
that array was the bulk of lex_open_mem's per-open cost in the profile.

Map each Punct value through a static punct_slot[] table to a compact 1-based
slot and size the cache at PUNCT_SLOTS (49) entries instead of 279. struct
Lexer shrinks 1208->288 B and the per-open memset shrinks 4.2x, while the
per-spelling caching is preserved (same Syms, same sentinel semantics; the
digraph bypass and uncached rare/unknown bytes intern directly as before).

Byte-identical: 60/60 perf identity gate PASS (incE/ppmE/spliceE -E
categories, diagnostics, splice battery, objects/-g/linked exe all match
golden).

Diffstat:
Mlang/cpp/lex/lex.c | 59++++++++++++++++++++++++++++++++++++++++++++++-------------
1 file changed, 46 insertions(+), 13 deletions(-)

diff --git a/lang/cpp/lex/lex.c b/lang/cpp/lex/lex.c @@ -93,6 +93,34 @@ static const u8 cclass[256] = { }; /* clang-format on */ +/* Dense slot for a punctuator's spelling cache. A Punct value indexes + * `punct_slot[]` to a compact 1-based slot; the few dozen distinct + * punctuators map to slots 1..PUNCT_SLOTS-1, and every other value (the + * `default`/unknown-byte path, the digraph-aliased forms) maps to slot 0, + * which is never cached (those intern their bytes directly). This keeps the + * per-lexer cache to PUNCT_SLOTS entries instead of one per Punct value + * (P_HASH_HASH+1 = 279), shrinking `struct Lexer` and its per-open memset by + * ~1 KB while preserving the per-spelling caching. Slot assignment is + * arbitrary but stable; only distinctness matters. */ +#define PUNCT_SLOTS 49u +/* clang-format off */ +static const u8 punct_slot[P_HASH_HASH + 1] = { + /* single-char punctuators (indexed by ASCII codepoint) */ + ['!'] = 1, ['#'] = 2, ['%'] = 3, ['&'] = 4, ['('] = 5, [')'] = 6, + ['*'] = 7, ['+'] = 8, [','] = 9, ['-'] = 10, ['.'] = 11, ['/'] = 12, + [':'] = 13, [';'] = 14, ['<'] = 15, ['='] = 16, ['>'] = 17, ['?'] = 18, + ['['] = 19, [']'] = 20, ['^'] = 21, ['{'] = 22, ['|'] = 23, ['}'] = 24, + ['~'] = 25, + /* multi-char punctuators (P_ARROW..P_HASH_HASH) */ + [P_ARROW] = 26, [P_INC] = 27, [P_DEC] = 28, [P_SHL] = 29, [P_SHR] = 30, + [P_LE] = 31, [P_GE] = 32, [P_EQ] = 33, [P_NE] = 34, [P_AND] = 35, + [P_OR] = 36, [P_ADD_ASSIGN] = 37, [P_SUB_ASSIGN] = 38, [P_MUL_ASSIGN] = 39, + [P_DIV_ASSIGN] = 40, [P_MOD_ASSIGN] = 41, [P_AND_ASSIGN] = 42, + [P_OR_ASSIGN] = 43, [P_XOR_ASSIGN] = 44, [P_SHL_ASSIGN] = 45, + [P_SHR_ASSIGN] = 46, [P_ELLIPSIS] = 47, [P_HASH_HASH] = 48, +}; +/* clang-format on */ + struct Lexer { Compiler* c; Heap* heap; @@ -127,17 +155,19 @@ struct Lexer { u32* splices; u32 nsplices; u32 next_splice; - /* Lazily-interned spelling Sym per punctuator value (0 = not yet interned). - * Punctuators are about half of all tokens but there are only ~60 distinct - * forms, so interning the spelling of every one (as identifiers are) is - * millions of pool hash-probes to rediscover the same handful of entries. - * Cache the Sym the first time each punctuator is seen and reuse it for every - * later occurrence. Indexed directly by the Punct value (single-char puncts - * use their ASCII code, multi-char use P_ARROW..P_HASH_HASH). The digraph - * spellings (<: <% %> :> %: %:%:) are NOT cached here: they share a Punct - * value with their canonical form but spell differently, so they intern - * their source bytes verbatim each time (see the punct scanner). */ - Sym punct_sym[P_HASH_HASH + 1]; + /* Lazily-interned spelling Sym per punctuator, cached so the same ~60 + * distinct punctuator spellings are not re-interned on every occurrence. + * The table is a compact, densely-indexed array (see PUNCT_SLOTS / + * punct_slot): a punctuator's Punct value maps through the static + * `punct_slot[]` table to a small slot, keeping the per-lexer cache to a + * few hundred bytes instead of the ~1.1 KB a Punct-value-indexed array + * cost โ€” the bulk of `struct Lexer` and of lex_open_mem's per-open memset. + * Entry value 0 is the reserved pool "none" sym, an unambiguous + * not-yet-interned sentinel. The digraph spellings (<: <% %> :> %: %:%:) + * are NOT cached here: they share a Punct value with their canonical form + * but spell differently, so they intern their source bytes verbatim each + * time (see the punct scanner). */ + Sym punct_sym[PUNCT_SLOTS]; }; /* ยง5.1.1.2 translation phase 2: fold every `\<newline>` line splice out of the @@ -358,8 +388,11 @@ static Sym lex_intern(Lexer* l, const char* a, const char* b) { * the caller: they share a Punct value with their canonical form but spell * differently, so they must intern their source bytes verbatim. */ static Sym punct_spelling(Lexer* l, u32 punct, const char* a, const char* b) { - Sym s = l->punct_sym[punct]; - if (!s) s = l->punct_sym[punct] = lex_intern(l, a, b); + u32 slot = punct_slot[punct]; + Sym s; + if (!slot) return lex_intern(l, a, b); /* uncached value: intern directly */ + s = l->punct_sym[slot]; + if (!s) s = l->punct_sym[slot] = lex_intern(l, a, b); return s; }