kit

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

commit 9670b86965fba2c727db42255967f7c64b27e0f0
parent 442fdc5555d055f48a12b59956a60177ad9413a8
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 13 Jun 2026 00:51:10 -0700

perf(obj): index relocations by section (kill O(n_sec*n_reloc) emit scan)

The native object emitters (macho/elf/coff) each rescanned the entire flat
reloc table once per section to build per-section relocation tables —
O(n_sections * n_relocs), a latent superlinear axis that grows with
-ffunction-sections and per-literal .rodata fan-out (PERF-TCC-SLIM.md D6).

Add a lazily-built per-section reloc index on ObjBuilder: a counting sort of
the live relocs keyed by section_id that preserves ascending global order
within each section. obj_reloc_count becomes O(1) and a new obj_reloc_section
yields a section's live reloc indices directly, so each emitter iterates only
its own relocs. The index is invalidated on reloc add and on obj_sweep_dead.

The macho symdiff (ADD/SUB) pairing still peeks the global next (ri+1) exactly
as before and skips the SUB partner only when it is globally adjacent in the
bucket, preserving byte-identical output.

Gate: scripts/perf_identity_gate.sh byte-identical across all categories;
test-elf/macho/ar/link green under ASan/UBSan.

Diffstat:
Msrc/obj/coff/emit.c | 11+++++------
Msrc/obj/elf/emit.c | 12+++++-------
Msrc/obj/macho/emit.c | 15++++++++++-----
Msrc/obj/obj.c | 105++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/obj/obj.h | 5+++++
5 files changed, 124 insertions(+), 24 deletions(-)

