kit

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

commit c89c5909d47ae0b854d9018f0bec63db7c559277
parent 9c3ce7acc8fe15631b220d004e0e5406340fa775
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu, 11 Jun 2026 09:43:56 -0700

perf: byte-identical -O0 throughput sprint (9 segments, +10–15%)

Constant-factor wins across every compile segment, each gated
byte-identical to the pre-change binary (objects -O0/-O1, -g DWARF,
-S, -E, diagnostics, linked exe, line-splice battery — see
scripts/perf_identity_gate.sh) and validated against the full test
suites. Measured (best-of-11, integrated kit vs HEAD golden):
type-decl +15.0%, fn-count +10.8%, body-size +10.3%, pp-macro -E
+8.5%, ref-density +7.1%, global-decl +4.8%, locals +3.0%.

Per segment:
- intern (core/pool): inline sym_eq + miss-path copy to drop the
  freestanding memcmp/memcpy libcalls; precompute the grow threshold.
- codegen-dispatch (cg/arith,value): cache a 1-byte wide_kind tag on
  the value-stack node (set once in api_push); the per-binop i128/
  wide64/f128 stack-top gauntlet becomes a load+compare. Biggest
  body-size lever. Also CSE the foldable-int can_delay check.
- codegen-regcache (cg/native_direct_target): flat reg_last_use[cls]
  [reg] mirror for the LRU victim scan + precomputed caller-saved-
  allocable sublist for the free-reg scan (~+2.2% body-size).
