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