Skip to content

Commit 7808686

Browse files
authored
Merge pull request #1679 from astandrik/codex/perf-cpp-short-name-lookups
perf(lsp): index C++ short-name type lookups
2 parents 641c71f + 32ae20d commit 7808686

4 files changed

Lines changed: 280 additions & 11 deletions

File tree

‎internal/cbm/lsp/c_lsp.c‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2818,7 +2818,10 @@ static const CBMRegisteredFunc *c_lookup_member_depth(CLSPContext *ctx, const ch
28182818
const char *shortn = dot ? dot + 1 : type_qn;
28192819
size_t slen = strlen(shortn);
28202820
const char *best_qn = NULL;
2821-
for (int i = 0; i < ctx->registry->type_count; i++) {
2821+
CBMTypeNameIter it;
2822+
cbm_registry_types_by_short_name(ctx->registry, shortn, &it);
2823+
int i;
2824+
while ((i = cbm_type_name_iter_next(&it)) >= 0) {
28222825
const char *q = ctx->registry->types[i].qualified_name;
28232826
if (!q) {
28242827
continue;
@@ -5972,6 +5975,7 @@ CBMTypeRegistry *cbm_c_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, in
59725975
c_register_lsp_defs(arena, reg, "", d, 1);
59735976
}
59745977
cbm_registry_finalize(reg);
5978+
cbm_registry_build_type_short_index(reg);
59755979
reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */
59765980
return reg;
59775981
}
@@ -6055,6 +6059,7 @@ void cbm_run_c_lsp_cross(CBMArena *arena, const char *source, int source_len, co
60556059
// Finalize registry — O(1) lookups. See go_lsp.c "3c. Finalize"
60566060
// comment for the rationale (linear-scan fallback otherwise).
60576061
cbm_registry_finalize(&reg);
6062+
cbm_registry_build_type_short_index(&reg);
60586063

60596064
// Initialize context and run
60606065
CLSPContext ctx;

‎internal/cbm/lsp/type_registry.c‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,56 @@ static void build_type_embed_index(CBMTypeRegistry *reg, CBMArena *idx_arena) {
167167
reg->type_embed_entry_count = idx;
168168
}
169169

170+
/* Optional index: final qualified-name segment -> chain of TYPE indices.
171+
* Descending iteration plus prepend preserves ascending registry scan order.
172+
* Consumers opt in after the QN index captures the finalized tail boundary. */
173+
void cbm_registry_build_type_short_index(CBMTypeRegistry *reg) {
174+
if (!reg)
175+
return;
176+
reg->type_short_buckets = NULL;
177+
reg->type_short_entries = NULL;
178+
reg->type_short_bucket_count = 0;
179+
if (!reg->arena || !reg->type_qn_buckets || reg->type_qn_bucket_count <= 0)
180+
return;
181+
CBMArena *idx_arena = reg->arena;
182+
int count = 0;
183+
for (int i = 0; i < reg->type_count; i++) {
184+
if (reg->types[i].qualified_name)
185+
count++;
186+
}
187+
if (count == 0)
188+
return;
189+
int bucket_count = next_pow2(count * 2);
190+
if (bucket_count < 16)
191+
bucket_count = 16;
192+
int *buckets = (int *)cbm_arena_alloc(idx_arena, (size_t)bucket_count * sizeof(int));
193+
CBMRegistryHashEntry *entries = (CBMRegistryHashEntry *)cbm_arena_alloc(
194+
idx_arena, (size_t)count * sizeof(CBMRegistryHashEntry));
195+
if (!buckets || !entries)
196+
return;
197+
for (int i = 0; i < bucket_count; i++)
198+
buckets[i] = -1;
199+
int idx = 0;
200+
for (int i = reg->type_count - 1; i >= 0; i--) {
201+
const char *qn = reg->types[i].qualified_name;
202+
if (!qn)
203+
continue;
204+
const char *dot = strrchr(qn, '.');
205+
const char *short_name = dot ? dot + 1 : qn;
206+
uint64_t h = fnv1a(short_name);
207+
int slot = (int)(h & (uint64_t)(bucket_count - 1));
208+
entries[idx].hash = h;
209+
entries[idx].payload_index = i;
210+
entries[idx].next_index = buckets[slot];
211+
entries[idx].slot = slot;
212+
buckets[slot] = idx;
213+
idx++;
214+
}
215+
reg->type_short_buckets = buckets;
216+
reg->type_short_entries = entries;
217+
reg->type_short_bucket_count = bucket_count;
218+
}
219+
170220
/* Index: short_name -> chain of FREE-function (receiver_type==NULL) indices.
171221
* Descending-iterate + prepend for ascending chain order (as above). */
172222
static void build_type_short_index(CBMTypeRegistry *reg, CBMArena *idx_arena) {
@@ -248,6 +298,43 @@ static void build_ffunc_short_index(CBMTypeRegistry *reg, CBMArena *idx_arena) {
248298
reg->ffunc_short_entry_count = idx;
249299
}
250300

301+
void cbm_registry_types_by_short_name(const CBMTypeRegistry *reg, const char *short_name,
302+
CBMTypeNameIter *out) {
303+
out->reg = reg;
304+
out->hash = fnv1a(short_name);
305+
if (reg->type_qn_buckets && reg->type_qn_bucket_count > 0) {
306+
if (reg->type_short_buckets && reg->type_short_bucket_count > 0) {
307+
int slot = (int)(out->hash & (uint64_t)(reg->type_short_bucket_count - 1));
308+
out->chain_idx = reg->type_short_buckets[slot];
309+
out->tail_i = reg->type_qn_entry_count;
310+
} else {
311+
out->chain_idx = -1;
312+
out->tail_i = 0; /* auxiliary-index allocation failed: preserve full-scan semantics */
313+
}
314+
out->tail_end = reg->type_count;
315+
} else {
316+
out->chain_idx = -1;
317+
out->tail_i = 0;
318+
out->tail_end = reg->type_count;
319+
}
320+
}
321+
322+
int cbm_type_name_iter_next(CBMTypeNameIter *it) {
323+
const CBMTypeRegistry *reg = it->reg;
324+
while (it->chain_idx >= 0) {
325+
const CBMRegistryHashEntry *e = &reg->type_short_entries[it->chain_idx];
326+
int p = e->payload_index;
327+
uint64_t h = e->hash;
328+
it->chain_idx = e->next_index;
329+
if (h != it->hash)
330+
continue;
331+
return p;
332+
}
333+
if (it->tail_i < it->tail_end)
334+
return it->tail_i++;
335+
return -1;
336+
}
337+
251338
void cbm_registry_types_by_embedded_bare(const CBMTypeRegistry *reg, const char *bare,
252339
CBMTypeEmbedIter *out) {
253340
out->reg = reg;

‎internal/cbm/lsp/type_registry.h‎

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,14 @@ typedef struct CBMTypeRegistry {
111111
int method_bucket_count;
112112
int method_entry_count;
113113

114-
// Auxiliary short-name / embedded-type indexes (built by finalize alongside the
115-
// QN buckets). Turn the Rust trait- and free-function fallback scans from
116-
// O(type_count)/O(func_count) into O(chain). Read-only after finalize.
114+
// Auxiliary short-name / embedded-type indexes. The type short-name index is
115+
// opt-in after finalize; the others are built by finalize. Current C/C++ cross
116+
// registries opt in, while other languages incur no construction cost.
117+
// Type short-name index: fnv1a(last-'.'-segment of qualified_name) -> chain
118+
// of TYPE indices. payload_index = type index.
119+
int *type_short_buckets;
120+
CBMRegistryHashEntry *type_short_entries;
121+
int type_short_bucket_count;
117122
// Embedded-type index: fnv1a(bare last-'.'-segment of each embedded_type) -> chain
118123
// of TYPE indices declaring it. payload_index = type index (a type may appear once
119124
// per embedded entry; consumers dedup adjacent same-type via the iterator).
@@ -160,6 +165,11 @@ void cbm_registry_finalize(CBMTypeRegistry *reg);
160165
// there add GBs across a large repo (FastAPI incremental test: +1.1 GB RSS).
161166
void cbm_registry_finalize_into(CBMTypeRegistry *reg, CBMArena *idx_arena);
162167

168+
// Build the optional final-QN-segment type index. Call immediately after
169+
// finalization. Consumers that do not opt in avoid its type_count-sized allocation
170+
// and scan. Allocation failure preserves the iterator's linear fallback.
171+
void cbm_registry_build_type_short_index(CBMTypeRegistry *reg);
172+
163173
// Register a function/method.
164174
void cbm_registry_add_func(CBMTypeRegistry *reg, CBMRegisteredFunc func);
165175

@@ -219,8 +229,24 @@ const CBMRegisteredFunc *cbm_registry_lookup_symbol_by_types(const CBMTypeRegist
219229
const CBMType **arg_types,
220230
int arg_count);
221231

222-
// --- Auxiliary index iterators (Rust trait / free-function fallback fast paths) ---
232+
// --- Auxiliary index iterators (language fallback fast paths) ---
223233
//
234+
// Iterate registry TYPE indices whose qualified-name final segment may equal
235+
// `short_name`. When the optional index is present this walks its hash chain plus
236+
// any post-finalize tail; otherwise it degrades to the original full scan. Results
237+
// preserve ascending registry order. The index is a hash prefilter; callers must
238+
// re-check their exact predicate, including tail entries.
239+
typedef struct {
240+
const CBMTypeRegistry *reg;
241+
uint64_t hash;
242+
int chain_idx;
243+
int tail_i;
244+
int tail_end;
245+
} CBMTypeNameIter;
246+
void cbm_registry_types_by_short_name(const CBMTypeRegistry *reg, const char *short_name,
247+
CBMTypeNameIter *out);
248+
int cbm_type_name_iter_next(CBMTypeNameIter *it);
249+
224250
// Iterate registry TYPE indices whose embedded_types contain an entry whose BARE
225251
// name (last '.'-segment) equals `bare`. On a finalized registry this walks the
226252
// embedded-type index plus any post-finalize tail; on an unfinalized registry it

‎tests/test_c_lsp.c‎

Lines changed: 157 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15250,12 +15250,9 @@ TEST(clsp_tier2_shared_registry_readonly_c) {
1525015250
PASS();
1525115251
}
1525215252

15253-
/* Direct guard for the finalize-time short-name / embedded-type indexes and their
15254-
* iterators (type_registry.c), which the Rust trait/free-func fast paths rely on.
15255-
* Verifies: embed index yields exactly the types whose embedded_types carry a
15256-
* matching BARE name, in ascending registry order, deduped when a type lists the
15257-
* same bare twice; free-func index yields only free funcs (receiver_type==NULL) with
15258-
* the given short_name, not methods. */
15253+
/* Direct guard for the type-name / embedded-type / free-function registry
15254+
* indexes and their iterators (type_registry.c). Verifies that every iterator
15255+
* preserves ascending registry order, which is part of resolver tie-breaking. */
1525915256
TEST(registry_short_name_indexes) {
1526015257
CBMArena arena;
1526115258
cbm_arena_init(&arena);
@@ -15288,6 +15285,25 @@ TEST(registry_short_name_indexes) {
1528815285
t.short_name = "C";
1528915286
t.embedded_types = c_emb;
1529015287
cbm_registry_add_type(&reg, t);
15288+
memset(&t, 0, sizeof(t));
15289+
t.qualified_name = "other.Trait";
15290+
t.short_name = "Trait";
15291+
cbm_registry_add_type(&reg, t);
15292+
memset(&t, 0, sizeof(t));
15293+
t.qualified_name = "Trait";
15294+
t.short_name = "Trait";
15295+
cbm_registry_add_type(&reg, t);
15296+
15297+
/* Populate enough distinct names to force many hash-bucket collisions, with
15298+
* repeated short names spread across the registry. The differential check
15299+
* below compares the index against the exact former linear-scan order. */
15300+
for (int i = 0; i < 512; i++) {
15301+
memset(&t, 0, sizeof(t));
15302+
t.qualified_name = cbm_arena_sprintf(&arena, "bulk.mod%d.Name%d", i, i % 23);
15303+
ASSERT_NOT_NULL(t.qualified_name);
15304+
t.short_name = strrchr(t.qualified_name, '.') + 1;
15305+
cbm_registry_add_type(&reg, t);
15306+
}
1529115307

1529215308
/* free func "helper" (x2 — different QNs), method "M.helper", free func "other". */
1529315309
CBMRegisteredFunc f;
@@ -15310,6 +15326,77 @@ TEST(registry_short_name_indexes) {
1531015326
cbm_registry_add_func(&reg, f);
1531115327

1531215328
cbm_registry_finalize(&reg);
15329+
cbm_registry_build_type_short_index(&reg);
15330+
15331+
/* Qualified-name bare segment "Trait": types 0, 4, then the bare-QN type
15332+
* 5. The iterator is a hash prefilter, so apply the exact predicate before
15333+
* asserting its order just like production consumers do. */
15334+
CBMTypeNameIter nit;
15335+
cbm_registry_types_by_short_name(&reg, "Trait", &nit);
15336+
{
15337+
int expected[] = {0, 4, 5};
15338+
int exact_count = 0;
15339+
int candidate;
15340+
while ((candidate = cbm_type_name_iter_next(&nit)) >= 0) {
15341+
const char *qn = reg.types[candidate].qualified_name;
15342+
const char *last_dot = qn ? strrchr(qn, '.') : NULL;
15343+
const char *candidate_short = last_dot ? last_dot + 1 : qn;
15344+
if (!candidate_short || strcmp(candidate_short, "Trait") != 0)
15345+
continue;
15346+
ASSERT_TRUE(exact_count < 3);
15347+
ASSERT_EQ(candidate, expected[exact_count++]);
15348+
}
15349+
ASSERT_EQ(exact_count, 3);
15350+
}
15351+
cbm_registry_types_by_short_name(&reg, "Missing", &nit);
15352+
{
15353+
int exact_count = 0;
15354+
int candidate;
15355+
while ((candidate = cbm_type_name_iter_next(&nit)) >= 0) {
15356+
const char *qn = reg.types[candidate].qualified_name;
15357+
const char *last_dot = qn ? strrchr(qn, '.') : NULL;
15358+
const char *candidate_short = last_dot ? last_dot + 1 : qn;
15359+
if (candidate_short && strcmp(candidate_short, "Missing") == 0)
15360+
exact_count++;
15361+
}
15362+
ASSERT_EQ(exact_count, 0);
15363+
}
15364+
15365+
for (int name_i = 0; name_i < 23; name_i++) {
15366+
char short_name[32];
15367+
snprintf(short_name, sizeof(short_name), "Name%d", name_i);
15368+
cbm_registry_types_by_short_name(&reg, short_name, &nit);
15369+
int linear_i = 0;
15370+
for (;;) {
15371+
int expected = -1;
15372+
while (linear_i < reg.type_count) {
15373+
int candidate = linear_i++;
15374+
const char *qn = reg.types[candidate].qualified_name;
15375+
const char *last_dot = qn ? strrchr(qn, '.') : NULL;
15376+
const char *candidate_short = last_dot ? last_dot + 1 : qn;
15377+
if (candidate_short && strcmp(candidate_short, short_name) == 0) {
15378+
expected = candidate;
15379+
break;
15380+
}
15381+
}
15382+
15383+
int actual;
15384+
for (;;) {
15385+
actual = cbm_type_name_iter_next(&nit);
15386+
if (actual < 0)
15387+
break;
15388+
const char *qn = reg.types[actual].qualified_name;
15389+
const char *last_dot = qn ? strrchr(qn, '.') : NULL;
15390+
const char *candidate_short = last_dot ? last_dot + 1 : qn;
15391+
if (candidate_short && strcmp(candidate_short, short_name) == 0)
15392+
break;
15393+
}
15394+
15395+
ASSERT_EQ(actual, expected);
15396+
if (expected < 0)
15397+
break;
15398+
}
15399+
}
1531315400

1531415401
/* Embed index for bare "Trait": types 1 (A) then 2 (B), ascending, B once. */
1531515402
CBMTypeEmbedIter it;
@@ -15339,6 +15426,70 @@ TEST(registry_short_name_indexes) {
1533915426
ASSERT_EQ(cbm_free_func_iter_next(&fit), 3);
1534015427
ASSERT_EQ(cbm_free_func_iter_next(&fit), -1);
1534115428

15429+
/* A type appended after finalize is covered by the iterator tail. If the
15430+
* auxiliary allocation is unavailable, the iterator falls back to the same
15431+
* complete linear scan rather than silently dropping candidates. Keep this
15432+
* after the other iterator assertions because their tails intentionally
15433+
* expose post-finalize candidates for caller-side exact filtering too. */
15434+
int tail_type_i = reg.type_count;
15435+
memset(&t, 0, sizeof(t));
15436+
t.qualified_name = "tail.Trait";
15437+
t.short_name = "Trait";
15438+
cbm_registry_add_type(&reg, t);
15439+
cbm_registry_types_by_short_name(&reg, "Trait", &nit);
15440+
ASSERT_EQ(cbm_type_name_iter_next(&nit), 0);
15441+
ASSERT_EQ(cbm_type_name_iter_next(&nit), 4);
15442+
ASSERT_EQ(cbm_type_name_iter_next(&nit), 5);
15443+
ASSERT_EQ(cbm_type_name_iter_next(&nit), tail_type_i);
15444+
ASSERT_EQ(cbm_type_name_iter_next(&nit), -1);
15445+
15446+
int *saved_type_short_buckets = reg.type_short_buckets;
15447+
reg.type_short_buckets = NULL;
15448+
cbm_registry_types_by_short_name(&reg, "Trait", &nit);
15449+
int fallback_expected[] = {0, 4, 5, tail_type_i};
15450+
int fallback_count = 0;
15451+
int candidate;
15452+
while ((candidate = cbm_type_name_iter_next(&nit)) >= 0) {
15453+
const char *qn = reg.types[candidate].qualified_name;
15454+
const char *last_dot = qn ? strrchr(qn, '.') : NULL;
15455+
const char *candidate_short = last_dot ? last_dot + 1 : qn;
15456+
if (!candidate_short || strcmp(candidate_short, "Trait") != 0)
15457+
continue;
15458+
ASSERT_TRUE(fallback_count < 4);
15459+
ASSERT_EQ(candidate, fallback_expected[fallback_count++]);
15460+
}
15461+
ASSERT_EQ(fallback_count, 4);
15462+
reg.type_short_buckets = saved_type_short_buckets;
15463+
15464+
/* A failed rebuild must discard the previous auxiliary index. Re-finalize
15465+
* after adding the tail type so the QN boundary advances, then force arena
15466+
* exhaustion for the optional rebuild. The iterator must fall back to the
15467+
* complete linear scan instead of retaining the stale pre-tail index. */
15468+
cbm_registry_finalize(&reg);
15469+
int saved_nblocks = arena.nblocks;
15470+
size_t saved_used = arena.used;
15471+
arena.nblocks = CBM_ARENA_MAX_BLOCKS;
15472+
arena.used = arena.block_size;
15473+
cbm_registry_build_type_short_index(&reg);
15474+
arena.nblocks = saved_nblocks;
15475+
arena.used = saved_used;
15476+
ASSERT_NULL(reg.type_short_buckets);
15477+
ASSERT_NULL(reg.type_short_entries);
15478+
ASSERT_EQ(reg.type_short_bucket_count, 0);
15479+
15480+
cbm_registry_types_by_short_name(&reg, "Trait", &nit);
15481+
fallback_count = 0;
15482+
while ((candidate = cbm_type_name_iter_next(&nit)) >= 0) {
15483+
const char *qn = reg.types[candidate].qualified_name;
15484+
const char *last_dot = qn ? strrchr(qn, '.') : NULL;
15485+
const char *candidate_short = last_dot ? last_dot + 1 : qn;
15486+
if (!candidate_short || strcmp(candidate_short, "Trait") != 0)
15487+
continue;
15488+
ASSERT_TRUE(fallback_count < 4);
15489+
ASSERT_EQ(candidate, fallback_expected[fallback_count++]);
15490+
}
15491+
ASSERT_EQ(fallback_count, 4);
15492+
1534215493
cbm_arena_destroy(&arena);
1534315494
PASS();
1534415495
}

0 commit comments

Comments
 (0)