kit

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

commit a753147bfd561abf78e6b010eeaaad3d3950b114
parent 503c2d59c817f84c69b267fa9747cdc6f2170b74
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Fri, 12 Jun 2026 14:26:06 -0700

perf(pp): multiple-include optimization (controlling #ifndef guard + #pragma once)

Record each header's controlling #ifndef macro (and #pragma once) as it is
first lexed, and skip re-lexing it entirely on a later #include while that
macro is defined -- the classic multiple-include optimization tcc/gcc/clang
have and kit lacked. kit's IncCache already served the bytes syscall-free, but
re-lexing them through skip_until_active still cost a full lex+intern of every
token of the guarded body, producing nothing.

Detection reuses the real lexer/pp as the file is first lexed (no second scan,
no divergence risk): a per-file 4-state machine (START->IN on the opening
'#ifndef GUARD' at the file's base if-depth; IN->AFTER on the matching #endif;
any code before the opening directive or after the controlling #endif ->
FAILED). Committed to the IncCache at src_pop; consulted at the next #include of
the same resolved path. A reading_directive flag keeps the directive's own
tokens (the 'ifndef'/macro name) from counting as file content.

sqlite amalgamation (arm64-macos, instructions, best-of-7): of 806 include
events, 502 now skipped (sys/cdefs.h was re-lexed 77x, now once). -E 1.42B ->
1.06B (-25.5%), -fsyntax-only 2.35B -> 1.98B (-15.5%), -c 2.83B -> 2.45B
(-13.2%). tcc gap 4.2x -> 3.66x.

Parser token stream is byte-identical (non-blank -E matches exactly; sqlite
compiles+runs 84|2; test-pp 110/0). The only -E text change is fewer blank
lines for skipped re-includes, matching gcc/clang (the compile path ignores
blank lines).

Diffstat:
Mlang/cpp/pp/pp.c | 23+++++++++++++++++++++++
Mlang/cpp/pp/pp_directive.c | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlang/cpp/pp/pp_priv.h | 35+++++++++++++++++++++++++++++++++++
3 files changed, 120 insertions(+), 0 deletions(-)

diff --git a/lang/cpp/pp/pp.c b/lang/cpp/pp/pp.c @@ -40,6 +40,18 @@ void src_pop(Pp* pp) { if (!pp->nsources) return; t = &pp->sources[pp->nsources - 1]; if (t->kind == SRC_LEX && t->lex) { + /* Commit the multiple-include memo for this file before closing it: a + * fully #ifndef-wrapped file records its controlling macro; a #pragma once + * file records the once flag. A later #include of the same resolved path + * then skips re-lexing it entirely (see do_include). */ + if (t->path_key && pp->inc_cache_ready) { + IncEntry* e = IncCache_get(&pp->inc_cache, t->path_key); + if (e) { + if (t->once) e->once = 1; + if (t->guard_state == GUARD_AFTER && t->guard_macro) + e->guard = t->guard_macro; + } + } lex_close(t->lex); t->lex = NULL; } @@ -94,6 +106,16 @@ void src_next_raw_into(Pp* pp, Tok* out, HidesetId* hs_out, u8* src_kind_out) { if (src_kind_out) *src_kind_out = SRC_LEX; return; } + /* Multiple-include-guard detection: real code before the opening guard + * directive, or after the controlling #endif, disqualifies the whole-file + * #ifndef guard. A newline is whitespace and a beginning-of-line `#` is a + * directive introducer — both transparent; anything else is content. */ + if (!pp->reading_directive && + (s->guard_state == GUARD_START || s->guard_state == GUARD_AFTER) && + out->kind != TOK_NEWLINE && + !(out->kind == TOK_PP_HASH && (out->flags & TF_AT_BOL))) { + s->guard_state = GUARD_FAILED; + } /* Apply #line line-number delta on the way out so the rest of * the pipeline sees user-visible line numbers (matters for * __LINE__ expansion and for line-tracking output cursors). */ @@ -273,6 +295,7 @@ static void pp_intern_keywords(Pp* pp) { pp->sym_endif = kit_sym_intern(p->c, KIT_SLICE_LIT("endif")); pp->sym_line = kit_sym_intern(p->c, KIT_SLICE_LIT("line")); pp->sym_pragma = kit_sym_intern(p->c, KIT_SLICE_LIT("pragma")); + pp->sym_once = kit_sym_intern(p->c, KIT_SLICE_LIT("once")); pp->sym_pragma_kw = pp->sym_pragma; pp->sym_error = kit_sym_intern(p->c, KIT_SLICE_LIT("error")); pp->sym_warning = kit_sym_intern(p->c, KIT_SLICE_LIT("warning")); diff --git a/lang/cpp/pp/pp_directive.c b/lang/cpp/pp/pp_directive.c @@ -41,6 +41,8 @@ void read_directive_line(Pp* pp, Tok** out_toks, u32* out_n) { u32 cap = 0, n = 0; Tok t; HidesetId hs; + u8 saved_rd = pp->reading_directive; + pp->reading_directive = 1; for (;;) { t = src_next_raw(pp, &hs, NULL); if (t.kind == TOK_NEWLINE || t.kind == TOK_EOF) break; @@ -53,6 +55,7 @@ void read_directive_line(Pp* pp, Tok** out_toks, u32* out_n) { } buf[n++] = t; } + pp->reading_directive = saved_rd; *out_toks = buf; *out_n = n; } @@ -1107,6 +1110,7 @@ static void do_include(Pp* pp, const Tok* line, u32 n, SrcLoc loc, u32 i; int found; TokSrc s; + Sym pkey; parse_include_path(pp, line, n, loc, path, sizeof(path), &system_form); @@ -1136,6 +1140,18 @@ static void do_include(Pp* pp, const Tok* line, u32 n, SrcLoc loc, KIT_SLICE_ARG(kit_slice_cstr(path))); } + /* Multiple-include optimization: if this resolved file was already lexed and + * is either #pragma once or still wrapped by a defined #ifndef guard, skip + * re-lexing it entirely — it would emit no tokens anyway. This is the classic + * controlling-macro optimization (tcc/gcc/clang have it); kit's IncCache + * already serves the bytes syscall-free, but re-lexing them through + * skip_until_active still costs the full lex+intern of every token. */ + pkey = inc_cache_key(pp, resolved); + if (pkey && pp->inc_cache_ready) { + IncEntry* e = IncCache_get(&pp->inc_cache, pkey); + if (e && (e->once || (e->guard && mt_get(pp, e->guard) != NULL))) return; + } + lex = lex_open_mem(pp->c, resolved, (const char*)data, size); included_id = lex_file_id(lex); @@ -1143,6 +1159,8 @@ static void do_include(Pp* pp, const Tok* line, u32 n, SrcLoc loc, s.kind = SRC_LEX; s.lex = lex; s.inc_next_start = next_start; + s.path_key = pkey; + s.guard_if_base = pp->ifstk_n; src_push(pp, s); kit_source_add_include(pp->c, includer_id, included_id, loc, system_form, @@ -1330,6 +1348,12 @@ static void handle_pragma_pack(Pp* pp, const Tok* line, u32 n, SrcLoc loc) { } static void do_pragma(Pp* pp, const Tok* line, u32 n, SrcLoc loc) { + /* #pragma once: mark the current file include-once for the multiple-include + * optimization (see do_include). Still forwarded to output like any pragma. */ + if (n >= 1 && line[0].kind == TOK_IDENT && line[0].v.ident == pp->sym_once) { + TokSrc* gls = current_lex_src(pp); + if (gls) gls->once = 1; + } /* Forward unrecognised pragmas to the output. STDC pragmas pass * through too; we don't act on them yet. */ handle_pragma_pack(pp, line, n, loc); @@ -1654,6 +1678,30 @@ void process_directive(Pp* pp, SrcLoc hash_loc) { compiler_panic(pp->c, line[0].loc, "expected directive name after '#'"); } name = line[0].v.ident; + + /* Multiple-include-guard state machine, run on the current file source + * before dispatch. The only directive that may open a whole-file guard is + * `#ifndef MACRO` as the very first directive (nothing but whitespace before + * it, at the file's base if-depth); any other directive seen at START, or any + * directive at all after the controlling #endif (AFTER), disqualifies it. + * #pragma is exempt — #pragma once is its own (orthogonal) mechanism. */ + { + TokSrc* gls = current_lex_src(pp); + if (gls && name != pp->sym_pragma) { + if (gls->guard_state == GUARD_START) { + if (name == pp->sym_ifndef && n >= 2 && line[1].kind == TOK_IDENT && + pp->ifstk_n == gls->guard_if_base) { + gls->guard_macro = line[1].v.ident; + gls->guard_state = GUARD_IN; + } else { + gls->guard_state = GUARD_FAILED; + } + } else if (gls->guard_state == GUARD_AFTER) { + gls->guard_state = GUARD_FAILED; + } + } + } + if (name == pp->sym_define) do_define(pp, line + 1, n - 1); else if (name == pp->sym_undef) @@ -1687,4 +1735,18 @@ void process_directive(Pp* pp, SrcLoc hash_loc) { else { compiler_panic(pp->c, line[0].loc, "unsupported directive"); } + + /* The controlling #endif (the one returning the if-stack to the file's base + * depth) closes a candidate guard. After this only whitespace/newlines may + * follow for the guard to hold; any further token/directive flips it to + * FAILED (see the START/AFTER handling above and the content check in + * src_next_raw_into). do_include/do_undef etc. do not change current_lex_src + * out from under us here because this only fires for #endif. */ + if (name == pp->sym_endif) { + TokSrc* gls = current_lex_src(pp); + if (gls && gls->guard_state == GUARD_IN && + pp->ifstk_n == gls->guard_if_base) { + gls->guard_state = GUARD_AFTER; + } + } } diff --git a/lang/cpp/pp/pp_priv.h b/lang/cpp/pp/pp_priv.h @@ -53,6 +53,21 @@ typedef struct Hideset { typedef enum { SRC_LEX = 1, SRC_BUF = 2 } SrcKind; +/* Multiple-include-guard detection state for a SRC_LEX file (the classic + * "controlling macro" optimization). A file whose entire body is wrapped in + * `#ifndef GUARD ... #endif` (with nothing but whitespace/comments outside) + * can be skipped on a later #include while GUARD is defined — it would emit no + * tokens anyway. Detected live as the file is first lexed (no second scan): + * START -> IN on the opening `#ifndef GUARD`; IN -> AFTER on the matching + * `#endif`; any code before the opening directive or after the controlling + * `#endif` -> FAILED. Committed to the IncCache at src_pop (see pp.c). */ +typedef enum GuardState { + GUARD_START = 0, /* only whitespace/newlines seen so far */ + GUARD_IN = 1, /* inside the controlling #ifndef ... #endif */ + GUARD_AFTER = 2, /* controlling #endif closed; only ws may follow */ + GUARD_FAILED = 3 /* not a whole-file guard */ +} GuardState; + typedef struct TokSrc { u8 kind; /* When set on a SRC_BUF: src_next_raw returns TOK_EOF when this is @@ -88,6 +103,15 @@ typedef struct TokSrc { * ("...") step or an absolute path, so an `#include_next` there scans the * whole search path (matching GCC). */ u32 inc_next_start; + /* Multiple-include-guard detection (SRC_LEX only). guard_if_base is the + * if-stack depth at file entry, used to recognize the controlling #endif; + * path_key is the IncCache key (interned resolved path, 0 for the top-level + * TU and other non-cached sources) used to commit the memo at src_pop. */ + u8 guard_state; /* GuardState */ + u8 once; /* #pragma once seen in this file */ + Sym guard_macro; + u32 guard_if_base; + Sym path_key; } TokSrc; typedef enum IfState { @@ -116,6 +140,11 @@ typedef struct IfFrame { typedef struct IncEntry { const u8* data; size_t size; + /* Multiple-include optimization (committed at src_pop, consulted at the next + * #include of the same resolved path). guard = the file's controlling + * #ifndef macro (0 = none); once = the file carried #pragma once. */ + Sym guard; + u8 once; } IncEntry; static inline u32 inc_hash_(Sym s) { return kit_hash_u32((u32)s); } KIT_HASHMAP_DEFINE(IncCache, Sym, IncEntry, inc_hash_); @@ -223,6 +252,7 @@ struct Pp { Sym sym_endif; Sym sym_line; Sym sym_pragma; + Sym sym_once; /* "once" — #pragma once (multiple-include opt) */ Sym sym_error; Sym sym_warning; Sym sym_embed; @@ -267,6 +297,11 @@ struct Pp { * before the macro-expansion check at the head of pp_next_raw. */ u8 in_if_expansion; u8 defined_skip; + /* Set while read_directive_line is pulling a directive's own tokens from the + * lexer, so the multiple-include-guard content check in src_next_raw_into + * doesn't count directive-internal tokens (e.g. the `ifndef` / macro name of + * the guard itself) as file content. */ + u8 reading_directive; }; /* ============================================================