kit

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

commit 8aa842d5dfa4e091671abf3ac05d9f90e3974271
parent e16197de325ce5acfa2ba59d63b6c3e03f2375ea
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Wed, 10 Jun 2026 22:37:33 -0700

perf(pp): drop degenerate hideset vector, batch -E output, pre-size subst buffers

Func-macro expansion was the dominant preprocessor cost. Three byte-identical
wins:
- substitute_body built a parallel hideset TokVec (a 24-byte Tok per body token,
  each memset-zeroed) only to set every slot to the SAME result_hs. Drop it and
  fill hids[i] = result_hs directly, exactly like the object-macro path —
  removing the per-token memset and the vector's grow churn.
- pp_emit_text staged each token through ~2 indirect kit_writer_write calls (a
  function-pointer call into a tiny memcpy per separator + spelling). Batch into
  a 16 KB local buffer flushed in bulk: ~2 indirect calls/token -> ~1 write per
  16 KB.
- subst_phase1/phase2 output vectors grew from cap 0 every expansion
  (0->8->16->32 doubling memcpys); reserve a cheap upper bound (body_len / nin)
  up front.

pp-macro -E 1294->1051ms (-19%). Output byte-identical incl recursive/paste/
stringize/include corpora. Verified: test-pp-ok/err, pp-file-escape,
test-parse-ok/err, test-smoke-x64 all green.

Diffstat:
Mlang/cpp/pp/pp.c | 34+++++++++++++++++++++++++++++-----
Mlang/cpp/pp/pp_expand.c | 27+++++++++++++--------------
2 files changed, 42 insertions(+), 19 deletions(-)

diff --git a/lang/cpp/pp/pp.c b/lang/cpp/pp/pp.c @@ -159,17 +159,40 @@ Tok pp_next(Pp* pp) { * pp_emit_text * ============================================================ */ -static void w_str(Writer* w, const char* s, size_t n) { - if (n) (void)kit_writer_write(w, s, n); +/* Stage output bytes into a caller buffer, flushing to the Writer only when it + * fills (or for a pathologically long spelling). Collapses the former ~2 + * indirect kit_writer_write calls per token — each a function-pointer call into + * a tiny memcpy — into one bulk write per buffer-full. (fdw_write keeps its own + * 64 KB buffer; this second cheap staging layer removes the per-token call + + * bounds-check, not redundant copying of the bulk.) */ +static void pp_emit_stage(Writer* out, char* buf, size_t cap, size_t* on, + const char* s, size_t n) { + if (!n) return; + if (n > cap) { + if (*on) { + (void)kit_writer_write(out, buf, *on); + *on = 0; + } + (void)kit_writer_write(out, s, n); + return; + } + if (*on + n > cap) { + (void)kit_writer_write(out, buf, *on); + *on = 0; + } + memcpy(buf + *on, s, n); + *on += n; } void pp_emit_text(Pp* pp, Writer* out) { + char obuf[16384]; + size_t on = 0; int at_bol = 1; for (;;) { Tok t = pp_next_raw(pp); if (t.kind == TOK_EOF) break; if (t.kind == TOK_NEWLINE) { - w_str(out, "\n", 1); + pp_emit_stage(out, obuf, sizeof obuf, &on, "\n", 1); at_bol = 1; continue; } @@ -178,14 +201,15 @@ void pp_emit_text(Pp* pp, Writer* out) { * had a line break here that the line-tracking cursor isn't * preserving — fall back to a single space so the tokens * don't run together. */ - w_str(out, " ", 1); + pp_emit_stage(out, obuf, sizeof obuf, &on, " ", 1); } if (t.spelling) { KitSlice s = kit_sym_str(pp->pool->c, t.spelling); - w_str(out, s.s, s.len); + pp_emit_stage(out, obuf, sizeof obuf, &on, s.s, s.len); } at_bol = 0; } + if (on) (void)kit_writer_write(out, obuf, on); } /* ============================================================ diff --git a/lang/cpp/pp/pp_expand.c b/lang/cpp/pp/pp_expand.c @@ -720,6 +720,10 @@ static Tok paste_tokens(Pp* pp, Tok lhs, Tok rhs, SrcLoc loc) { static void subst_phase1(Pp* pp, const Macro* m, ArgList* a, const Tok* invoke, TokVec* out) { u32 j; + /* Reserve a cheap upper bound up front so the per-expansion output vector + * doesn't re-grow from cap 0 (0->8->16->32 doubling memcpys); arg expansion + * may exceed it, which just falls back to the doubling. */ + tv_grow(pp, out, m->body_len + 4u); for (j = 0; j < m->body_len; ++j) { const Tok* bt = &m->body[j]; if (bt->kind == TOK_PP_HASH) { @@ -789,6 +793,9 @@ static void subst_phase1(Pp* pp, const Macro* m, ArgList* a, const Tok* invoke, static void subst_phase2(Pp* pp, const Tok* in, u32 nin, const Tok* invoke, TokVec* out) { u32 i; + /* Phase-2 output is at most the input length (paste/placemarker only + * shrink); reserve it to avoid re-growing from cap 0. */ + tv_grow(pp, out, nin); for (i = 0; i < nin; ++i) { Tok t = in[i]; if (t.kind == TOK_PP_PASTE) { @@ -826,8 +833,7 @@ static void subst_phase2(Pp* pp, const Tok* in, u32 nin, const Tok* invoke, /* Wrapper: phases 1 and 2 in sequence, plus invocation-loc / flag transfer. */ static void substitute_body(Pp* pp, const Macro* m, ArgList* a, - const Tok* invoke, HidesetId result_hs, TokVec* out, - TokVec* hs_out) { + const Tok* invoke, TokVec* out) { TokVec phase1 = {0}; u32 i; subst_phase1(pp, m, a, invoke, &phase1); @@ -840,13 +846,6 @@ static void substitute_body(Pp* pp, const Macro* m, ArgList* a, } /* Locations to invocation site. */ for (i = 0; i < out->n; ++i) out->data[i].loc = invoke->loc; - /* Build parallel hideset vector. */ - for (i = 0; i < out->n; ++i) { - Tok hsmark; - memset(&hsmark, 0, sizeof(hsmark)); - hsmark.spelling = (Sym)result_hs; - tv_push(pp, hs_out, hsmark); - } } /* Expand a function-like macro invocation: peek for `(`, collect args, @@ -858,7 +857,6 @@ static int try_expand_func_macro(Pp* pp, const Macro* m, const Tok* invoke, int saw_ws; ArgList args; TokVec body = {0}; - TokVec hsvec = {0}; /* parallel to body, holds HidesetId per slot */ HidesetId result_hs; Tok close_tok; @@ -877,14 +875,15 @@ static int try_expand_func_macro(Pp* pp, const Macro* m, const Tok* invoke, * for the freshly-collected `)` from the lex source that's the empty * set, so the union form suffices here. */ result_hs = hs_add(pp, invoke_hs, m->name); - substitute_body(pp, m, &args, invoke, result_hs, &body, &hsvec); + substitute_body(pp, m, &args, invoke, &body); { u32 i; HidesetId* hids = arena_array(pp->xarena, HidesetId, body.n ? body.n : 1); - for (i = 0; i < body.n; ++i) { - hids[i] = (HidesetId)hsvec.data[i].spelling; - } + /* result_hs is uniform across the entire substituted body, so fill it + * directly — like the object-macro path — instead of materializing a + * parallel hideset token vector. */ + for (i = 0; i < body.n; ++i) hids[i] = result_hs; push_buf(pp, body.data, hids, body.n); } return 1;