commit bd148cea9313867a40abbbf442aaa29591a41d9c
parent 3938a2976de2f2ce3dd46fbf64a4b59ee355805c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 10 Jun 2026 17:05:08 -0700
perf: linearize C-frontend declarations, buf patch, and section dedup
The -O0 compile+link scaling benchmark (make bench-cc) found five input axes
were O(n^2): every path that adds N entries to a symbol/scope/type table did a
linear scan per item. Make them O(1)/O(log n).
- C frontend (lang/c/parse): Scope.entries, Scope.tags and the file-scope
external_funcs list were LIFO linked lists scanned by scope_lookup*,
tag_lookup*, external_func_* and make_local_aligned's redefinition check.
Each now carries a KIT_HASHMAP_DEFINE index keyed on the interned Sym, built
lazily once a scope exceeds a small threshold (tiny block scopes stay on the
cheap list) and allocated through an arena-heap facade so there is nothing to
free.
- Chunked buffer (src/core/buf): buf_patch/buf_read walked chunks from the head
(O(N_chunks)), so single-pass emission patch-ups over a multi-MB .text were
O(n^2). Add a sorted chunk-start directory + binary search -> O(log n);
append stays O(1).
- Object builder (src/obj/obj): obj_section's find-or-create linearly scanned
every section; add a composite-key SecKeyIndex. Latent today (cc emits one
.text) but O(n^2) under -ffunction-sections.
All axes are now linear. On the formerly-O(n^2) axes kit went from 6-70x slower
than clang -O0 to as fast or faster (type-decl 70x slower -> 1.6x faster;
fn-count 7.5x -> 3x faster; ref-density 6x -> 6x faster; locals 25x faster).
Full frontend/cg/link/elf/macho/debug/dwarf/smoke suites pass under ASan/UBSan.
Diffstat:
5 files changed, 245 insertions(+), 67 deletions(-)
diff --git a/lang/c/parse/parse.c b/lang/c/parse/parse.c
@@ -287,11 +287,44 @@ u32 count_recorded_top_level_items(const Tok* vec, u32 len) {
* Scopes
* ============================================================ */
+/* A scope only builds its hashmap index once it holds more than this many
+ * entries; below it the linear LIFO scan is cheaper than a hashed lookup and
+ * needs no allocation. The bound is a constant, so even pre-index scopes are
+ * O(1) per lookup. */
+#define SCOPE_INDEX_THRESHOLD 12u
+
+/* KitHeap facade over the pool's arena: the scope/tag/external indexes allocate
+ * through this so all their storage lives in the per-TU arena and is reclaimed
+ * in bulk when the pool is freed. The file scope is never popped, so this also
+ * removes any per-scope teardown bookkeeping. `free` is a no-op (the arena owns
+ * the memory); resize orphans the old table in the arena, which is bounded by
+ * the final table size and negligible against the rest of the TU's arena use.
+ */
+static void* c_arena_heap_alloc(KitHeap* h, size_t n, size_t align) {
+ return kit_arena_alloc((KitArena*)h->user, n, align);
+}
+static void* c_arena_heap_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 c_arena_heap_free(KitHeap* h, void* p, size_t n) {
+ (void)h;
+ (void)p;
+ (void)n;
+}
+static void c_arena_heap_init(Parser* p) {
+ p->arena_heap.alloc = c_arena_heap_alloc;
+ p->arena_heap.realloc = c_arena_heap_realloc;
+ p->arena_heap.free = c_arena_heap_free;
+ p->arena_heap.user = p->pool->arena;
+}
+
Scope* scope_new(Parser* p, Scope* parent) {
Scope* s = arena_new(p->pool->arena, Scope);
if (!s) perr(p, "out of memory in scope_new");
- s->entries = NULL;
- s->tags = NULL;
+ memset(s, 0, sizeof *s);
s->parent = parent;
s->saved_vla_mark = p->vla_mark;
return s;
@@ -306,35 +339,62 @@ void scope_pop(Parser* p) {
}
}
+/* Build a scope's name->entry index from its existing LIFO list. Insert
+ * newest-first with try_insert (which keeps the first writer) so the head — the
+ * current binding a linear scan would return — wins on shadowing. */
+static void scope_entries_index_build(Parser* p, Scope* s) {
+ SymEntry* e;
+ SymEntryMap_init_cap(&s->emap, &p->arena_heap, 64u);
+ for (e = s->entries; e; e = e->next)
+ if (e->name) (void)SymEntryMap_try_insert(&s->emap, e->name, e, NULL);
+}
+
+static void scope_entries_index_put(Parser* p, Scope* s, SymEntry* e) {
+ if (!e->name)
+ return; /* anonymous: never name-looked-up, keep off the index */
+ if (s->emap.cap) {
+ (void)SymEntryMap_set(&s->emap, e->name, e); /* newest define wins */
+ } else if (s->nentries > SCOPE_INDEX_THRESHOLD) {
+ scope_entries_index_build(p, s);
+ }
+}
+
+static SymEntry* scope_entries_find(Scope* s, Sym name) {
+ SymEntry* e;
+ if (name && s->emap.cap) {
+ SymEntry** v = SymEntryMap_get(&s->emap, name);
+ return v ? *v : NULL;
+ }
+ for (e = s->entries; e; e = e->next)
+ if (e->name == name) return e;
+ return NULL;
+}
+
SymEntry* scope_define(Parser* p, Sym name, SymEntryKind kind,
const Type* type) {
SymEntry* e = arena_new(p->pool->arena, SymEntry);
+ Scope* s = p->scope;
if (!e) perr(p, "out of memory in scope_define");
memset(e, 0, sizeof *e);
e->name = name;
e->kind = (u8)kind;
e->type = type;
- e->next = p->scope->entries;
- p->scope->entries = e;
+ e->next = s->entries;
+ s->entries = e;
+ s->nentries++;
+ scope_entries_index_put(p, s, e);
return e;
}
SymEntry* scope_lookup_current(Parser* p, Sym name) {
- SymEntry* e;
- if (!p->scope) return NULL;
- for (e = p->scope->entries; e; e = e->next) {
- if (e->name == name) return e;
- }
- return NULL;
+ return p->scope ? scope_entries_find(p->scope, name) : NULL;
}
SymEntry* scope_lookup(Parser* p, Sym name) {
Scope* s;
for (s = p->scope; s; s = s->parent) {
- SymEntry* e;
- for (e = s->entries; e; e = e->next) {
- if (e->name == name) return e;
- }
+ SymEntry* e = scope_entries_find(s, name);
+ if (e) return e;
}
return NULL;
}
@@ -350,28 +410,13 @@ static void sym_set_decl(SymEntry* e, DeclId id, DeclStorage storage,
}
static SymEntry* external_func_lookup(Parser* p, Sym name) {
- ExternalFuncDecl* f;
- for (f = p->external_funcs; f; f = f->next) {
- if (f->name == name) return f->entry;
- }
- return NULL;
+ SymEntry** v = name ? ExternalFuncMap_get(&p->external_funcs, name) : NULL;
+ return v ? *v : NULL;
}
static void external_func_remember(Parser* p, Sym name, SymEntry* entry) {
- ExternalFuncDecl* f;
- if (!entry) return;
- for (f = p->external_funcs; f; f = f->next) {
- if (f->name == name) {
- f->entry = entry;
- return;
- }
- }
- f = arena_new(p->pool->arena, ExternalFuncDecl);
- if (!f) perr(p, "out of memory in external_func_remember");
- f->name = name;
- f->entry = entry;
- f->next = p->external_funcs;
- p->external_funcs = f;
+ if (!entry || !name) return;
+ (void)ExternalFuncMap_set(&p->external_funcs, name, entry); /* newest wins */
}
static int is_ordinary_decl_kind(SymEntryKind k) {
@@ -391,37 +436,61 @@ static void reject_same_scope_redefinition(Parser* p, Sym name,
perr(p, "redefinition of identifier");
}
+static void tag_index_build(Parser* p, Scope* s) {
+ TagEntry* e;
+ TagEntryMap_init_cap(&s->tmap, &p->arena_heap, 64u);
+ for (e = s->tags; e; e = e->next)
+ if (e->name) (void)TagEntryMap_try_insert(&s->tmap, e->name, e, NULL);
+}
+
+static void tag_index_put(Parser* p, Scope* s, TagEntry* e) {
+ if (!e->name) return; /* anonymous struct/union: not name-looked-up */
+ if (s->tmap.cap) {
+ (void)TagEntryMap_set(&s->tmap, e->name, e);
+ } else if (s->ntags > SCOPE_INDEX_THRESHOLD) {
+ tag_index_build(p, s);
+ }
+}
+
+static TagEntry* tag_find_in(Scope* s, Sym name) {
+ TagEntry* e;
+ if (name && s->tmap.cap) {
+ TagEntry** v = TagEntryMap_get(&s->tmap, name);
+ return v ? *v : NULL;
+ }
+ for (e = s->tags; e; e = e->next)
+ if (e->name == name) return e;
+ return NULL;
+}
+
TagEntry* tag_define(Parser* p, Sym name, TagDeclKind kind, Type* type,
int complete) {
TagEntry* e = arena_new(p->pool->arena, TagEntry);
+ Scope* s = p->scope;
if (!e) perr(p, "out of memory in tag_define");
memset(e, 0, sizeof *e);
e->name = name;
e->kind = (u8)kind;
e->complete = (u8)(complete ? 1 : 0);
e->type = type;
- e->next = p->scope->tags;
- p->scope->tags = e;
+ e->next = s->tags;
+ s->tags = e;
+ s->ntags++;
+ tag_index_put(p, s, e);
return e;
}
TagEntry* tag_lookup(Parser* p, Sym name) {
Scope* s;
for (s = p->scope; s; s = s->parent) {
- TagEntry* e;
- for (e = s->tags; e; e = e->next) {
- if (e->name == name) return e;
- }
+ TagEntry* e = tag_find_in(s, name);
+ if (e) return e;
}
return NULL;
}
TagEntry* tag_lookup_local(Parser* p, Sym name) {
- TagEntry* e;
- for (e = p->scope->tags; e; e = e->next) {
- if (e->name == name) return e;
- }
- return NULL;
+ return tag_find_in(p->scope, name);
}
/* ============================================================
@@ -1542,6 +1611,11 @@ void parse_c(Compiler* c, Pool* pool, Pp* pp, DeclTable* decls, CG* cg,
p.default_visibility = parser_default_visibility(default_visibility);
p.auto_var_init = (u8)auto_var_init;
+ /* Index storage for the scope/tag tables and the external-function table all
+ * comes from the arena via this facade; the maps need it before first use. */
+ c_arena_heap_init(&p);
+ ExternalFuncMap_init(&p.external_funcs, &p.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]));
}
diff --git a/lang/c/parse/parse_priv.h b/lang/c/parse/parse_priv.h
@@ -6,6 +6,7 @@
#pragma once
+#include <kit/support/hashmap.h>
#include <stdarg.h>
#include <string.h>
@@ -88,7 +89,6 @@ typedef enum SymEntryKind {
typedef struct SymEntry SymEntry;
typedef struct VLABound VLABound;
-typedef struct ExternalFuncDecl ExternalFuncDecl;
typedef struct StaticReloc StaticReloc;
struct VLABound {
const Type* array_ty;
@@ -142,12 +142,6 @@ struct SymEntry {
SymEntry* next;
};
-struct ExternalFuncDecl {
- Sym name;
- SymEntry* entry;
- ExternalFuncDecl* next;
-};
-
typedef struct TagEntry TagEntry;
struct TagEntry {
Sym name;
@@ -159,12 +153,28 @@ struct TagEntry {
TagEntry* next;
};
+/* Name -> entry indexes, keyed on the interned Sym (Sym 0 = "none" doubles as
+ * the hashmap empty sentinel). Each scope keeps its LIFO list as the source of
+ * truth and ordering, and lazily builds an index once it grows past
+ * SCOPE_INDEX_THRESHOLD, so the O(n^2) "scan the whole scope per declaration"
+ * cost (file scopes with thousands of globals/typedefs, functions with
+ * thousands of locals) collapses to O(1) average lookups while tiny block
+ * scopes stay on the cheap linear list. The maps allocate through the parser's
+ * arena-heap facade (see parse.c) so there is nothing to free. */
+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);
+
typedef struct Scope Scope;
struct Scope {
SymEntry* entries; /* LIFO */
TagEntry* tags; /* LIFO */
Scope* parent;
u32 saved_vla_mark;
+ u32 nentries; /* count in `entries`; index built once it exceeds threshold */
+ u32 ntags; /* count in `tags`; index built once it exceeds threshold */
+ SymEntryMap emap; /* name -> entry; active (cap != 0) once built */
+ TagEntryMap tmap; /* name -> tag; active (cap != 0) once built */
};
/* ============================================================
@@ -308,7 +318,11 @@ typedef struct Parser {
Sym sym_a_signal_fence;
Scope* scope;
- ExternalFuncDecl* external_funcs;
+ /* name -> current file-scope function entry. Replaces what was an O(n) linear
+ * list walked on every function declaration/reference. */
+ ExternalFuncMap external_funcs;
+ /* KitHeap facade over pool->arena backing the scope/tag/external indexes. */
+ KitHeap arena_heap;
CGLabel cur_break;
CGLabel cur_continue;
diff --git a/src/core/buf.c b/src/core/buf.c
@@ -1,6 +1,8 @@
-/* Chunked byte buffer. Append is O(1) (spills to a fresh chunk when the
- * tail fills). Random-access patch is O(N_chunks) — sections rarely
- * cross more than a handful of chunks, so a linear walk is fine. */
+/* Chunked byte buffer. Append is O(1) (spills to a fresh chunk when the tail
+ * fills). Random-access patch/read is O(log N_chunks) via a sorted directory of
+ * chunk start offsets — large sections (e.g. a multi-megabyte .text emitted for
+ * tens of thousands of functions) span hundreds of chunks, and per-function
+ * patch-ups would otherwise be O(N_chunks) each, i.e. O(n^2) overall. */
#include "core/buf.h"
@@ -21,6 +23,9 @@ void buf_init(Buf* b, Heap* h) {
b->head = NULL;
b->tail = NULL;
b->total = 0;
+ b->dir = NULL;
+ b->ndir = 0;
+ b->dir_cap = 0;
}
void buf_fini(Buf* b) {
@@ -30,8 +35,33 @@ void buf_fini(Buf* b) {
b->heap->free(b->heap, c, sizeof(BufChunk) + c->cap);
c = next;
}
+ if (b->dir)
+ b->heap->free(b->heap, b->dir, (size_t)b->dir_cap * sizeof(BufChunkRef));
b->head = b->tail = NULL;
b->total = 0;
+ b->dir = NULL;
+ b->ndir = b->dir_cap = 0;
+}
+
+/* Record a newly created chunk in the directory. Best-effort: on allocation
+ * failure the chunk is still reachable via the `next` chain, so buf_walk stays
+ * correct (just slower for offsets past the last indexed chunk). */
+static void buf_dir_push(Buf* b, BufChunk* c, u32 start) {
+ if (b->ndir == b->dir_cap) {
+ u32 ncap = b->dir_cap ? b->dir_cap * 2u : 16u;
+ BufChunkRef* nd = (BufChunkRef*)b->heap->alloc(
+ b->heap, (size_t)ncap * sizeof(BufChunkRef), _Alignof(BufChunkRef));
+ if (!nd) return;
+ if (b->dir) {
+ memcpy(nd, b->dir, (size_t)b->ndir * sizeof(BufChunkRef));
+ b->heap->free(b->heap, b->dir, (size_t)b->dir_cap * sizeof(BufChunkRef));
+ }
+ b->dir = nd;
+ b->dir_cap = ncap;
+ }
+ b->dir[b->ndir].start = start;
+ b->dir[b->ndir].chunk = c;
+ b->ndir++;
}
static int buf_ensure_tail(Buf* b, size_t need) {
@@ -41,6 +71,8 @@ static int buf_ensure_tail(Buf* b, size_t need) {
cap = need > BUF_CHUNK ? need : BUF_CHUNK;
c = chunk_new(b->heap, cap);
if (!c) return 1;
+ /* The new chunk starts at the current total (all prior chunks are frozen). */
+ buf_dir_push(b, c, b->total);
if (!b->head) b->head = c;
if (b->tail) b->tail->next = c;
b->tail = c;
@@ -79,11 +111,28 @@ u32 buf_pos(const Buf* b) { return b->total; }
*
* `direction`: 0 = copy from `external` into chunk; 1 = copy from chunk
* into `external`. Inlined at both call sites by clang. */
-static inline void buf_walk(BufChunk* head, u32 ofs, void* external, size_t n,
+static inline void buf_walk(const Buf* b, u32 ofs, void* external, size_t n,
int from_chunk) {
- BufChunk* c = head;
- u32 chunk_start = 0;
+ BufChunk* c;
+ u32 chunk_start;
u8* ext = (u8*)external;
+ /* Binary-search the directory for the last chunk whose start is <= ofs, then
+ * walk forward from there (the range may span into following chunks). */
+ u32 lo = 0, hi = b->ndir;
+ while (lo < hi) {
+ u32 mid = lo + ((hi - lo) >> 1);
+ if (b->dir[mid].start <= ofs)
+ lo = mid + 1;
+ else
+ hi = mid;
+ }
+ if (lo == 0) {
+ c = b->head;
+ chunk_start = 0;
+ } else {
+ c = b->dir[lo - 1].chunk;
+ chunk_start = b->dir[lo - 1].start;
+ }
while (c && n) {
u32 chunk_end = chunk_start + c->used;
if (ofs < chunk_end) {
@@ -107,11 +156,11 @@ static inline void buf_walk(BufChunk* head, u32 ofs, void* external, size_t n,
}
void buf_patch(Buf* b, u32 ofs, const void* data, size_t n) {
- buf_walk(b->head, ofs, (void*)data, n, /*from_chunk=*/0);
+ buf_walk(b, ofs, (void*)data, n, /*from_chunk=*/0);
}
void buf_read(const Buf* b, u32 ofs, void* dst, size_t n) {
- buf_walk(b->head, ofs, dst, n, /*from_chunk=*/1);
+ buf_walk(b, ofs, dst, n, /*from_chunk=*/1);
}
void buf_flatten(const Buf* b, u8* dst) {
diff --git a/src/core/buf.h b/src/core/buf.h
@@ -14,11 +14,26 @@ struct BufChunk {
u8 data[];
};
+/* Directory entry: a chunk and the buffer offset of its first byte. Chunk
+ * starts increase monotonically, so the directory is a sorted index that lets
+ * buf_patch / buf_read binary-search to the owning chunk. */
+typedef struct BufChunkRef {
+ u32 start;
+ BufChunk* chunk;
+} BufChunkRef;
+
typedef struct Buf {
Heap* heap;
BufChunk* head;
BufChunk* tail;
u32 total; /* sum of used across all chunks */
+ /* Sorted index of chunk start offsets, appended to as chunks are created.
+ * Turns random-access patch/read from O(n_chunks) (walk from head) into
+ * O(log n_chunks). It is purely an accelerator: if a push ever fails, the
+ * forward `next` walk from the nearest indexed chunk still finds the byte. */
+ BufChunkRef* dir;
+ u32 ndir;
+ u32 dir_cap;
} Buf;
void buf_init(Buf*, Heap*);
diff --git a/src/obj/obj.c b/src/obj/obj.c
@@ -28,6 +28,14 @@ SEGVEC_DEFINE(Symbols, ObjSym, 6); /* 64 entries per segment */
* obj_symbol_rename keeps the index exact (re-homing or dropping a renamed
* symbol's entry), so the lookup never sees a stale hit. */
HASHMAP_DEFINE(SymNameIndex, Sym, ObjSymId, hash_u32);
+
+/* (name, kind) -> first PROGBITS ObjSecId. obj_section is a find-or-create that
+ * collapses repeated requests for the same logical section (e.g. one .rodata
+ * per literal) onto a single Section; without this index that find is a linear
+ * scan of every section, which is O(n^2) once a build emits many distinct
+ * sections (e.g. -ffunction-sections). The key packs (name, kind) and is always
+ * odd so it never collides with the hashmap's 0 empty-slot sentinel. */
+HASHMAP_DEFINE(SecKeyIndex, u64, ObjSecId, hash_u64);
SEGVEC_DEFINE(Relocs, Reloc, 6); /* 64 entries per segment */
SEGVEC_DEFINE(Groups, ObjGroup, 3); /* 8 entries per segment */
SEGVEC_DEFINE(Atoms, ObjAtom, 5); /* 32 entries per segment */
@@ -58,6 +66,7 @@ struct KitObjBuilder {
Groups groups; /* index 0 reserved as "none" */
Atoms atoms; /* index 0 reserved as "none" */
SymNameIndex sym_by_name; /* name -> first ObjSymId; accelerates find */
+ SecKeyIndex sec_by_key; /* (name,kind) -> first PROGBITS ObjSecId */
/* Format-specific ELF e_flags. Set by read_elf to the input's
* e_flags (e.g. on RISC-V, EF_RISCV_RVC | EF_RISCV_FLOAT_ABI_DOUBLE);
* consumed by emit_elf to round-trip. Zero when not set — emit_elf
@@ -115,6 +124,7 @@ ObjBuilder* obj_new(Compiler* c) {
Groups_init(&ob->groups, h);
Atoms_init(&ob->atoms, h);
SymNameIndex_init(&ob->sym_by_name, h);
+ SecKeyIndex_init(&ob->sec_by_key, h);
WeakAliases_init(&ob->weak_aliases, h);
/* Reserve index 0 in each id space as the "none" sentinel. SegVec
@@ -167,6 +177,7 @@ void obj_free(ObjBuilder* ob) {
Groups_fini(&ob->groups);
Atoms_fini(&ob->atoms);
SymNameIndex_fini(&ob->sym_by_name);
+ SecKeyIndex_fini(&ob->sec_by_key);
WeakAliases_fini(&ob->weak_aliases);
obj_image_free_(ob);
ob->heap->free(ob->heap, ob, sizeof(*ob));
@@ -466,6 +477,13 @@ void obj_ext_clear(ObjBuilder* ob, ObjExtKind kind) {
/* ---- write side ---- */
+/* Pack (name, kind) into a nonzero hashmap key. The low bit is always set so
+ * the key is never 0 (the empty-slot sentinel), and name/kind occupy disjoint
+ * bit ranges so distinct (name, kind) pairs never collide. */
+static u64 sec_progbits_key(Sym name, SecKind kind) {
+ return (((u64)name) << 17) | (((u64)(u16)kind) << 1) | 1u;
+}
+
ObjSecId obj_section(ObjBuilder* ob, Sym name, SecKind kind, u16 flags,
u32 align) {
/* Find-or-create by (name, kind, sem=PROGBITS). Repeated calls for the
@@ -473,12 +491,13 @@ ObjSecId obj_section(ObjBuilder* ob, Sym name, SecKind kind, u16 flags,
* .data per static initializer — collapse onto a single Section and
* accumulate bytes into it instead of emitting a fan-out of identically-
* named output sections. Merge align (max) and flags (union) so a
- * stricter requirement from a later caller wins. */
- u32 n = Sections_count(&ob->sections);
- for (u32 i = 1; i < n; ++i) {
- Section* s = Sections_at(&ob->sections, i);
- if (s && s->name == name && s->kind == (u16)kind &&
- s->sem == SSEM_PROGBITS) {
+ * stricter requirement from a later caller wins. The PROGBITS find is an
+ * O(1) index lookup (sec_by_key), kept exact by obj_section_ex. */
+ ObjSecId* hit =
+ SecKeyIndex_get(&ob->sec_by_key, sec_progbits_key(name, kind));
+ if (hit) {
+ Section* s = Sections_at(&ob->sections, *hit);
+ if (s) {
if (align > s->align) s->align = align;
s->flags = (u16)(s->flags | flags);
/* Pad to align so the next obj_reserve / obj_write lands at an
@@ -497,7 +516,7 @@ ObjSecId obj_section(ObjBuilder* ob, Sym name, SecKind kind, u16 flags,
if (dst) memset(dst, 0, pad);
}
}
- return (ObjSecId)i;
+ return *hit;
}
}
return obj_section_ex(ob, name, kind, SSEM_PROGBITS, flags, align, 0,
@@ -522,6 +541,13 @@ ObjSecId obj_section_ex(ObjBuilder* ob, Sym name, SecKind kind, SecSem sem,
s->bss_size = 0;
s->addr = 0;
buf_init(&s->bytes, ob->heap);
+ /* Index PROGBITS sections so obj_section's find is O(1). try_insert keeps the
+ * first id for a (name, kind), matching the old scan's first-match semantics
+ * (and leaving distinct same-named COMDAT sections, created directly here, to
+ * resolve to the first — exactly as the linear scan did). */
+ if (sem == SSEM_PROGBITS)
+ (void)SecKeyIndex_try_insert(&ob->sec_by_key, sec_progbits_key(name, kind),
+ (ObjSecId)id, NULL);
return (ObjSecId)id;
}