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:
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;
}