diff --git a/src/obj/coff/emit.c b/src/obj/coff/emit.c @@ -343,7 +343,6 @@ void emit_coff(Compiler* c, ObjBuilder* ob, Writer* w) { /* COFF stores NumberOfRelocations as u16; sections with > 65535 * relocs use the IMAGE_SCN_LNK_NRELOC_OVFL extension which we don't * implement in v1. Panic if any single section exceeds the limit. */ - u32 total_relocs = obj_reloc_total(ob); for (u32 ci = 0; ci < nsecs; ++ci) { CSec* cs = &secs[ci]; u32 nr = obj_reloc_count(ob, cs->obj_sec); @@ -569,15 +568,15 @@ void emit_coff(Compiler* c, ObjBuilder* ob, Writer* w) { /* ---- pass 4: build per-section relocation tables --------------- */ for (u32 ci = 0; ci < nsecs; ++ci) { CSec* cs = &secs[ci]; - u32 nr = cs->number_of_relocations; + u32 nr; + const u32* rix = obj_reloc_section(ob, cs->obj_sec, &nr); if (!nr) continue; u8* buf = (u8*)arena_alloc(c->scratch, (size_t)COFF_RELOC_SIZE * nr, _Alignof(u32)); u32 j = 0; - for (u32 ri = 0; ri < total_relocs; ++ri) { - const Reloc* r = obj_reloc_at(ob, ri); - if (r->removed) continue; - if (r->section_id != cs->obj_sec) continue; + /* rix[] lists this section's live relocs in ascending global order. */ + for (u32 k = 0; k < nr; ++k) { + const Reloc* r = obj_reloc_at(ob, rix[k]); if (r->sym == OBJ_SYM_NONE) { compiler_panic(c, SRCLOC_NONE, "emit_coff: reloc without symbol not supported " diff --git a/src/obj/elf/emit.c b/src/obj/elf/emit.c @@ -454,8 +454,6 @@ void emit_elf(Compiler* c, ObjBuilder* ob, Writer* w) { /* ---- pass 3: build .rela.<name> contents ------------------------ */ /* Allocate one .rela section per obj section that has any relocs. */ - u32 total_relocs = obj_reloc_total(ob); - typedef struct RelaPlan { u32 obj_section; /* obj section the rela applies to */ u8* bytes; /* arena-allocated rela bytes */ @@ -468,15 +466,15 @@ void emit_elf(Compiler* c, ObjBuilder* ob, Writer* w) { for (u32 si = 1; si < nobjsec; ++si) { const Section* host = obj_section_get(ob, si); if (!host || host->removed) continue; - u32 nr = obj_reloc_count(ob, si); + u32 nr; + const u32* rix = obj_reloc_section(ob, si, &nr); if (!nr) continue; u8* buf = (u8*)arena_alloc(c->scratch, (size_t)rela_size * nr, _Alignof(u64)); u32 j = 0; - for (u32 i = 0; i < total_relocs; ++i) { - const Reloc* r = obj_reloc_at(ob, i); - if (r->removed) continue; - if (r->section_id != si) continue; + /* rix[] lists section si's live relocs in ascending global order. */ + for (u32 k = 0; k < nr; ++k) { + const Reloc* r = obj_reloc_at(ob, rix[k]); u32 etype = reloc_to(r->kind); if (etype == ELF_R_AARCH64_NONE /* == ELF_R_X86_64_NONE == 0 */ && r->kind != R_NONE) { diff --git a/src/obj/macho/emit.c b/src/obj/macho/emit.c @@ -537,17 +537,20 @@ void emit_macho(Compiler* c, ObjBuilder* ob, Writer* w) { u32 total_relocs = obj_reloc_total(ob); for (u32 i = 0; i < nsecs; ++i) { MSec* m = &secs[i]; - u32 nr = obj_reloc_count(ob, m->obj_sec); + u32 nr; + const u32* rix = obj_reloc_section(ob, m->obj_sec, &nr); if (!nr) continue; /* Worst case: each reloc may be preceded by an ARM64_RELOC_ADDEND * pair entry. We size the buffer for that upper bound. */ u8* buf = (u8*)arena_alloc(c->scratch, (size_t)MACHO_RELOC_SIZE * nr * 2, _Alignof(u32)); u32 j = 0; - for (u32 ri = 0; ri < total_relocs; ++ri) { + /* rix[] lists this section's live relocs in ascending global order, so + * each is in-section and non-removed by construction. The symdiff + * pairing below still peeks the *global* next (ri+1) exactly as before. */ + for (u32 k = 0; k < nr; ++k) { + u32 ri = rix[k]; const Reloc* r = obj_reloc_at(ob, ri); - if (r->removed) continue; - if (r->section_id != m->obj_sec) continue; if ((r->kind == R_ADD8 || r->kind == R_ADD16 || r->kind == R_ADD32 || r->kind == R_ADD64) && ri + 1u < total_relocs) { @@ -597,7 +600,9 @@ void emit_macho(Compiler* c, ObjBuilder* ob, Writer* w) { ((unsigned_type & 0xfu) << 28)); ++j; } - ++ri; + /* Skip the SUB partner (global ri+1) — it is this section's next + * live reloc iff it is globally adjacent in the bucket. */ + if (k + 1u < nr && rix[k + 1u] == ri + 1u) ++k; continue; } } diff --git a/src/obj/obj.c b/src/obj/obj.c @@ -100,6 +100,19 @@ struct KitObjBuilder { /* Linked-image view (segments + dynamic info). NULL on relocatable * inputs; lazily created by obj_image_ensure. See obj.h. */ ObjImage* image; + /* Per-section reloc index (lazy cache). Without it, every native emitter + * rescans the whole flat reloc table once per section — O(n_sections * + * n_relocs), a latent superlinear axis that grows with -ffunction-sections + * and literal fan-out. `reloc_index` holds the global indices of live + * relocs grouped by section in ascending global order; `reloc_index_off` + * is the prefix-offset array (len reloc_index_nsec+1) so section `s`'s run + * is reloc_index[off[s] .. off[s+1]). Rebuilt by obj_reloc_index_ensure + * when `reloc_index_dirty` (set on reloc add and on obj_sweep_dead). */ + u32* reloc_index; + u32* reloc_index_off; + u32 reloc_index_nsec; /* #sections reloc_index_off was sized for */ + u32 reloc_index_len; /* #live relocs in reloc_index */ + u8 reloc_index_dirty; }; struct ObjSymIter { @@ -194,6 +207,12 @@ void obj_free(ObjBuilder* ob) { SecKeyIndex_fini(&ob->sec_by_key); WeakAliases_fini(&ob->weak_aliases); obj_image_free_(ob); + if (ob->reloc_index) + ob->heap->free(ob->heap, ob->reloc_index, + sizeof(u32) * ob->reloc_index_len); + if (ob->reloc_index_off) + ob->heap->free(ob->heap, ob->reloc_index_off, + sizeof(u32) * (ob->reloc_index_nsec + 1u)); ob->heap->free(ob->heap, ob, sizeof(*ob)); } @@ -819,6 +838,7 @@ void obj_reloc_ex(ObjBuilder* ob, ObjSecId section_id, u32 offset, r->pair = (u8)pair; r->sym = sym; r->addend = addend; + ob->reloc_index_dirty = 1; /* invalidate the per-section reloc index */ /* Any reloc against this symbol is enough to retain it through the * emit-time UNDEF prune. See ObjSym::referenced. */ obj_sym_mark_referenced(ob, sym); @@ -1105,10 +1125,76 @@ void obj_sweep_dead(ObjBuilder* ob) { if (!lk || lk->removed) s->link = OBJ_SEC_NONE; } } + + /* Removed flags changed — the per-section reloc index must rebuild. */ + ob->reloc_index_dirty = 1; } /* ---- read side ---- */ +/* (Re)build the per-section reloc index: a counting sort of the live relocs + * keyed by section_id, preserving ascending global order within each section. + * O(n_relocs + n_sections). Idempotent while the index stays clean; callers + * reach it through obj_reloc_count / obj_reloc_section. Cast away const at the + * call sites — the index is a lazily-filled cache, not logical state. */ +static void obj_reloc_index_ensure(ObjBuilder* ob) { + u32 nsec, total, i, nlive; + u32* off; + if (ob->reloc_index_off && !ob->reloc_index_dirty) return; + nsec = Sections_count(&ob->sections); + total = Relocs_count(&ob->relocs); + + if (!ob->reloc_index_off || ob->reloc_index_nsec != nsec) { + if (ob->reloc_index_off) + ob->heap->free(ob->heap, ob->reloc_index_off, + sizeof(u32) * (ob->reloc_index_nsec + 1u)); + off = (u32*)ob->heap->alloc(ob->heap, sizeof(u32) * (nsec + 1u), + _Alignof(u32)); + ob->reloc_index_off = off; + ob->reloc_index_nsec = nsec; + } else { + off = ob->reloc_index_off; + } + memset(off, 0, sizeof(u32) * (nsec + 1u)); + + /* Pass 1: tally live relocs into off[section_id + 1]. */ + nlive = 0; + for (i = 0; i < total; ++i) { + const Reloc* r = Relocs_at(&ob->relocs, i); + if (r->removed) continue; + ++off[r->section_id + 1u]; + ++nlive; + } + /* Prefix-sum: off[s] = start of section s's run; off[nsec] = nlive. */ + for (i = 1; i <= nsec; ++i) off[i] += off[i - 1u]; + + if (!ob->reloc_index || ob->reloc_index_len != nlive) { + if (ob->reloc_index) + ob->heap->free(ob->heap, ob->reloc_index, + sizeof(u32) * ob->reloc_index_len); + ob->reloc_index = + nlive ? (u32*)ob->heap->alloc(ob->heap, sizeof(u32) * nlive, + _Alignof(u32)) + : NULL; + ob->reloc_index_len = nlive; + } + + /* Pass 2: scatter global indices into per-section runs. A heap-temp cursor + * (seeded from off[]) keeps the prefix offsets intact for the readers. */ + if (nlive) { + u32* cur = (u32*)ob->heap->alloc(ob->heap, sizeof(u32) * (nsec + 1u), + _Alignof(u32)); + memcpy(cur, off, sizeof(u32) * (nsec + 1u)); + for (i = 0; i < total; ++i) { + const Reloc* r = Relocs_at(&ob->relocs, i); + if (r->removed) continue; + ob->reloc_index[cur[r->section_id]++] = i; + } + ob->heap->free(ob->heap, cur, sizeof(u32) * (nsec + 1u)); + } + ob->reloc_index_dirty = 0; +} + u32 obj_section_count(const ObjBuilder* ob) { return Sections_count(&ob->sections); } @@ -1119,13 +1205,20 @@ const Section* obj_section_get(const ObjBuilder* ob, ObjSecId id) { } u32 obj_reloc_count(const ObjBuilder* ob, ObjSecId id) { - u32 i, total = Relocs_count(&ob->relocs), n = 0; - for (i = 0; i < total; ++i) { - const Reloc* r = Relocs_at(&ob->relocs, i); - if (r->removed) continue; - if (r->section_id == id) ++n; + obj_reloc_index_ensure((ObjBuilder*)ob); + if (id >= ob->reloc_index_nsec) return 0; + return ob->reloc_index_off[id + 1u] - ob->reloc_index_off[id]; +} + +const u32* obj_reloc_section(const ObjBuilder* ob, ObjSecId id, u32* out_len) { + obj_reloc_index_ensure((ObjBuilder*)ob); + if (id >= ob->reloc_index_nsec) { + if (out_len) *out_len = 0; + return NULL; } - return n; + if (out_len) + *out_len = ob->reloc_index_off[id + 1u] - ob->reloc_index_off[id]; + return ob->reloc_index + ob->reloc_index_off[id]; } u32 obj_reloc_total(const ObjBuilder* ob) { return Relocs_count(&ob->relocs); } diff --git a/src/obj/obj.h b/src/obj/obj.h @@ -604,6 +604,11 @@ const Section* obj_section_get(const ObjBuilder*, ObjSecId id); u32 obj_reloc_count(const ObjBuilder*, ObjSecId section_id); u32 obj_reloc_total(const ObjBuilder*); const Reloc* obj_reloc_at(const ObjBuilder*, u32 idx); /* 0..total-1 */ +/* The global indices of section_id's live relocs, ascending (the order + * obj_reloc_at expects), via a cached per-section index — O(1) lookup instead + * of rescanning the flat table. *out_len gets the count (may be 0 -> NULL). */ +const u32* obj_reloc_section(const ObjBuilder*, ObjSecId section_id, + u32* out_len); /* Diagnostic spelling for a RelocKind. The returned pointer is a static * literal that mirrors the enum identifier without the R_ prefix (e.g.