- parser (lang/c/parse): fuse reject_same_scope_redefinition +
  scope_define into one hash probe (NAME##_replace); O(1) keyword
  classification (KwMap) replacing the ~52-entry linear scan; drop a
  redundant current-scope lookup in declare_function.
- lexer (cpp/lex): drop the per-token memset in lex_next (+ the two
  src_next_raw EOF fallbacks); splice-free fast-path ident scan
  (~+6% type-decl).
- pp (cpp/pp): out-pointer token readers (pp_next_raw_into /
  src_next_raw_into) killing the 24B sret round-trip; inline the
  SRC_BUF replay fast path; skip subst_phase2 placemarker compaction
  when none emitted (~+8.5% pp-macro -E).
- emit (core/buf, obj, arch/mc): inline buf_write small-write fast
  path (buf_write_slow tail); cache the active section Buf* on the
  MCEmitter; pre-size SymNameIndex.
- memset-abi (arch/{aa64,x64,riscv}/native, mc): right-size the
  NativeCallPlanRet scratch in plan_ret/plan_call (skip the alloc on
  the sret path; size to nparts); drop the dead labels_grow tail
  memset.
- include-io (cpp/pp): resolved-path header-content cache + include-
  resolution memo (collapses re-opens of repeatedly-included headers;
  helps real header graphs, not the single-include synthetic axis).

Diffstat:
Minclude/kit/support/hashmap.h | 31+++++++++++++++++++++++++++++++
Mlang/c/parse/parse.c | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mlang/c/parse/parse_priv.h | 26++++++++++++++------------
Mlang/cpp/lex/lex.c | 45++++++++++++++++++++++++++++++++++++++++-----
Mlang/cpp/pp/pp.c | 73+++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mlang/cpp/pp/pp_directive.c | 147++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mlang/cpp/pp/pp_expand.c | 125++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mlang/cpp/pp/pp_priv.h | 49++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/arch/aa64/native.c | 11+++++++++--
Msrc/arch/mc.c | 17+++++++++++++++--
Msrc/arch/mc.h | 7+++++++
Msrc/arch/riscv/native.c | 8+++++++-
Msrc/arch/x64/native.c | 8+++++++-
Msrc/cg/arith.c | 56++++++++++++++++++++++++++++++++------------------------
Msrc/cg/internal.h | 30++++++++++++++++++++++++++++--
Msrc/cg/native_direct_target.c | 49+++++++++++++++++++++++++++++++++++++------------
Msrc/cg/native_direct_target.h | 16++++++++++++++++
Msrc/cg/value.c | 67++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/core/buf.c | 5++++-
Msrc/core/buf.h | 22+++++++++++++++++++++-
Msrc/core/pool.c | 34+++++++++++++++++++++++++++++++---
Msrc/core/pool.h | 1+
Msrc/obj/obj.c | 19++++++++++++++++++-
Msrc/obj/obj.h | 6++++++
24 files changed, 795 insertions(+), 162 deletions(-)

diff --git a/include/kit/support/hashmap.h b/include/kit/support/hashmap.h @@ -122,6 +122,37 @@ static inline uint32_t kit_hash_u64(uint64_t x) { return 1; \ } \ \ + /* Like NAME##_set (newest writer wins) but reports the prior value: writes \ + * the old value to *old_out on overwrite, or a zero-initialized VT on \ + * insert. Lets a caller fuse a read-then-write of the same key into one \ + * hash+probe (e.g. the redefinition check + define in the C parser). */ \ + KIT_HASHMAP_UNUSED static inline int NAME##_replace(NAME* m, KT k, VT v, \ + VT* old_out) { \ + uint32_t mask, j; \ + if (m->cap == 0 || \ + m->used * KIT_HASHMAP_LOAD_DEN >= m->cap * KIT_HASHMAP_LOAD_NUM) \ + NAME##_resize(m, m->cap ? m->cap * 2u : KIT_HASHMAP_INIT_CAP); \ + if (m->cap == 0) { \ + if (old_out) memset(old_out, 0, sizeof *old_out); \ + return -1; /* resize OOM'd from empty: avoid NULL deref */ \ + } \ + mask = m->cap - 1u; \ + j = HASH_FN(k) & mask; \ + while (m->slots[j].k) { \ + if (m->slots[j].k == (k)) { \ + if (old_out) *old_out = m->slots[j].v; \ + m->slots[j].v = (v); \ + return 0; \ + } \ + j = (j + 1u) & mask; \ + } \ + if (old_out) memset(old_out, 0, sizeof *old_out); \ + m->slots[j].k = (k); \ + m->slots[j].v = (v); \ + m->used++; \ + return 1; \ + } \ + \ KIT_HASHMAP_UNUSED static inline int NAME##_try_insert(NAME* m, KT k, VT v, \ VT* existing_out) { \ uint32_t mask, j; \ diff --git a/lang/c/parse/parse.c b/lang/c/parse/parse.c @@ -363,15 +363,22 @@ SymEntry* scope_lookup_current(Parser* p, Sym name) { return p->scope ? scope_entries_find(p->scope, name) : NULL; } -SymEntry* scope_lookup(Parser* p, Sym name) { +/* Walk the scope chain starting at `from` (inclusive), returning the first + * binding for `name`. scope_lookup is the common case starting at p->scope; a + * caller that has already probed the current scope can start at its parent. */ +static SymEntry* scope_lookup_from(Scope* from, Sym name) { Scope* s; - for (s = p->scope; s; s = s->parent) { + for (s = from; s; s = s->parent) { SymEntry* e = scope_entries_find(s, name); if (e) return e; } return NULL; } +SymEntry* scope_lookup(Parser* p, Sym name) { + return scope_lookup_from(p->scope, name); +} + static void sym_set_decl(SymEntry* e, DeclId id, DeclStorage storage, DeclLinkage linkage, u32 flags, DeclState state) { e->decl_id = id; @@ -397,18 +404,62 @@ static int is_ordinary_decl_kind(SymEntryKind k) { k == SEK_TYPEDEF || k == SEK_ENUM_CST; } -static void reject_same_scope_redefinition(Parser* p, Sym name, - SymEntryKind kind, - const Type* type) { - SymEntry* e = scope_lookup_current(p, name); - if (!e || !is_ordinary_decl_kind((SymEntryKind)e->kind)) return; - if (e->kind == SEK_TYPEDEF && kind == SEK_TYPEDEF && - type_compatible(e->type, type)) { +/* Apply the same-scope redefinition rule against `prior` (the binding present + * before the new entry is installed): reject an ordinary-kind redeclaration + * unless it is a typedef of a compatible type. */ +static void reject_redef_on(Parser* p, const SymEntry* prior, SymEntryKind kind, + const Type* type) { + if (!prior || !is_ordinary_decl_kind((SymEntryKind)prior->kind)) return; + if (prior->kind == SEK_TYPEDEF && kind == SEK_TYPEDEF && + type_compatible(prior->type, type)) { return; } perr(p, "redefinition of identifier"); } +/* Fused redefinition-check + define: one hash+probe on the scope index reads + * the prior binding (for the redef rule) AND installs the new entry, instead of + * probing the same key twice (a get for the redef check then a set in + * scope_define). Behavior is identical to a redef check followed by + * return scope_define(p, name, kind, type); + * — the redef rule fires on the captured prior before the new entry shadows it, + * and the LIFO list + newest-wins index semantics are preserved. Use this only + * at the rejecting declaration sites; plain scope_define stays for the + * non-rejecting redeclaration-merge sites. */ +static SymEntry* scope_define_checked(Parser* p, Sym name, SymEntryKind kind, + const Type* type) { + SymEntry* e = arena_new(p->pool->arena, SymEntry); + Scope* s = p->scope; + SymEntry* prior; + if (!e) perr(p, "out of memory in scope_define_checked"); + memset(e, 0, sizeof *e); + e->name = name; + e->kind = (u8)kind; + e->type = type; + if (name && s->emap.cap) { + /* Active index: prepend to the LIFO list, then fold the prior-read and the + * index install into a single probe (newest define wins). */ + SymEntry* old = NULL; + e->next = s->entries; + s->entries = e; + s->nentries++; + (void)SymEntryMap_replace(&s->emap, name, e, &old); + prior = old; + } else { + /* No index yet (or anonymous): capture the prior binding before the prepend + * shadows it, then mirror scope_entries_index_put's threshold-trip build. + * Anonymous entries (name == 0) stay off the index, as before. */ + prior = scope_entries_find(s, name); + e->next = s->entries; + s->entries = e; + s->nentries++; + if (name && s->nentries > SCOPE_INDEX_THRESHOLD) + scope_entries_index_build(p, s); + } + reject_redef_on(p, prior, kind, type); + return e; +} + static void tag_index_build(Parser* p, Scope* s) { TagEntry* e; TagEntryMap_init_cap(&s->tmap, &p->pool->arena_heap, 64u); @@ -493,8 +544,7 @@ FrameSlot make_local_aligned(Parser* p, Sym name, const Type* type, SrcLoc loc, fsd.kind = FS_LOCAL; fsd.flags = FSF_NONE; s = pcg_local(p, &fsd); - reject_same_scope_redefinition(p, name, SEK_LOCAL, type); - e = scope_define(p, name, SEK_LOCAL, type); + e = scope_define_checked(p, name, SEK_LOCAL, type); e->v.slot = s; sym_set_decl(e, DECL_NONE, DS_AUTO, DL_NONE, DF_NONE, DSTATE_DEFINED); return s; @@ -754,8 +804,7 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) { perr(p, "typedef declarator cannot have initializer"); } { - reject_same_scope_redefinition(p, name, SEK_TYPEDEF, var_ty); - SymEntry* e = scope_define(p, name, SEK_TYPEDEF, var_ty); + SymEntry* e = scope_define_checked(p, name, SEK_TYPEDEF, var_ty); sym_set_decl(e, DECL_NONE, DS_TYPEDEF, DL_NONE, specs->flags, DSTATE_DECLARED); if (p->vla_pending && var_ty && var_ty->kind == TY_ARRAY) { @@ -822,8 +871,7 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) { attr_list_to_decl(p->c, p->decls, specs->attrs, &decl_in); did = decl_declare(p->decls, &decl_in); sym = decl_obj_sym(p->decls, did); - reject_same_scope_redefinition(p, name, SEK_GLOBAL, var_ty); - e = scope_define(p, name, SEK_GLOBAL, var_ty); + e = scope_define_checked(p, name, SEK_GLOBAL, var_ty); e->v.sym = sym; sym_set_decl(e, did, DS_STATIC, DL_NONE, decl_in.flags, DSTATE_DEFINED); align_eff = (specs->align > decl_in.align) ? specs->align : decl_in.align; @@ -870,8 +918,7 @@ static void parse_init_declarator(Parser* p, const DeclSpecs* specs) { attr_list_to_decl(p->c, p->decls, specs->attrs, &decl_in); did = decl_declare(p->decls, &decl_in); sym = decl_obj_sym(p->decls, did); - reject_same_scope_redefinition(p, name, SEK_GLOBAL, var_ty); - e = scope_define(p, name, SEK_GLOBAL, var_ty); + e = scope_define_checked(p, name, SEK_GLOBAL, var_ty); e->v.sym = sym; sym_set_decl(e, did, DS_EXTERN, DL_EXTERNAL, decl_in.flags, DSTATE_DECLARED); @@ -1113,7 +1160,10 @@ static SymEntry* declare_function(Parser* p, Sym fname, const Type* fn_ty, external_func_remember(p, fname, existing); return existing; } - visible = scope_lookup(p, fname); + /* `existing` is provably NULL here (both branches above perr or return), so + * the current scope has no binding for fname. Skip re-probing it: walk from + * the parent scope. */ + visible = scope_lookup_from(p->scope ? p->scope->parent : NULL, fname); if (!existing && visible && visible->kind == SEK_FUNC) { existing = visible; } @@ -1252,9 +1302,7 @@ static void parse_function_body(Parser* p, ObjSymId fsym, const Type* fn_ty, s = pcg_param_slot(p, i, &fsd); pds[i].slot = s; if (infos[i].name) { - reject_same_scope_redefinition(p, infos[i].name, SEK_LOCAL, - infos[i].type); - e = scope_define(p, infos[i].name, SEK_LOCAL, infos[i].type); + e = scope_define_checked(p, infos[i].name, SEK_LOCAL, infos[i].type); e->v.slot = s; sym_set_decl(e, DECL_NONE, DS_AUTO, DL_NONE, DF_NONE, DSTATE_DEFINED); e->vla_bounds = build_param_vla_bounds(p, &infos[i], infos[i].loc); @@ -1312,9 +1360,8 @@ static void parse_external_decl(Parser* p) { if (is_punct(&p->cur, '=')) { perr(p, "typedef declarator cannot have initializer"); } - reject_same_scope_redefinition(p, tname, SEK_TYPEDEF, tty); { - SymEntry* te = scope_define(p, tname, SEK_TYPEDEF, tty); + SymEntry* te = scope_define_checked(p, tname, SEK_TYPEDEF, tty); sym_set_decl(te, DECL_NONE, DS_TYPEDEF, DL_NONE, specs.flags, DSTATE_DECLARED); } @@ -1587,9 +1634,11 @@ void parse_c(Compiler* c, Pool* pool, Pp* pp, DeclTable* decls, CG* cg, /* Index storage for the scope/tag tables and the external-function table all * comes from the arena via the pool's shared arena-heap facade. */ ExternalFuncMap_init(&p.external_funcs, &p.pool->arena_heap); + KwMap_init(&p.kw_map, &p.pool->arena_heap); for (i = (CKw)1; i < KW_COUNT; ++i) { p.kw_sym[i] = kit_sym_intern(p.pool->c, kit_slice_cstr(kw_names[i])); + (void)KwMap_set(&p.kw_map, p.kw_sym[i], (u8)i); /* canonical keyword */ } p.sym_b_alloca = kit_sym_intern(p.pool->c, KIT_SLICE_LIT("__builtin_alloca")); @@ -1674,6 +1723,16 @@ void parse_c(Compiler* c, Pool* pool, Pp* pp, DeclTable* decls, CG* cg, p.sym_restrict_alias2 = kit_sym_intern(p.pool->c, KIT_SLICE_LIT("__restrict__")); p.sym_thread_alias = kit_sym_intern(p.pool->c, KIT_SLICE_LIT("__thread")); + /* GNU alias spellings -> their canonical CKw. Registered after the canonical + * keywords above so the canonical mapping always wins (matching the old + * ident_kw_inline fall-through, which scanned kw_sym[] before the aliases). */ + (void)KwMap_set(&p.kw_map, p.sym_alignof_alias, (u8)KW_ALIGNOF); + (void)KwMap_set(&p.kw_map, p.sym_asm_alias, (u8)KW_BUILTIN_ASM); + (void)KwMap_set(&p.kw_map, p.sym_inline_alias, (u8)KW_INLINE); + (void)KwMap_set(&p.kw_map, p.sym_inline_alias2, (u8)KW_INLINE); + (void)KwMap_set(&p.kw_map, p.sym_restrict_alias, (u8)KW_RESTRICT); + (void)KwMap_set(&p.kw_map, p.sym_restrict_alias2, (u8)KW_RESTRICT); + (void)KwMap_set(&p.kw_map, p.sym_thread_alias, (u8)KW_THREAD_LOCAL); p.sym_int128 = kit_sym_intern(p.pool->c, KIT_SLICE_LIT("__int128")); p.sym_int128_t = kit_sym_intern(p.pool->c, KIT_SLICE_LIT("__int128_t")); p.sym_uint128_t = kit_sym_intern(p.pool->c, KIT_SLICE_LIT("__uint128_t")); diff --git a/lang/c/parse/parse_priv.h b/lang/c/parse/parse_priv.h @@ -165,6 +165,13 @@ KIT_HASHMAP_DEFINE(SymEntryMap, Sym, SymEntry*, kit_hash_u32); KIT_HASHMAP_DEFINE(TagEntryMap, Sym, TagEntry*, kit_hash_u32); KIT_HASHMAP_DEFINE(ExternalFuncMap, Sym, SymEntry*, kit_hash_u32); +/* Interned keyword/alias Sym -> CKw (stored as u8), built once in parse_c. + * Replaces the per-identifier linear scan of kw_sym[] (plus the alias compares) + * with a single O(1) probe: every non-keyword identifier (variable / typedef / + * field name) previously scanned all ~52 keyword syms before returning KW_NONE. + * Storage comes from the pool's shared arena-heap facade (Pool.arena_heap). */ +KIT_HASHMAP_DEFINE(KwMap, Sym, u8, kit_hash_u32); + typedef struct Scope Scope; struct Scope { SymEntry* entries; /* LIFO */ @@ -236,6 +243,7 @@ typedef struct Parser { int has_pending; Sym kw_sym[KW_COUNT]; + KwMap kw_map; /* keyword/alias Sym -> CKw; built once, see ident_kw_inline */ Sym sym_b_alloca; Sym sym_b_ctz; @@ -434,18 +442,12 @@ static inline int is_punct(const Tok* t, u32 punct) { static inline int is_pp_hash(const Tok* t) { return t->kind == TOK_PP_HASH; } static inline CKw ident_kw_inline(const Parser* p, Sym name) { - CKw i; - for (i = (CKw)1; i < KW_COUNT; ++i) { - if (p->kw_sym[i] == name) return i; - } - if (name == p->sym_alignof_alias) return KW_ALIGNOF; - if (name == p->sym_asm_alias) return KW_BUILTIN_ASM; - if (name == p->sym_inline_alias || name == p->sym_inline_alias2) - return KW_INLINE; - if (name == p->sym_restrict_alias || name == p->sym_restrict_alias2) - return KW_RESTRICT; - if (name == p->sym_thread_alias) return KW_THREAD_LOCAL; - return KW_NONE; + /* O(1) classification via kw_map (canonical keywords + GNU alias spellings, + * populated once in parse_c with canonical-first ordering so a real keyword + * is never overwritten by an alias). Equivalent to the old linear scan of + * kw_sym[] followed by the explicit alias compares. */ + const u8* v = name ? KwMap_get(&p->kw_map, name) : NULL; + return v ? (CKw)*v : KW_NONE; } static inline int is_kw(const Parser* p, const Tok* t, CKw k) { diff --git a/lang/cpp/lex/lex.c b/lang/cpp/lex/lex.c @@ -150,6 +150,34 @@ static int is_alpha(int c) { } static int is_alnum(int c) { return is_alpha(c) || is_digit(c); } +/* Consume a maximal run of identifier-continuation bytes (is_alnum) from the + * cursor, advancing line/col exactly as a per-byte bump() would. No is_alnum + * byte is '\n', so within a run line changes only when a folded splice point is + * crossed. The common splice-free input (l->splices == NULL) collapses to a + * single pos/col advance with src/len held in registers — no per-byte struct + * reload and no per-byte splice load+branch in the hottest scan loop. Stops at + * the first non-alnum byte (a '\\' UCN lead, a punctuator, or end), which the + * caller re-examines (e.g. for a UCN continuation). */ +static void scan_ident_run(Lexer* l) { + const char* s = l->src; + size_t n = l->len; + if (l->splices == NULL) { + size_t p = l->pos; + while (p < n && is_alnum((unsigned char)s[p])) ++p; + l->col += (u32)(p - l->pos); + l->pos = p; + return; + } + /* Splice-present slow path: keep the per-byte cursor advance and the splice + * line/col sync bit-for-bit identical to bump() (is_alnum bytes are never + * '\n', so only lex_sync_splices can move the line). */ + while (l->pos < n && is_alnum((unsigned char)s[l->pos])) { + l->pos++; + l->col++; + lex_sync_splices(l); + } +} + /* Match a UCN at offset `off` from the current position. Returns the total * length (6 for \uXXXX, 10 for \UXXXXXXXX), or 0 if no UCN matches. The * range constraints from §6.4.3 (no UCN < 00A0 except $/@/`, and none in @@ -357,7 +385,14 @@ Tok lex_next(Lexer* l) { size_t start; int ch; - memset(&t, 0, sizeof(t)); + /* No per-token memset: every content-token path assigns kind/loc/spelling/v, + * and flags is the only field accumulated via |=, so a deterministic zero + * base for flags (and the v union, which the number path leaves unwritten) is + * all the hot path needs. The cold EOF/NEWLINE early returns explicitly clear + * spelling so the returned token stays byte-identical to the old + * memset-then-fill token. */ + t.flags = 0; + t.v.ident = 0; /* Skip whitespace and comments. A newline token is emitted before any * subsequent content tokens for the line that follows. */ @@ -366,6 +401,7 @@ Tok lex_next(Lexer* l) { if (l->pos >= l->len) { t.kind = TOK_EOF; t.loc = lex_here(l); + t.spelling = 0; return t; } if (peek(l, 0) == '\n') { @@ -373,6 +409,7 @@ Tok lex_next(Lexer* l) { bump(l); t.kind = TOK_NEWLINE; t.loc = tloc; + t.spelling = 0; l->at_bol = 1; l->had_space = 0; l->dstate = 0; @@ -486,10 +523,8 @@ Tok lex_next(Lexer* l) { } else bump(l); for (;;) { - int c = peek(l, 0); - if (is_alnum(c)) { - bump(l); - } else if ((u = ucn_len(l, 0))) { + scan_ident_run(l); /* consume the maximal is_alnum run in one go */ + if ((u = ucn_len(l, 0))) { int i; for (i = 0; i < u; ++i) bump(l); } else { diff --git a/lang/cpp/pp/pp.c b/lang/cpp/pp/pp.c @@ -46,59 +46,80 @@ void src_pop(Pp* pp) { --pp->nsources; } -/* Read next raw token from the top source. Returns TOK_EOF when stack is - * empty. Pops empty buffer/lexer sources as it descends. `src_kind_out`, - * if non-NULL, receives the kind of the source the token came from - * (SRC_LEX vs SRC_BUF). Used by pp_next_raw to gate directive recognition - * to lex-sourced tokens only — a `#` produced by macro expansion never - * starts a directive (§6.10.3.4 ¶3, covered by `63_rescan_not_directive`). */ -Tok src_next_raw(Pp* pp, HidesetId* hs_out, u8* src_kind_out) { - Tok t; +/* Read next raw token from the top source, writing it through `out`. Sets + * *out to TOK_EOF when the stack is empty. Pops empty buffer/lexer sources + * as it descends. `src_kind_out`, if non-NULL, receives the kind of the + * source the token came from (SRC_LEX vs SRC_BUF). Used by pp_next_raw to + * gate directive recognition to lex-sourced tokens only — a `#` produced by + * macro expansion never starts a directive (§6.10.3.4 ¶3, covered by + * `63_rescan_not_directive`). + * + * This is the out-pointer form: lex_next already returns via sret, so it + * writes straight into the caller's slot with no inter-frame copy, and the + * EOF/empty-stack fallthrough is the only path that has to zero *out. */ +void src_next_raw_into(Pp* pp, Tok* out, HidesetId* hs_out, u8* src_kind_out) { TokSrc* s; while ((s = src_top(pp)) != NULL) { if (s->kind == SRC_BUF) { if (s->i < s->n) { - t = s->toks[s->i]; + *out = s->toks[s->i]; if (hs_out) *hs_out = s->hs ? s->hs[s->i] : HS_EMPTY; if (src_kind_out) *src_kind_out = SRC_BUF; ++s->i; - return t; + return; } if (s->scope_top) { - memset(&t, 0, sizeof(t)); - t.kind = TOK_EOF; + /* Explicit field zeroing in lieu of memset: byte-identical to the + * old memset-then-kind EOF token, minus the per-EOF libcall. */ + out->kind = TOK_EOF; + out->flags = 0; + out->loc = (SrcLoc){0}; + out->spelling = 0; + out->v.ident = 0; if (hs_out) *hs_out = HS_EMPTY; if (src_kind_out) *src_kind_out = SRC_BUF; - return t; + return; } src_pop(pp); continue; } /* SRC_LEX */ - t = lex_next(s->lex); - if (t.kind == TOK_EOF) { + *out = lex_next(s->lex); + if (out->kind == TOK_EOF) { if (pp->nsources > 1) { src_pop(pp); continue; } if (hs_out) *hs_out = HS_EMPTY; if (src_kind_out) *src_kind_out = SRC_LEX; - return t; + return; } /* 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). */ if (s->line_delta) { - t.loc.line = (u32)((i32)t.loc.line + s->line_delta); + out->loc.line = (u32)((i32)out->loc.line + s->line_delta); } if (hs_out) *hs_out = HS_EMPTY; if (src_kind_out) *src_kind_out = SRC_LEX; - return t; + return; } - memset(&t, 0, sizeof(t)); - t.kind = TOK_EOF; + /* Explicit field zeroing in lieu of memset: byte-identical to the old + * memset-then-kind EOF token, minus the per-EOF libcall. */ + out->kind = TOK_EOF; + out->flags = 0; + out->loc = (SrcLoc){0}; + out->spelling = 0; + out->v.ident = 0; if (hs_out) *hs_out = HS_EMPTY; if (src_kind_out) *src_kind_out = SRC_LEX; +} + +/* Thin by-value shim for the cold/general callers (arg collection, paren + * peek) that pass a NULL src_kind and don't sit in the hot -E loop. */ +Tok src_next_raw(Pp* pp, HidesetId* hs_out, u8* src_kind_out) { + Tok t; + src_next_raw_into(pp, &t, hs_out, src_kind_out); return t; } @@ -131,13 +152,16 @@ Tok pp_next(Pp* pp) { * tokens as stray identifiers. When we see TOK_PP_HASH followed by * `pragma`, swallow tokens through the next NEWLINE. */ for (;;) { - Tok t = pp_next_raw(pp); + Tok t; + pp_next_raw_into(pp, &t); if (t.kind == TOK_NEWLINE) continue; if (t.kind == TOK_PP_HASH) { - Tok t2 = pp_next_raw(pp); + Tok t2; + pp_next_raw_into(pp, &t2); if (t2.kind == TOK_IDENT && t2.v.ident == pp->sym_pragma) { for (;;) { - Tok tt = pp_next_raw(pp); + Tok tt; + pp_next_raw_into(pp, &tt); if (tt.kind == TOK_NEWLINE || tt.kind == TOK_EOF) break; } continue; @@ -189,7 +213,8 @@ void pp_emit_text(Pp* pp, Writer* out) { size_t on = 0; int at_bol = 1; for (;;) { - Tok t = pp_next_raw(pp); + Tok t; + pp_next_raw_into(pp, &t); if (t.kind == TOK_EOF) break; if (t.kind == TOK_NEWLINE) { pp_emit_stage(out, obuf, sizeof obuf, &on, "\n", 1); diff --git a/lang/cpp/pp/pp_directive.c b/lang/cpp/pp/pp_directive.c @@ -585,6 +585,45 @@ static void do_endif(Pp* pp, SrcLoc loc) { * #include (§6.10.2) * ============================================================ */ +/* Arena-heap adapter backing the IncCache slot table: alloc/realloc bump + * from pp->arena (immortal until pp_free), free is a no-op. A resize + * orphans the small old slot table in the arena — bounded by the final + * table size and negligible. Mirrors the parser's c_arena_heap. */ +static void* inc_cache_alloc(KitHeap* h, size_t n, size_t align) { + return kit_arena_alloc((KitArena*)h->user, n, align); +} +static void* inc_cache_realloc(KitHeap* h, void* old, size_t old_n, + size_t new_n, size_t align) { + void* q = kit_arena_alloc((KitArena*)h->user, new_n, align); + if (q && old && old_n) memcpy(q, old, old_n < new_n ? old_n : new_n); + return q; +} +static void inc_cache_free(KitHeap* h, void* p, size_t n) { + (void)h; + (void)p; + (void)n; +} + +/* Lazily set up the header-content cache, resolution memo, and their + * shared arena-backed heap. */ +static void inc_cache_ensure(Pp* pp) { + if (pp->inc_cache_ready) return; + pp->inc_cache_heap.alloc = inc_cache_alloc; + pp->inc_cache_heap.realloc = inc_cache_realloc; + pp->inc_cache_heap.free = inc_cache_free; + pp->inc_cache_heap.user = pp->arena; + IncCache_init(&pp->inc_cache, &pp->inc_cache_heap); + IncResolveMap_init(&pp->inc_resolve, &pp->inc_cache_heap); + pp->inc_cache_ready = 1; +} + +/* Intern a candidate/resolved path string into a Sym usable as a cache + * key. Returns 0 (an invalid key) only for the empty string, which never + * names a real header. */ +static Sym inc_cache_key(Pp* pp, const char* path) { + return kit_sym_intern(pp->pool->c, kit_slice_cstr(path)); +} + /* Read `path` via the host's file_io and copy its bytes into the pp * arena so they outlive io->release. Returns 1 on success. */ static int try_open_include(Pp* pp, const char* path, const u8** data_out, @@ -611,6 +650,36 @@ static int try_open_include(Pp* pp, const char* path, const u8** data_out, return 1; } +/* Open `path` for #include, serving from the header-content cache when the + * same resolved path was already read this TU. A hit returns the cached + * arena bytes with NO syscalls; a miss reads via try_open_include and + * caches the result keyed on the path string. The returned (data,size) is + * byte-identical to a fresh read, so this never affects which file wins a + * search nor the bytes that feed the lexer. */ +static int open_include_cached(Pp* pp, const char* path, const u8** data_out, + size_t* size_out) { + Sym key; + IncEntry* hit; + inc_cache_ensure(pp); + key = inc_cache_key(pp, path); + if (key) { + hit = IncCache_get(&pp->inc_cache, key); + if (hit) { + *data_out = hit->data; + *size_out = hit->size; + return 1; + } + } + if (!try_open_include(pp, path, data_out, size_out)) return 0; + if (key) { + IncEntry e; + e.data = *data_out; + e.size = *size_out; + IncCache_set(&pp->inc_cache, key, e); + } + return 1; +} + /* Return the includer's directory for resolving a quoted include, or "." * for in-memory/builtin sources (where CWD is the natural fallback, like * gcc treats stdin). `dir_out` must point to a buffer of size >= cap. */ @@ -656,10 +725,59 @@ static int includer_dir(Pp* pp, SrcLoc loc, char* dir_out, size_t cap) { return 1; } +/* Build the resolution-memo key for a (spelling, system, includer-dir) + * request and intern it. The key folds the search-affecting inputs into + * one string: a leading '<' (system) or '"' (quoted) tag, and — for the + * quoted form, whose includer-relative step is includer-specific — the + * includer directory followed by a '\n' separator (which can appear in + * neither a path nor a header spelling) before the spelling. System form + * skips the includer step, so its key omits the dir. Returns 0 only if + * the key would overflow the scratch buffer (memo simply not used). */ +static Sym inc_resolve_key(Pp* pp, const char* path, size_t plen, int system, + SrcLoc loc) { + char key[4096 + 4096 + 8]; + size_t pos = 0; + key[pos++] = system ? '<' : '"'; + if (!system) { + char dir[4096]; + if (!includer_dir(pp, loc, dir, sizeof(dir))) return 0; + { + size_t dlen = kit_slice_cstr(dir).len; + if (pos + dlen + 1 + plen >= sizeof(key)) return 0; + memcpy(key + pos, dir, dlen); + pos += dlen; + key[pos++] = '\n'; + } + } else if (pos + plen >= sizeof(key)) { + return 0; + } + memcpy(key + pos, path, plen); + pos += plen; + return kit_sym_intern(pp->pool->c, + (KitSlice){.s = key, .len = pos}); +} + +/* Record a successful dir-search resolution under its spelling key, so the + * next request for the same spelling skips the search. No-op when the key + * was unavailable (rkey == 0). `resolved` is a NUL-terminated path. */ +static void inc_resolve_record(Pp* pp, Sym rkey, const char* resolved, + int resolved_system) { + IncResolved e; + if (!rkey) return; + e.path = kit_sym_intern(pp->pool->c, kit_slice_cstr(resolved)); + e.system = (u8)(resolved_system ? 1 : 0); + IncResolveMap_set(&pp->inc_resolve, rkey, e); +} + /* Search for a header. Absolute paths are opened verbatim. Quoted form * ("...") additionally searches the directory of the file containing the * #include first (per C §6.10.2); bracket form (<...>) skips that step. - * Both forms then walk the configured -I / -isystem dirs in order. */ + * Both forms then walk the configured -I / -isystem dirs in order. + * + * A resolution memo short-circuits the repeat case: when the same spelling + * (with the same system flag and, for quoted form, the same includer dir) + * was resolved before, we go straight to the winning path — skipping the + * dir-by-dir ENOENT storm — and the content cache serves its bytes. */ static int find_and_open_include(Pp* pp, const char* path, int system, SrcLoc loc, const u8** data, size_t* size, char* resolved, size_t resolved_cap, @@ -667,13 +785,14 @@ static int find_and_open_include(Pp* pp, const char* path, int system, char buf[4096]; u32 i; size_t plen = kit_slice_cstr(path).len; + Sym rkey = 0; /* Absolute paths and the includer-relative ("...") step are not system * search dirs; only a configured -isystem dir flips this to 1 below. */ *resolved_system_out = 0; if (plen > 0 && path[0] == '/') { - if (try_open_include(pp, path, data, size)) { + if (open_include_cached(pp, path, data, size)) { if (plen + 1 > resolved_cap) return 0; memcpy(resolved, path, plen + 1); return 1; @@ -681,6 +800,24 @@ static int find_and_open_include(Pp* pp, const char* path, int system, return 0; } + /* Probe the resolution memo (covers only the dir-search winners below; + * absolute paths return above). On a hit, reopen the recorded resolved + * path — which the content cache already holds, so this is syscall-free + * — and reproduce the byte-identical resolved string + system flag. */ + inc_cache_ensure(pp); + rkey = inc_resolve_key(pp, path, plen, system, loc); + if (rkey) { + IncResolved* hit = IncResolveMap_get(&pp->inc_resolve, rkey); + if (hit) { + KitSlice rp = kit_sym_str(pp->pool->c, hit->path); + if (rp.len + 1 > resolved_cap) return 0; + if (!open_include_cached(pp, rp.s, data, size)) return 0; + memcpy(resolved, rp.s, rp.len + 1); + *resolved_system_out = hit->system ? 1 : 0; + return 1; + } + } + if (!system) { char dir[4096]; if (includer_dir(pp, loc, dir, sizeof(dir))) { @@ -690,9 +827,10 @@ static int find_and_open_include(Pp* pp, const char* path, int system, buf[dlen] = '/'; memcpy(buf + dlen + 1, path, plen); buf[dlen + 1 + plen] = 0; - if (try_open_include(pp, buf, data, size)) { + if (open_include_cached(pp, buf, data, size)) { if (dlen + 1 + plen + 1 > resolved_cap) return 0; memcpy(resolved, buf, dlen + 1 + plen + 1); + inc_resolve_record(pp, rkey, resolved, 0); return 1; } } @@ -706,10 +844,11 @@ static int find_and_open_include(Pp* pp, const char* path, int system, buf[dlen] = '/'; memcpy(buf + dlen + 1, path, plen); buf[dlen + 1 + plen] = 0; - if (try_open_include(pp, buf, data, size)) { + if (open_include_cached(pp, buf, data, size)) { if (dlen + 1 + plen + 1 > resolved_cap) return 0; memcpy(resolved, buf, dlen + 1 + plen + 1); *resolved_system_out = pp->inc_dirs[i].system ? 1 : 0; + inc_resolve_record(pp, rkey, resolved, *resolved_system_out); return 1; } } diff --git a/lang/cpp/pp/pp_expand.c b/lang/cpp/pp/pp_expand.c @@ -427,7 +427,7 @@ void expand_arg_to_eof(Pp* pp, Tok* in, HidesetId* hs, u32 nin, TokVec* out) { src_push(pp, src); for (;;) { - t = pp_next_raw(pp); /* drives macro expansion within this scope */ + pp_next_raw_into(pp, &t); /* drives macro expansion within this scope */ if (t.kind == TOK_EOF) break; if (t.kind == TOK_NEWLINE) { /* Newlines inside an arg act as whitespace; convert to @@ -793,24 +793,35 @@ 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; + /* Track whether any placemarker actually lands in `out`. The strip pass + * below is a full O(out->n) re-walk + element-wise compaction; the common + * macro body has no empty-arg/paste placemarkers, so when none was emitted + * we skip the second walk entirely. */ + int had_pm = 0; /* 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) { - Tok lhs, rhs; + Tok lhs, rhs, pasted; if (out->n == 0 || i + 1 >= nin) { compiler_panic(pp->c, invoke->loc, "'##' at start or end of replacement list"); } lhs = out->data[--out->n]; rhs = in[++i]; - tv_push(pp, out, paste_tokens(pp, lhs, rhs, invoke->loc)); + /* paste_tokens collapses a placemarker operand to the other side, so + * the result is a placemarker only when both operands were. */ + pasted = paste_tokens(pp, lhs, rhs, invoke->loc); + if (pasted.kind == TOK_PP_PLACEMARKER) had_pm = 1; + tv_push(pp, out, pasted); continue; } + if (t.kind == TOK_PP_PLACEMARKER) had_pm = 1; tv_push(pp, out, t); } + if (!had_pm) return; /* no placemarker to strip — skip the compaction walk */ /* Strip placemarkers, preserving leading-space flag on the next token. */ { u32 r = 0, w = 0; @@ -894,14 +905,21 @@ static int try_expand_func_macro(Pp* pp, const Macro* m, const Tok* invoke, * Defined here; also declared in pp_priv.h so pp.c can call it. * ============================================================ */ -/* pp_next_raw: reads from the top source, applies macro expansion when an - * identifier names a macro that isn't blue-painted, and consumes - * directives in-place. TOK_NEWLINE is preserved for pp_emit_text. */ -Tok pp_next_raw(Pp* pp) { - Tok t; +/* pp_next_raw_into: out-pointer form. Reads from the top source into *out, + * applies macro expansion when an identifier names a macro that isn't + * blue-painted, and consumes directives in-place. TOK_NEWLINE is preserved + * for pp_emit_text. + * + * Tok is 24B, so the by-value form (pp_next_raw, below) returns indirectly + * via sret and pays a 24B inter-frame copy at every `return t`. Writing + * through `out` lets the token — and lex_next's own sret — land in the + * caller's slot directly. The hot SRC_BUF case (macro replay) is inlined + * here so the common token never pays a call into src_next_raw_into. */ +void pp_next_raw_into(Pp* pp, Tok* out) { HidesetId hs; u8 src_kind; for (;;) { + TokSrc* s; /* Reclaim the transient expansion scratch whenever the source stack has * drained back to a lexer. A SRC_BUF always sits above every SRC_LEX (a * macro can't #include), so a lexer on top means no expansion buffer is @@ -916,10 +934,25 @@ Tok pp_next_raw(Pp* pp) { pp->sources[pp->nsources - 1].kind == SRC_LEX)) { kit_arena_reset(pp->xarena); } - t = src_next_raw(pp, &hs, &src_kind); - if (t.kind == TOK_EOF) return t; - if (t.kind == TOK_PP_HASH && (t.flags & TF_AT_BOL) && src_kind == SRC_LEX) { - process_directive(pp, t.loc); + /* Fast path: top source is a non-exhausted SRC_BUF (the dominant + * macro-replay case). Pull the token, hideset, and kind inline so the + * common token avoids the call + sret setup of src_next_raw_into. The + * scope_top-EOF and #line-delta cases never apply to a plain in-bounds + * SRC_BUF read, so they stay on the general (cold) path. */ + if (pp->nsources != 0 && + (s = &pp->sources[pp->nsources - 1])->kind == SRC_BUF && + s->i < s->n) { + *out = s->toks[s->i]; + hs = s->hs ? s->hs[s->i] : HS_EMPTY; + src_kind = SRC_BUF; + ++s->i; + } else { + src_next_raw_into(pp, out, &hs, &src_kind); + } + if (out->kind == TOK_EOF) return; + if (out->kind == TOK_PP_HASH && (out->flags & TF_AT_BOL) && + src_kind == SRC_LEX) { + process_directive(pp, out->loc); /* No synthesized newline: the comparator collapses * whitespace, so blank-line replacement of consumed * directives isn't observable here. Directives that produce @@ -935,32 +968,32 @@ Tok pp_next_raw(Pp* pp) { * the `defined_skip` field comment in pp_priv.h. */ if (pp->in_if_expansion) { if (pp->defined_skip == 2) { - if (t.kind == TOK_PUNCT && t.v.punct == '(') { + if (out->kind == TOK_PUNCT && out->v.punct == '(') { pp->defined_skip = 3; - } else if (t.kind == TOK_IDENT) { + } else if (out->kind == TOK_IDENT) { /* `defined IDENT` (no parens): mark the operand and reset. */ - t.flags |= TF_NO_EXPAND; + out->flags |= TF_NO_EXPAND; pp->defined_skip = 0; } else { pp->defined_skip = 0; } } else if (pp->defined_skip == 3) { - if (t.kind == TOK_IDENT) { - t.flags |= TF_NO_EXPAND; + if (out->kind == TOK_IDENT) { + out->flags |= TF_NO_EXPAND; pp->defined_skip = 4; - } else if (t.kind == TOK_PUNCT && t.v.punct == ')') { + } else if (out->kind == TOK_PUNCT && out->v.punct == ')') { pp->defined_skip = 0; } } else if (pp->defined_skip == 4) { - if (t.kind == TOK_PUNCT && t.v.punct == ')') { + if (out->kind == TOK_PUNCT && out->v.punct == ')') { pp->defined_skip = 0; } - } else if (t.kind == TOK_IDENT && t.v.ident == pp->sym_defined) { + } else if (out->kind == TOK_IDENT && out->v.ident == pp->sym_defined) { pp->defined_skip = 2; } } - if (t.kind == TOK_IDENT && (t.flags & TF_NO_EXPAND) == 0) { - Sym id = t.v.ident; + if (out->kind == TOK_IDENT && (out->flags & TF_NO_EXPAND) == 0) { + Sym id = out->v.ident; /* Dynamic predefined macros: __LINE__ / __FILE__ / * __DATE__ / __TIME__. Always expand, ignoring the macro @@ -968,7 +1001,7 @@ Tok pp_next_raw(Pp* pp) { if (id == pp->sym_line__) { char tmp[16], buf[16]; int k = 0, j = 0; - u32 ln = t.loc.line; + u32 ln = out->loc.line; if (ln == 0) buf[k++] = '0'; else { @@ -978,10 +1011,10 @@ Tok pp_next_raw(Pp* pp) { } while (j > 0) buf[k++] = tmp[--j]; } - t.kind = TOK_NUM; - t.spelling = + out->kind = TOK_NUM; + out->spelling = kit_sym_intern(pp->pool->c, (KitSlice){.s = buf, .len = (size_t)k}); - return t; + return; } if (id == pp->sym_file__) { TokSrc* ls = current_lex_src(pp); @@ -1020,27 +1053,27 @@ Tok pp_next_raw(Pp* pp) { buf[bn++] = ch; } buf[bn++] = '"'; - t.kind = TOK_STR; - t.spelling = + out->kind = TOK_STR; + out->spelling = kit_sym_intern(pp->pool->c, (KitSlice){.s = buf, .len = bn}); - t.v.str = t.spelling; + out->v.str = out->spelling; } - return t; + return; } if (id == pp->sym_date__) { - t.kind = TOK_STR; - t.spelling = pp->val_date_str; - t.v.str = t.spelling; - return t; + out->kind = TOK_STR; + out->spelling = pp->val_date_str; + out->v.str = out->spelling; + return; } if (id == pp->sym_time__) { - t.kind = TOK_STR; - t.spelling = pp->val_time_str; - t.v.str = t.spelling; - return t; + out->kind = TOK_STR; + out->spelling = pp->val_time_str; + out->v.str = out->spelling; + return; } if (id == pp->sym__pragma) { - if (try_expand_pragma_op(pp, &t)) continue; + if (try_expand_pragma_op(pp, out)) continue; /* No '(' — fall through and emit as plain ident. */ } @@ -1048,16 +1081,24 @@ Tok pp_next_raw(Pp* pp) { Macro* m = mt_get(pp, id); if (m && !hs_contains(pp, hs, m->name)) { if (!m->is_func) { - expand_object_macro(pp, m, &t, hs); + expand_object_macro(pp, m, out, hs); continue; } - if (try_expand_func_macro(pp, m, &t, hs)) { + if (try_expand_func_macro(pp, m, out, hs)) { continue; } /* No '(' followed; emit as plain identifier. */ } } } - return t; + return; } } + +/* Thin by-value shim: the mutual-recursion entry (expand_arg_to_eof) and any + * external caller use this; the hot -E loops call pp_next_raw_into directly. */ +Tok pp_next_raw(Pp* pp) { + Tok t; + pp_next_raw_into(pp, &t); + return t; +} diff --git a/lang/cpp/pp/pp_priv.h b/lang/cpp/pp/pp_priv.h @@ -87,6 +87,34 @@ typedef struct IfFrame { static inline u32 macro_hash_(Sym s) { return kit_hash_u32((u32)s); } KIT_HASHMAP_DEFINE(MacroMap, Sym, Macro*, macro_hash_); +/* IncCache = Sym(resolved-path) -> cached header bytes. The same guarded + * header included many times across a TU is read from disk once; later + * inclusions reuse the arena-resident bytes (lex_open_mem only borrows + * them and the pp arena keeps them alive until pp_free), saving the + * open+fstat+read+close + malloc/memcpy/free per repeat. Keyed strictly + * on the resolved-path STRING so distinct spellings/symlinks never + * over-merge; the cached bytes are byte-identical to a fresh read. */ +typedef struct IncEntry { + const u8* data; + size_t size; +} IncEntry; +static inline u32 inc_hash_(Sym s) { return kit_hash_u32((u32)s); } +KIT_HASHMAP_DEFINE(IncCache, Sym, IncEntry, inc_hash_); + +/* IncResolveMap = Sym(spelling key) -> resolved (path Sym, system flag). + * Memoizes the winner of the -I/-isystem dir search so a repeated request + * for the same header spelling skips the ENOENT storm of failed opens on + * the earlier dirs. The key folds the system flag (and, for quoted form, + * the includer directory) into the spelling so the resolved result is the + * exact one a fresh search would produce — byte-identical resolved path + * and resolved_system flag feeding source_add_include / DWARF / -M. */ +typedef struct IncResolved { + Sym path; /* interned resolved path string */ + u8 system; /* the resolved_system flag (winning -isystem) */ +} IncResolved; +static inline u32 incres_hash_(Sym s) { return kit_hash_u32((u32)s); } +KIT_HASHMAP_DEFINE(IncResolveMap, Sym, IncResolved, incres_hash_); + /* ============================================================ * Pp struct (definition shared across all three TUs) * ============================================================ */ @@ -103,6 +131,19 @@ struct Pp { /* Macro table (open-addressed; key = Sym, value = Macro*). */ MacroMap mtab; + /* Header-content cache (key = interned resolved-path Sym, value = + * arena-resident bytes). Collapses N re-includes of the same header to + * one read + N-1 cache hits. Lazily initialized on first #include; its + * slot table is backed by inc_cache_heap (an arena adapter over + * pp->arena), so nothing needs freeing at pp_free — the arena is. */ + IncCache inc_cache; + KitHeap inc_cache_heap; + u8 inc_cache_ready; + + /* Include-resolution memo (spelling -> winning resolved path + system + * flag). Shares inc_cache_heap (gated by inc_cache_ready). */ + IncResolveMap inc_resolve; + /* Conditional inclusion stack (#if / #ifdef / #ifndef → #endif). */ IfFrame* ifstk; u32 ifstk_n; @@ -300,6 +341,9 @@ static inline void cb_putc(Pp* pp, CharBuf* b, char c) { * ============================================================ */ /* --- pp.c (source stack) → pp_expand.c, pp_directive.c --- */ +/* Out-pointer form (hot path, no 24B sret round-trip); src_next_raw is the + * by-value shim over it for the cold/general callers. */ +void src_next_raw_into(Pp* pp, Tok* out, HidesetId* hs_out, u8* src_kind_out); Tok src_next_raw(Pp* pp, HidesetId* hs_out, u8* src_kind_out); void src_push(Pp* pp, TokSrc s); void src_pop(Pp* pp); @@ -307,7 +351,10 @@ void push_buf(Pp* pp, Tok* toks, HidesetId* hs, u32 n); /* pp_next_raw is the mutual-recursion entry: expand_arg_to_eof calls it, * and pp_next_raw drives directives and expansion. Declared non-static so - * pp_expand.c can call it without a forward decl each time. */ + * pp_expand.c can call it without a forward decl each time. pp_next_raw_into + * is the out-pointer form the hot -E loops call directly; pp_next_raw is the + * by-value shim. */ +void pp_next_raw_into(Pp* pp, Tok* out); Tok pp_next_raw(Pp* pp); /* --- pp_expand.c → pp.c, pp_directive.c --- */ diff --git a/src/arch/aa64/native.c b/src/arch/aa64/native.c @@ -2839,8 +2839,14 @@ static void aa_plan_call(NativeTarget* t, const NativeCallDesc* desc, NativeCallPlan* plan) { NativeCallPlanRet* rets; const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type); + /* Right-size the result scratch to the exact number of entries the ret loops + * below write: nparts on a DIRECT register return, 1 on the !abi fallback, + * 0 (NULL) otherwise (IGNORE / sret / no results). */ + u32 nrets_cap = (abi && abi->ret.kind == ABI_ARG_DIRECT && desc->nresults) + ? abi->ret.nparts + : ((!abi && desc->nresults) ? 1u : 0u); memset(plan, 0, sizeof *plan); - rets = desc->nresults ? arena_zarray(t->c->tu, NativeCallPlanRet, 4) : NULL; + rets = nrets_cap ? arena_zarray(t->c->tu, NativeCallPlanRet, nrets_cap) : NULL; plan->callee = desc->callee; plan->rets = rets; plan->flags = desc->flags; @@ -3058,7 +3064,6 @@ static void aa_plan_ret(NativeTarget* t, const CGFuncDesc* fd, const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fd->fn_type); NativeCallPlanRet* rets = NULL; u32 nr = 0; - if (value) rets = arena_zarray(t->c->tu, NativeCallPlanRet, 4); if (value && abi && abi->ret.kind == ABI_ARG_INDIRECT) { AANativeTarget* a = aa_of(t); /* Hold the sret destination pointer in x8, not AA_TMP1: aa_copy_bytes @@ -3088,6 +3093,7 @@ static void aa_plan_ret(NativeTarget* t, const CGFuncDesc* fd, } if (value && abi && abi->ret.kind == ABI_ARG_DIRECT) { u32 ni = 0, nf = 0; + rets = arena_zarray(t->c->tu, NativeCallPlanRet, abi->ret.nparts); for (u32 p = 0; p < abi->ret.nparts; ++p) { const ABIArgPart* part = &abi->ret.parts[p]; NativeAllocClass cls = @@ -3110,6 +3116,7 @@ static void aa_plan_ret(NativeTarget* t, const CGFuncDesc* fd, nr++; } } else if (value) { + rets = arena_zarray(t->c->tu, NativeCallPlanRet, 1); rets[0].src = *value; rets[0].dst = native_loc_reg(value->type, NATIVE_REG_INT, 0); rets[0].mem = aa_mem_for_type(t, value->type, 0); diff --git a/src/arch/mc.c b/src/arch/mc.c @@ -144,7 +144,10 @@ static void labels_grow(MCImpl* mc, u32 want) { while (ncap < want) ncap *= 2; MCLabelInfo* nbuf = arena_array(mc->arena, MCLabelInfo, ncap); if (mc->labels) memcpy(nbuf, mc->labels, sizeof(MCLabelInfo) * mc->nlabels); - memset(nbuf + mc->nlabels, 0, sizeof(MCLabelInfo) * (ncap - mc->nlabels)); + /* The grown tail is left uninitialized: m_label_new fully assigns every + * field of the one slot it hands out before any consumer indexes it, and + * nothing ever reads labels[i] for i >= nlabels (every access guards on + * id < nlabels and rejects MC_LABEL_NONE). */ mc->labels = nbuf; mc->cap = ncap; } @@ -233,6 +236,10 @@ ObjSymId mc_label_symbol(MCEmitter* m, MCLabel id) { static void m_set_section(MCEmitter* m, u32 section_id) { m->section_id = section_id; + /* Cache the active section's byte buffer so the hot emit path avoids the + * per-instruction Sections_at deref + nobits branch. NULL for NOBITS/.bss + * (or none): emit then falls back to obj_write for bss_size accounting. */ + m->cur_bytes = obj_section_bytes(m->obj, section_id); } static u32 m_pos(MCEmitter* m) { return obj_pos(m->obj, m->section_id); } @@ -289,7 +296,13 @@ static void m_label_place(MCEmitter* m, MCLabel id) { } static void m_emit_bytes(MCEmitter* m, const u8* data, size_t n) { - obj_write(m->obj, m->section_id, data, n); + /* Fast path: append straight to the cached section buffer (inlined + * buf_write). cur_bytes is NULL for NOBITS/.bss/none, where obj_write does + * the bss_size accounting instead. */ + if (m->cur_bytes) + buf_write(m->cur_bytes, data, n); + else + obj_write(m->obj, m->section_id, data, n); } static void m_emit_fill(MCEmitter* m, size_t n, u8 byte) { diff --git a/src/arch/mc.h b/src/arch/mc.h @@ -47,6 +47,13 @@ struct MCEmitter { Compiler* c; ObjBuilder* obj; u32 section_id; + /* Byte buffer of the active section, resolved once per set_section so the + * per-instruction emit_bytes path skips obj_write's Sections_at deref + + * nobits branch. NULL when the active section is NOBITS/.bss (or none): emit + * then falls back to obj_write so bss_size accounting still runs. The pointer + * is stable across emits (SegVec elements don't move); only set_section + * re-points it. */ + Buf* cur_bytes; /* Pending source location, updated by set_loc. Promoted to the base so * arch backends' emit-bytes choke point can read it without reaching diff --git a/src/arch/riscv/native.c b/src/arch/riscv/native.c @@ -2240,8 +2240,14 @@ static void rv_plan_call(NativeTarget* t, const NativeCallDesc* desc, const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type); NativeCallPlanRet* rets; KitCgTypeId i64t = builtin_id(KIT_CG_BUILTIN_I64); + /* Right-size the result scratch to the exact number of entries the ret loops + * below write: nparts on a DIRECT register return, 1 on the !abi fallback, + * 0 (NULL) otherwise (IGNORE / sret / no results). */ + u32 nrets_cap = (abi && abi->ret.kind == ABI_ARG_DIRECT && desc->nresults) + ? abi->ret.nparts + : ((!abi && desc->nresults) ? 1u : 0u); memset(plan, 0, sizeof *plan); - rets = desc->nresults ? arena_zarray(t->c->tu, NativeCallPlanRet, 4) : NULL; + rets = nrets_cap ? arena_zarray(t->c->tu, NativeCallPlanRet, nrets_cap) : NULL; plan->callee = desc->callee; plan->rets = rets; plan->flags = desc->flags; diff --git a/src/arch/x64/native.c b/src/arch/x64/native.c @@ -2504,8 +2504,14 @@ static void x64_plan_call(NativeTarget* t, const NativeCallDesc* desc, NativeCallPlanRet* rets; KitCgTypeId i64t = builtin_id(KIT_CG_BUILTIN_I64); u32 c; + /* Right-size the result scratch to the exact number of entries the ret loops + * below write: nparts on a DIRECT register return, 1 on the !abi fallback, + * 0 (NULL) otherwise (IGNORE / sret / no results). */ + u32 nrets_cap = (abi && abi->ret.kind == ABI_ARG_DIRECT && desc->nresults) + ? abi->ret.nparts + : ((!abi && desc->nresults) ? 1u : 0u); memset(plan, 0, sizeof *plan); - rets = desc->nresults ? arena_zarray(t->c->tu, NativeCallPlanRet, 4) : NULL; + rets = nrets_cap ? arena_zarray(t->c->tu, NativeCallPlanRet, nrets_cap) : NULL; plan->callee = desc->callee; plan->rets = rets; plan->flags = desc->flags; diff --git a/src/cg/arith.c b/src/cg/arith.c @@ -35,11 +35,17 @@ void api_cg_binop(KitCg* g, BinOp iop, u32 flags) { Operand dst; ApiSValue folded_sv; i64 folded; + int can_delay; if (!g) return; T = g->target; b = api_pop(g); a = api_pop(g); ty = a.type ? a.type : b.type; + /* Delayability is a pure function of (ty, flags), neither of which changes + * below (strength-reduce rewrites the op and operands, not the type), so + * classify the foldable int once instead of re-deriving it at each of the + * three delay gates. */ + can_delay = api_can_delay_int_arith(g, ty, flags); if (!flags && api_sv_op_is(&a, OPK_IMM) && api_sv_op_is(&b, OPK_IMM) && api_try_fold_int_binop(g, iop, ty, a.op.v.imm, b.op.v.imm, &folded)) { @@ -54,7 +60,7 @@ void api_cg_binop(KitCg* g, BinOp iop, u32 flags) { * identity / fallback machinery as any other shift or and. */ if (!flags) api_try_strength_reduce(g, &iop, ty, &a, &b); - if (api_can_delay_int_arith(g, ty, flags) && + if (can_delay && api_try_fold_arith_chain(g, iop, ty, &a, &b, &folded_sv)) { api_release(g, &a); api_release(g, &b); @@ -70,7 +76,7 @@ void api_cg_binop(KitCg* g, BinOp iop, u32 flags) { rb = api_force_local_unless_imm(g, &b, ty); } - if (api_can_delay_int_arith(g, ty, flags) && + if (can_delay && api_try_collapse_binop_identity(g, iop, ty, &a, &b, &folded_sv)) { api_release(g, &a); api_release(g, &b); @@ -78,7 +84,7 @@ void api_cg_binop(KitCg* g, BinOp iop, u32 flags) { return; } - if (api_can_delay_int_arith(g, ty, flags) && + if (can_delay && (ra.kind == OPK_LOCAL || rb.kind == OPK_LOCAL) && (ra.kind == OPK_LOCAL || ra.kind == OPK_IMM) && (rb.kind == OPK_LOCAL || rb.kind == OPK_IMM)) { @@ -109,10 +115,14 @@ void api_cg_unop(KitCg* g, UnOp iop, u32 flags) { Operand dst; ApiSValue folded_sv; i64 folded; + int can_delay; if (!g) return; T = g->target; a = api_pop(g); ty = a.type ? a.type : a.op.type; + /* Pure function of (ty, flags); classify the foldable int once for both delay + * gates below. */ + can_delay = api_can_delay_int_arith(g, ty, flags); if (iop == UO_FNEG) { if (!api_type_is_float(g->c, ty)) { @@ -145,15 +155,14 @@ void api_cg_unop(KitCg* g, UnOp iop, u32 flags) { return; } - if (api_can_delay_int_arith(g, ty, flags) && - api_try_fold_unary_chain(&a, iop, ty, &folded_sv)) { + if (can_delay && api_try_fold_unary_chain(&a, iop, ty, &folded_sv)) { api_release(g, &a); api_push(g, folded_sv); return; } ra = api_force_local_unless_imm(g, &a, ty); - if (api_can_delay_int_arith(g, ty, flags) && ra.kind == OPK_LOCAL) { + if (can_delay && ra.kind == OPK_LOCAL) { int a_owned = api_sv_owns_operand_local(&a, &ra); api_push(g, api_make_arith_unop(iop, ra, ty, a_owned)); if (a_owned) a.res = RES_INHERENT; @@ -307,26 +316,28 @@ void api_cg_convert_kind(KitCg* g, KitCgTypeId dst_type, ConvKind ck) { * mirrors the f128 dispatch in kit_cg_fp_*. * ============================================================ */ +/* The wide / soft-float dispatch class cached on the value-stack node at + * `depth` below TOS (api_push computes it via api_wide_kind_for). The five + * stack-top predicates below are now a bounds check plus a tag compare; the + * type-get + alias-chase gauntlet that used to run per operand happens once at + * push instead. WK_NARROW for an out-of-range depth keeps every predicate false + * exactly as the old bounds guards did. */ +static u8 api_stack_wide_kind(KitCg* g, u32 depth) { + if (!g || g->sp <= depth) return WK_NARROW; + return g->stack[g->sp - 1u - depth].bitfield.wide_kind; +} + int api_i128_stack_top(KitCg* g, u32 depth) { - if (!g || g->sp <= depth) return 0; - return api_is_i128_type(g->c, api_sv_type(&g->stack[g->sp - 1u - depth])); + return api_stack_wide_kind(g, depth) == WK_I128; } /* 64-bit integer split into two 32-bit lanes by the selected ABI. The native * backend handles add/sub/and/or/xor on such values as register pairs, but * mul/div/shift must be lowered to a __*di3 runtime call (see * api_wideint64_binop). i128 routes through its own ti3 path (api_i128_*), so - * it is explicitly excluded here. */ -static int api_int_is_wide64(KitCg* g, KitCgTypeId ty) { - if (!g) return 0; - if (api_is_i128_type(g->c, ty)) return 0; - if (kit_cg_type_int_width((KitCompiler*)g->c, ty) == 0) return 0; - return api_is_wide8_scalar_type(g->c, ty); -} - + * the WK_WIDE8 class (computed in api_wide_kind_for) excludes it. */ static int api_wide64_stack_top(KitCg* g, u32 depth) { - if (!g || g->sp <= depth) return 0; - return api_int_is_wide64(g, api_sv_type(&g->stack[g->sp - 1u - depth])); + return api_stack_wide_kind(g, depth) == WK_WIDE8; } static int api_binop_is_shift(BinOp iop) { @@ -1196,8 +1207,7 @@ static const char* api_softdf_binop_helper(KitCgFpBinOp op) { } int api_f128_stack_top(KitCg* g, u32 depth) { - if (!g || g->sp <= depth) return 0; - return api_is_f128_type(g->c, api_sv_type(&g->stack[g->sp - 1u - depth])); + return api_stack_wide_kind(g, depth) == WK_F128; } /* True when the target has no hardware double: float_abi is SOFT (ilp32/lp64, @@ -1220,8 +1230,7 @@ static int api_type_is_soft_double(KitCg* g, KitCgTypeId ty) { } static int api_soft_double_stack_top(KitCg* g, u32 depth) { - if (!g || g->sp <= depth) return 0; - return api_type_is_soft_double(g, api_sv_type(&g->stack[g->sp - 1u - depth])); + return api_stack_wide_kind(g, depth) == WK_SOFT_DOUBLE; } /* f32 under pure-soft ilp32/lp64 (float_abi SOFT, no FP unit): single-precision @@ -1233,8 +1242,7 @@ static int api_type_is_soft_single(KitCg* g, KitCgTypeId ty) { } static int api_soft_single_stack_top(KitCg* g, u32 depth) { - if (!g || g->sp <= depth) return 0; - return api_type_is_soft_single(g, api_sv_type(&g->stack[g->sp - 1u - depth])); + return api_stack_wide_kind(g, depth) == WK_SOFT_SINGLE; } /* Runtime helper for f32 arithmetic on a soft-float target (mirrors diff --git a/src/cg/internal.h b/src/cg/internal.h @@ -59,18 +59,43 @@ typedef struct ApiDelayedArith { u8 pad; } ApiDelayedArith; +/* Scalar wide-class tag cached on every value-stack node (api_push). The wide / + * soft-float dispatch in arith.c re-derived this on every operand with a string + * of type-get + alias-chase round trips; it is a pure function of the value type + * (and the target's fixed float-ABI / split-lane policy), and the type never + * changes after a value is pushed, so it is computed once at push time and the + * stack-top predicates become a load and compare. The tags are mutually + * exclusive (a type matches at most one; see api_wide_kind_for). WK_I128 is kept + * distinct from WK_WIDE8 because the int wide64 path explicitly excludes i128 + * (i128 routes through its own ti3 lowering). WK_NARROW == 0 so a zeroed node + * (api_make_sv memsets) defaults to the common narrow case. */ +typedef enum ApiWideKind { + WK_NARROW = 0, /* not wide / not soft-float: the common fast path */ + WK_I128, /* 128-bit integer (api_is_i128_type) */ + WK_F128, /* 128-bit float / long double (api_is_f128_type) */ + WK_WIDE8, /* int split into two 32-bit lanes (api_int_is_wide64) */ + WK_SOFT_DOUBLE, /* f64 on a target without hardware double */ + WK_SOFT_SINGLE, /* f32 on a pure-soft target (no FP unit) */ +} ApiWideKind; + /* Bit-field geometry carried by a bit-field PLACE. `kit_cg_field` fills this * from the record layout when it projects to a bit-field; a plain load/store on * the carrying place then performs the extract/insert. The storage MemAccess is * rebuilt from the place operand + field type at load/store time, so only the * bit geometry needs to ride on the place. width == 0 means "not a bit-field". - */ + * + * `wide_kind` (ApiWideKind) rides in this struct's trailing slack: it is the + * only one-byte hole in the value-stack node, and keeping it here leaves the + * node size unchanged (static_assert in value.c). It is node state, not bit + * geometry, so kit_cg_field_bits writes only the bit_* members and leaves it + * intact, and node copies (dup/swap/rot) carry it along with the type. */ typedef struct ApiBitField { u16 bit_offset; /* target-endian bit offset within the storage unit */ u16 bit_width; /* 0 => the place is not a bit-field place */ u32 bit_storage_size; /* storage-unit size in bytes */ u8 bit_signed; /* signed extraction on load */ - u8 pad[3]; + u8 wide_kind; /* ApiWideKind, cached at api_push (node slack) */ + u8 pad[2]; } ApiBitField; typedef struct ApiSValue { @@ -397,6 +422,7 @@ int api_is_f128_type(Compiler* c, KitCgTypeId ty); int api_is_i128_type(Compiler* c, KitCgTypeId ty); int api_is_wide16_scalar_type(Compiler* c, KitCgTypeId ty); int api_is_wide8_scalar_type(Compiler* c, KitCgTypeId ty); +u8 api_wide_kind_for(KitCg* g, KitCgTypeId ty); Operand api_op_imm(i64 v, KitCgTypeId ty); Operand api_op_local(CGLocal r, KitCgTypeId ty); Operand api_op_global(ObjSymId sym, i64 addend, KitCgTypeId ty); diff --git a/src/cg/native_direct_target.c b/src/cg/native_direct_target.c @@ -500,9 +500,12 @@ static int nd_local_cacheable(NativeDirectTarget* d, * a possibly-stale frame home. Base/index reads are always of the local's own * type, so the width check is trivially met for that use; the value-read width * hazard is handled separately in nd_materialize_operand. */ -/* Stamp a cache touch (def/read/addressing use) for LRU victim selection. */ +/* Stamp a cache touch (def/read/addressing use) for LRU victim selection. Every + * caller holds l->reg set (l is currently cached), so the flat reg_last_use + * mirror the victim scan reads stays in lockstep with l->last_use. */ static void nd_touch_local(NativeDirectTarget* d, NativeDirectLocal* l) { l->last_use = ++d->use_tick; + d->reg_last_use[l->cls][l->reg] = l->last_use; } static Reg nd_cache_reg_for(NativeDirectTarget* d, CGLocal local, @@ -526,14 +529,15 @@ static Reg nd_pick_cache_victim(NativeDirectTarget* d, NativeAllocClass cls) { u32 best_use = 0; for (u32 i = 0; i < ci->nallocable; ++i) { Reg r = ci->allocable[i]; - CGLocal owner; + u32 use; if (r >= 32u) continue; - owner = d->reg_owner[cls][r]; - if (owner == CG_LOCAL_NONE) continue; + if (d->reg_owner[cls][r] == CG_LOCAL_NONE) continue; if (d->scratch_used[cls] & (1u << r)) continue; /* pinned: never a victim */ - if (best == REG_NONE || nd_local(d, owner)->last_use < best_use) { + /* Flat mirror: equals nd_local(owner)->last_use for the owning local. */ + use = d->reg_last_use[cls][r]; + if (best == REG_NONE || use < best_use) { best = r; - best_use = nd_local(d, owner)->last_use; + best_use = use; } } return best; @@ -543,13 +547,17 @@ static Reg nd_pick_cache_victim(NativeDirectTarget* d, NativeAllocClass cls) { * evict the LRU non-pinned cached local. REG_NONE means use the frame-only * path. */ static Reg nd_cache_alloc(NativeDirectTarget* d, NativeAllocClass cls) { - const NativeAllocClassInfo* ci = nd_class_info(d, cls); - u32 caller = nd_caller_saved_mask(d, cls); + u32 caller = d->caller_saved[cls]; + const Reg* sub = d->caller_alloc[cls]; + u32 nsub = d->ncaller_alloc[cls]; Reg victim; - for (u32 i = 0; i < ci->nallocable; ++i) { - Reg r = ci->allocable[i]; - if (r >= 32u) continue; - if ((caller & (1u << r)) && d->reg_owner[cls][r] == CG_LOCAL_NONE && + /* The cache only ever allocates caller-saved registers, so scan the + * precomputed caller-saved-allocable sublist (allocable order preserved) and + * skip the always-failing callee-saved candidates. First match is identical + * to the former full-allocable scan. */ + for (u32 i = 0; i < nsub; ++i) { + Reg r = sub[i]; + if (d->reg_owner[cls][r] == CG_LOCAL_NONE && (d->scratch_used[cls] & (1u << r)) == 0) { nd_note_reg_used(d, cls, r); return r; @@ -1919,6 +1927,23 @@ CgTarget* native_direct_target_new(Compiler* c, ObjBuilder* obj, } } + /* Precompute, per class, the caller-saved-allocable sublist (in allocable + * order) and the live-ABI caller-saved mask, both constant for the program. + * The local register cache only allocates caller-saved registers, so the + * free-reg scan iterates this short list and avoids re-querying the mask. */ + for (u32 cls = 0; cls < 3u; ++cls) { + const NativeAllocClassInfo* ci = d->class_info[cls]; + u32 mask, n = 0; + if (!ci) continue; + mask = nd_caller_saved_mask(d, (NativeAllocClass)cls); + d->caller_saved[cls] = mask; + for (u32 i = 0; i < ci->nallocable; ++i) { + Reg r = ci->allocable[i]; + if (r < 32u && (mask & (1u << r))) d->caller_alloc[cls][n++] = r; + } + d->ncaller_alloc[cls] = n; + } + d->base.func_begin = nd_func_begin; d->base.func_end = nd_func_end; d->base.alias = nd_alias; diff --git a/src/cg/native_direct_target.h b/src/cg/native_direct_target.h @@ -120,6 +120,15 @@ struct NativeDirectTarget { const NativeRegInfo* reg_info; const NativeAllocClassInfo* class_info[3]; + /* Per-class caller-saved-allocable sublist, precomputed at construction in + * `allocable[]` order (the cache only ever allocates caller-saved registers, + * so the free-reg scan iterates this short list instead of all allocable + * registers, and avoids re-querying the live-ABI caller-saved mask per call). + * caller_saved[cls] is that same mask cached. */ + Reg caller_alloc[3][32]; + u32 ncaller_alloc[3]; + u32 caller_saved[3]; + const CGFuncDesc* func; SrcLoc loc; @@ -146,6 +155,13 @@ struct NativeDirectTarget { * NativeDirectLocal. See doc/CODEGEN.md "local register cache". */ CGLocal reg_owner[3][32]; u32 use_tick; /* monotonic counter stamped onto NativeDirectLocal.last_use */ + /* Flat mirror of the currently-owning local's last_use, keyed by physical + * register, so the LRU victim scan reads one contiguous array instead of + * chasing a NativeDirectLocal per candidate. Written by nd_touch_local + * whenever an owned register's last_use changes; only read for registers with + * reg_owner[cls][reg] != CG_LOCAL_NONE, where it equals that local's + * last_use. */ + u32 reg_last_use[3][32]; /* Head/tail of the intrusive cached-locals list (in caching order), -1 when * empty; ncached is its length. Lets nd_flush_all run in O(cached) instead of * scanning all nlocals on every control-flow / barrier op; cache_tail makes diff --git a/src/cg/value.c b/src/cg/value.c @@ -1,5 +1,15 @@ #include "cg/internal.h" +/* The cached wide-class tag (ApiBitField.wide_kind) reuses the bit-field + * member's trailing pad: ApiBitField stays at its original 12-byte footprint + * (u16 + u16 + u32 + u8 bit_signed + u8 wide_kind + u8 pad[2]), and the node has + * no slack past it, so the value-stack node size is unchanged. */ +_Static_assert(sizeof(ApiBitField) == 12, + "wide_kind must reuse ApiBitField pad, not grow it"); +_Static_assert(offsetof(ApiSValue, bitfield) + sizeof(ApiBitField) == + sizeof(ApiSValue), + "ApiSValue size must be unchanged by the cached wide_kind tag"); + int api_type_is_float(Compiler* c, KitCgTypeId ty) { const CgType* cg; ty = api_unalias_type(c, ty); @@ -38,6 +48,55 @@ int api_is_wide8_scalar_type(Compiler* c, KitCgTypeId ty) { (ti.scalar_kind == ABI_SC_INT || ti.scalar_kind == ABI_SC_FLOAT); } +/* Classify a value type into its wide / soft-float dispatch class once, so the + * stack-top predicates in arith.c become a cached-byte compare instead of a + * fresh gauntlet of type-get + alias-chase round trips per operand. Called from + * api_push (the sole stack writer); the result rides on the node (the bitfield + * slack) for the value's lifetime, which is sound because the class is a pure + * function of the type plus the target's fixed float-ABI / split-lane policy, + * and the type never changes after a push. + * + * The classes are mutually exclusive and reproduce the original predicates + * exactly: + * WK_I128 == api_is_i128_type + * WK_F128 == api_is_f128_type + * WK_WIDE8 == api_int_is_wide64 (int-width-bearing wide8, excluding i128) + * WK_SOFT_DOUBLE == api_type_is_soft_double (f64 under SOFT/SINGLE float-abi) + * WK_SOFT_SINGLE == api_type_is_soft_single (f32 under SOFT float-abi) + * They cannot overlap: i128/wide8 are integer, f128/soft-double/soft-single are + * float, and the three float classes have distinct widths (128/64/32). A wide8 + * float (e.g. double on a 32-bit ABI) bears int width 0 so it is not WK_WIDE8 — + * it falls through to WK_SOFT_DOUBLE, exactly as the original int-width guard in + * api_int_is_wide64 intended. */ +u8 api_wide_kind_for(KitCg* g, KitCgTypeId ty) { + Compiler* c; + u8 fa; + if (!g || !ty) return WK_NARROW; + c = g->c; + if (api_is_i128_type(c, ty)) return WK_I128; + if (api_is_f128_type(c, ty)) return WK_F128; + /* Int split into two 32-bit lanes by the ABI. The original api_int_is_wide64 + * excludes i128 (handled above) and requires a nonzero int-like width, so a + * split-lane *float* never matches here. */ + if (kit_cg_type_int_width((KitCompiler*)c, ty) != 0 && + api_is_wide8_scalar_type(c, ty)) { + return WK_WIDE8; + } + fa = c->target.float_abi; + /* Soft double: f64 where the target has no hardware double (SOFT or SINGLE; + * under SINGLE only single-precision is in FP regs, double is always soft). */ + if ((fa == KIT_FLOAT_ABI_SOFT || fa == KIT_FLOAT_ABI_SINGLE) && + kit_cg_type_float_width((KitCompiler*)c, ty) == 64) { + return WK_SOFT_DOUBLE; + } + /* Soft single: f32 under pure-soft float-abi (no FP unit at all). */ + if (fa == KIT_FLOAT_ABI_SOFT && + kit_cg_type_float_width((KitCompiler*)c, ty) == 32) { + return WK_SOFT_SINGLE; + } + return WK_NARROW; +} + Operand api_op_imm(i64 v, KitCgTypeId ty) { Operand o; memset(&o, 0, sizeof o); @@ -194,16 +253,22 @@ void api_stack_grow(KitCg* g, u32 want) { } void api_push(KitCg* g, ApiSValue v) { + KitCgTypeId ty = api_sv_type(&v); /* An aggregate (record) can only ever be a PLACE: it is addressed, loaded, * and passed by SRET/BYVAL/BYREF, never materialized as a scalar VALUE. Catch * any aggregate VALUE at the point it would enter the stack. i128/f128 are * scalars (cg_type_is_aggregate is false for them), so they remain valid * VALUEs and are unaffected. */ - if (cg_type_is_aggregate(g->c, api_sv_type(&v)) && !api_is_lvalue_sv(&v)) { + if (cg_type_is_aggregate(g->c, ty) && !api_is_lvalue_sv(&v)) { compiler_panic(g->c, g->cur_loc, "KitCg: aggregate must be a place, not a value; load the " "place or pass it by reference"); } + /* Cache the wide/soft-float dispatch class once, here at the sole stack + * writer; the wide and soft-float predicates in arith.c read it back instead + * of re-deriving it per operand. dup/swap/rot copy the whole node (type and + * tag together), so it stays valid for every later reader. */ + v.bitfield.wide_kind = api_wide_kind_for(g, ty); api_stack_grow(g, g->sp + 1); g->stack[g->sp++] = v; } diff --git a/src/core/buf.c b/src/core/buf.c @@ -79,7 +79,10 @@ static int buf_ensure_tail(Buf* b, size_t need) { return 0; } -void buf_write(Buf* b, const void* data, size_t n) { +/* Out-of-line tail of buf_write: handles the empty-tail and chunk-spanning + * cases. The common single-chunk append is inlined in buf.h; only writes that + * don't fit the current tail (or hit an empty/NULL tail) land here. */ +void buf_write_slow(Buf* b, const void* data, size_t n) { const u8* p = (const u8*)data; while (n) { size_t avail; diff --git a/src/core/buf.h b/src/core/buf.h @@ -1,6 +1,8 @@ #ifndef KIT_BUF_H #define KIT_BUF_H +#include <string.h> + #include "core/core.h" #include "core/heap.h" @@ -39,7 +41,25 @@ typedef struct Buf { void buf_init(Buf*, Heap*); void buf_fini(Buf*); -void buf_write(Buf*, const void* data, size_t n); +/* Out-of-line body for the empty-tail / chunk-spanning case. */ +void buf_write_slow(Buf*, const void* data, size_t n); + +/* Append n bytes. Inlined fast path: when the bytes fit the current tail chunk + * (the overwhelmingly common code-emit case — chunks hold 64KB), this is a bare + * memcpy + two field bumps with no call. Empty-tail / spanning writes fall to + * buf_write_slow. Behaviour is byte-for-byte identical to the old out-of-line + * loop; only the call/loop scaffolding is removed from the hot case. */ +static inline void buf_write(Buf* b, const void* data, size_t n) { + BufChunk* t = b->tail; + if (t && (size_t)(t->cap - t->used) >= n) { + memcpy(t->data + t->used, data, n); + t->used += (u32)n; + b->total += (u32)n; + return; + } + buf_write_slow(b, data, n); +} + u8* buf_reserve(Buf*, size_t n); /* contiguous; spills to a fresh chunk if needed */ u32 buf_pos(const Buf*); diff --git a/src/core/pool.c b/src/core/pool.c @@ -29,7 +29,19 @@ static u32 fnv1a(const char* s, size_t len) { } static int sym_eq(const PoolEntry* e, const char* s, size_t len, u32 h) { - return e->hash == h && e->len == (u32)len && memcmp(e->data, s, len) == 0; + /* Inline byte loop instead of memcmp: the library is built -ffreestanding, + * under which clang emits a real memcmp libcall for a runtime length rather + * than inlining it. For the short identifiers the frontend interns, the call + * dispatch dominates the few-byte compare. The hash+len guards reject almost + * all non-matches before any byte is read. No word-at-a-time read: `s` points + * into the borrowed lexer buffer with no trailing-NUL/padding guarantee, so an + * over-wide load could overread at EOF; the byte loop is the safe form (and + * matches include/kit/core.h kit_slice_eq). */ + size_t i; + if (e->hash != h || e->len != (u32)len) return 0; + for (i = 0; i < len; ++i) + if (e->data[i] != s[i]) return 0; + return 1; } static void table_rehash(Pool* p, u32 new_cap) { @@ -50,6 +62,13 @@ static void table_rehash(Pool* p, u32 new_cap) { if (p->table) p->heap->free(p->heap, p->table, sizeof(Sym) * p->cap); p->table = new_table; p->cap = new_cap; + /* Precompute the grow trigger so the per-intern load-factor test is a single + * compare (no multiplies). cap is always a power of two >= INITIAL_TABLE_CAP, + * so cap*LOAD_NUM is divisible by LOAD_DEN and this is exact: the trigger + * `used >= grow_threshold` fires at the same point as `used*DEN >= cap*NUM`. + * u64 in the product avoids overflow when cap is large. */ + p->grow_threshold = + (u32)((u64)new_cap * POOL_TABLE_LOAD_NUM / POOL_TABLE_LOAD_DEN); } static int entries_grow(Pool* p) { @@ -105,7 +124,7 @@ Sym pool_intern_slice(Pool* p, Slice in) { Sym sym; if (!s || len == 0) return 0; - if (p->used * POOL_TABLE_LOAD_DEN >= p->cap * POOL_TABLE_LOAD_NUM) { + if (p->used >= p->grow_threshold) { table_rehash(p, p->cap * 2); } h = fnv1a(s, len); @@ -131,7 +150,16 @@ Sym pool_intern_slice(Pool* p, Slice in) { { char* dst = (char*)arena_alloc(&p->arena, len + 1, 1); if (!dst) return 0; - memcpy(dst, s, len); + /* Inline copy for the short-string common case: under -ffreestanding the + * memcpy is a libcall, and interned spellings are short (a few bytes), so + * the inline loop wins the call dispatch. Fall back to memcpy for the rare + * long spelling (e.g. a big string-literal token routed through intern). */ + if (len <= 32) { + size_t i; + for (i = 0; i < len; ++i) dst[i] = s[i]; + } else { + memcpy(dst, s, len); + } dst[len] = '\0'; sym = (Sym)p->nentries++; p->entries[sym].data = dst; diff --git a/src/core/pool.h b/src/core/pool.h @@ -20,6 +20,7 @@ struct Pool { Sym* table; u32 cap; /* always a power of two */ u32 used; + u32 grow_threshold; /* precomputed cap*LOAD_NUM/LOAD_DEN; grow when used>= it */ /* Sym → string mapping. Index 0 reserved as Sym = 0 ("none"). */ PoolEntry* entries; diff --git a/src/obj/obj.c b/src/obj/obj.c @@ -123,7 +123,12 @@ ObjBuilder* obj_new(Compiler* c) { Relocs_init(&ob->relocs, h); Groups_init(&ob->groups, h); Atoms_init(&ob->atoms, h); - SymNameIndex_init(&ob->sym_by_name, h); + /* Pre-size the symbol-name index: a typical TU has hundreds of symbols, and + * the default 16-slot map would otherwise resize/rehash ~4 times (16->256) + * while declarations stream in. 256 (a power of two, holds 192 at the 3/4 + * load factor) skips that early cascade. Order-preserving — resize reinserts + * and the first-wins set at obj_symbol_make is unchanged. */ + SymNameIndex_init_cap(&ob->sym_by_name, h, 256u); SecKeyIndex_init(&ob->sec_by_key, h); WeakAliases_init(&ob->weak_aliases, h); @@ -620,6 +625,18 @@ void obj_write(ObjBuilder* ob, ObjSecId id, const void* data, size_t n) { buf_write(&s->bytes, data, n); } +/* See obj.h: the byte buffer for a PROGBITS section, NULL for NOBITS/.bss (so + * the caller falls back to obj_write's bss_size path) or invalid ids. Lets the + * MCEmitter hoist the per-emit Sections_at deref + nobits branch out of the hot + * loop, re-resolving only at set_section. */ +Buf* obj_section_bytes(ObjBuilder* ob, ObjSecId id) { + Section* s; + if (id == OBJ_SEC_NONE) return NULL; + s = Sections_at(&ob->sections, id); + if (!s || sec_is_nobits(s)) return NULL; + return &s->bytes; +} + u8* obj_reserve(ObjBuilder* ob, ObjSecId id, size_t n) { Section* s; if (id == OBJ_SEC_NONE) return NULL; diff --git a/src/obj/obj.h b/src/obj/obj.h @@ -433,6 +433,12 @@ void obj_section_set_addr(ObjBuilder*, ObjSecId, u64 addr); void obj_section_set_ext(ObjBuilder*, ObjSecId, ObjExtKind, u32 ext_type, u32 ext_flags); void obj_write(ObjBuilder*, ObjSecId section_id, const void* data, size_t n); +/* Resolve a section's byte buffer for direct emit. Returns NULL for the + * OBJ_SEC_NONE sentinel, an unknown id, or a NOBITS/.bss section (which stores + * only a size, not bytes — callers must route those through obj_write so the + * bss_size accounting runs). The returned pointer is stable: SegVec elements do + * not move, so callers may cache it until the next set_section. */ +Buf* obj_section_bytes(ObjBuilder*, ObjSecId section_id); u8* obj_reserve(ObjBuilder*, ObjSecId section_id, size_t n); void obj_reserve_bss(ObjBuilder*, ObjSecId section_id, u32 size, u32 align); /* Pad `section_id` to `align`, returning the resulting offset. For