perf(lsp): index C++ short-name type lookups

Avoid repeated full type-registry scans in the namespaced-type fallback. Build the optional index only for C/C++ cross registries while preserving candidate order, post-finalize tails, and allocation fallback.

Refs #1677

Signed-off-by: astandrik <astandrik@yandex-team.ru>
This commit is contained in:
astandrik
2026-08-16 17:14:34 +03:00
parent 49d928be67
commit 793716dcb4
4 changed files with 223 additions and 11 deletions
+6 -1
View File
@@ -2818,7 +2818,10 @@ static const CBMRegisteredFunc *c_lookup_member_depth(CLSPContext *ctx, const ch
const char *shortn = dot ? dot + 1 : type_qn;
size_t slen = strlen(shortn);
const char *best_qn = NULL;
for (int i = 0; i < ctx->registry->type_count; i++) {
CBMTypeNameIter it;
cbm_registry_types_by_short_name(ctx->registry, shortn, &it);
int i;
while ((i = cbm_type_name_iter_next(&it)) >= 0) {
const char *q = ctx->registry->types[i].qualified_name;
if (!q) {
continue;
@@ -5972,6 +5975,7 @@ CBMTypeRegistry *cbm_c_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, in
c_register_lsp_defs(arena, reg, "", d, 1);
}
cbm_registry_finalize(reg);
cbm_registry_build_type_short_index(reg);
reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */
return reg;
}
@@ -6055,6 +6059,7 @@ void cbm_run_c_lsp_cross(CBMArena *arena, const char *source, int source_len, co
// Finalize registry — O(1) lookups. See go_lsp.c "3c. Finalize"
// comment for the rationale (linear-scan fallback otherwise).
cbm_registry_finalize(&reg);
cbm_registry_build_type_short_index(&reg);
// Initialize context and run
CLSPContext ctx;
+82
View File
@@ -167,6 +167,51 @@ static void build_type_embed_index(CBMTypeRegistry *reg, CBMArena *idx_arena) {
reg->type_embed_entry_count = idx;
}
/* Optional index: final qualified-name segment -> chain of TYPE indices.
* Descending iteration plus prepend preserves ascending registry scan order.
* Consumers opt in after the QN index captures the finalized tail boundary. */
void cbm_registry_build_type_short_index(CBMTypeRegistry *reg) {
if (!reg || !reg->arena || !reg->type_qn_buckets || reg->type_qn_bucket_count <= 0)
return;
CBMArena *idx_arena = reg->arena;
int count = 0;
for (int i = 0; i < reg->type_count; i++) {
if (reg->types[i].qualified_name)
count++;
}
if (count == 0)
return;
int bucket_count = next_pow2(count * 2);
if (bucket_count < 16)
bucket_count = 16;
int *buckets = (int *)cbm_arena_alloc(idx_arena, (size_t)bucket_count * sizeof(int));
CBMRegistryHashEntry *entries = (CBMRegistryHashEntry *)cbm_arena_alloc(
idx_arena, (size_t)count * sizeof(CBMRegistryHashEntry));
if (!buckets || !entries)
return;
for (int i = 0; i < bucket_count; i++)
buckets[i] = -1;
int idx = 0;
for (int i = reg->type_count - 1; i >= 0; i--) {
const char *qn = reg->types[i].qualified_name;
if (!qn)
continue;
const char *dot = strrchr(qn, '.');
const char *short_name = dot ? dot + 1 : qn;
uint64_t h = fnv1a(short_name);
int slot = (int)(h & (uint64_t)(bucket_count - 1));
entries[idx].hash = h;
entries[idx].payload_index = i;
entries[idx].next_index = buckets[slot];
entries[idx].slot = slot;
buckets[slot] = idx;
idx++;
}
reg->type_short_buckets = buckets;
reg->type_short_entries = entries;
reg->type_short_bucket_count = bucket_count;
}
/* Index: short_name -> chain of FREE-function (receiver_type==NULL) indices.
* Descending-iterate + prepend for ascending chain order (as above). */
static void build_ffunc_short_index(CBMTypeRegistry *reg, CBMArena *idx_arena) {
@@ -208,6 +253,43 @@ static void build_ffunc_short_index(CBMTypeRegistry *reg, CBMArena *idx_arena) {
reg->ffunc_short_entry_count = idx;
}
void cbm_registry_types_by_short_name(const CBMTypeRegistry *reg, const char *short_name,
CBMTypeNameIter *out) {
out->reg = reg;
out->hash = fnv1a(short_name);
if (reg->type_qn_buckets && reg->type_qn_bucket_count > 0) {
if (reg->type_short_buckets && reg->type_short_bucket_count > 0) {
int slot = (int)(out->hash & (uint64_t)(reg->type_short_bucket_count - 1));
out->chain_idx = reg->type_short_buckets[slot];
out->tail_i = reg->type_qn_entry_count;
} else {
out->chain_idx = -1;
out->tail_i = 0; /* auxiliary-index allocation failed: preserve full-scan semantics */
}
out->tail_end = reg->type_count;
} else {
out->chain_idx = -1;
out->tail_i = 0;
out->tail_end = reg->type_count;
}
}
int cbm_type_name_iter_next(CBMTypeNameIter *it) {
const CBMTypeRegistry *reg = it->reg;
while (it->chain_idx >= 0) {
const CBMRegistryHashEntry *e = &reg->type_short_entries[it->chain_idx];
int p = e->payload_index;
uint64_t h = e->hash;
it->chain_idx = e->next_index;
if (h != it->hash)
continue;
return p;
}
if (it->tail_i < it->tail_end)
return it->tail_i++;
return -1;
}
void cbm_registry_types_by_embedded_bare(const CBMTypeRegistry *reg, const char *bare,
CBMTypeEmbedIter *out) {
out->reg = reg;
+30 -4
View File
@@ -111,9 +111,14 @@ typedef struct CBMTypeRegistry {
int method_bucket_count;
int method_entry_count;
// Auxiliary short-name / embedded-type indexes (built by finalize alongside the
// QN buckets). Turn the Rust trait- and free-function fallback scans from
// O(type_count)/O(func_count) into O(chain). Read-only after finalize.
// Auxiliary short-name / embedded-type indexes. The type short-name index is
// opt-in after finalize; the others are built by finalize. Current C/C++ cross
// registries opt in, while other languages incur no construction cost.
// Type short-name index: fnv1a(last-'.'-segment of qualified_name) -> chain
// of TYPE indices. payload_index = type index.
int *type_short_buckets;
CBMRegistryHashEntry *type_short_entries;
int type_short_bucket_count;
// Embedded-type index: fnv1a(bare last-'.'-segment of each embedded_type) -> chain
// of TYPE indices declaring it. payload_index = type index (a type may appear once
// per embedded entry; consumers dedup adjacent same-type via the iterator).
@@ -156,6 +161,11 @@ void cbm_registry_finalize(CBMTypeRegistry *reg);
// there add GBs across a large repo (FastAPI incremental test: +1.1 GB RSS).
void cbm_registry_finalize_into(CBMTypeRegistry *reg, CBMArena *idx_arena);
// Build the optional final-QN-segment type index. Call immediately after
// finalization. Consumers that do not opt in avoid its type_count-sized allocation
// and scan. Allocation failure preserves the iterator's linear fallback.
void cbm_registry_build_type_short_index(CBMTypeRegistry *reg);
// Register a function/method.
void cbm_registry_add_func(CBMTypeRegistry *reg, CBMRegisteredFunc func);
@@ -215,8 +225,24 @@ const CBMRegisteredFunc *cbm_registry_lookup_symbol_by_types(const CBMTypeRegist
const CBMType **arg_types,
int arg_count);
// --- Auxiliary index iterators (Rust trait / free-function fallback fast paths) ---
// --- Auxiliary index iterators (language fallback fast paths) ---
//
// Iterate registry TYPE indices whose qualified-name final segment may equal
// `short_name`. When the optional index is present this walks its hash chain plus
// any post-finalize tail; otherwise it degrades to the original full scan. Results
// preserve ascending registry order. The index is a hash prefilter; callers must
// re-check their exact predicate, including tail entries.
typedef struct {
const CBMTypeRegistry *reg;
uint64_t hash;
int chain_idx;
int tail_i;
int tail_end;
} CBMTypeNameIter;
void cbm_registry_types_by_short_name(const CBMTypeRegistry *reg, const char *short_name,
CBMTypeNameIter *out);
int cbm_type_name_iter_next(CBMTypeNameIter *it);
// Iterate registry TYPE indices whose embedded_types contain an entry whose BARE
// name (last '.'-segment) equals `bare`. On a finalized registry this walks the
// embedded-type index plus any post-finalize tail; on an unfinalized registry it
+105 -6
View File
@@ -15250,12 +15250,9 @@ TEST(clsp_tier2_shared_registry_readonly_c) {
PASS();
}
/* Direct guard for the finalize-time short-name / embedded-type indexes and their
* iterators (type_registry.c), which the Rust trait/free-func fast paths rely on.
* Verifies: embed index yields exactly the types whose embedded_types carry a
* matching BARE name, in ascending registry order, deduped when a type lists the
* same bare twice; free-func index yields only free funcs (receiver_type==NULL) with
* the given short_name, not methods. */
/* Direct guard for the type-name / embedded-type / free-function registry
* indexes and their iterators (type_registry.c). Verifies that every iterator
* preserves ascending registry order, which is part of resolver tie-breaking. */
TEST(registry_short_name_indexes) {
CBMArena arena;
cbm_arena_init(&arena);
@@ -15288,6 +15285,25 @@ TEST(registry_short_name_indexes) {
t.short_name = "C";
t.embedded_types = c_emb;
cbm_registry_add_type(&reg, t);
memset(&t, 0, sizeof(t));
t.qualified_name = "other.Trait";
t.short_name = "Trait";
cbm_registry_add_type(&reg, t);
memset(&t, 0, sizeof(t));
t.qualified_name = "Trait";
t.short_name = "Trait";
cbm_registry_add_type(&reg, t);
/* Populate enough distinct names to force many hash-bucket collisions, with
* repeated short names spread across the registry. The differential check
* below compares the index against the exact former linear-scan order. */
for (int i = 0; i < 512; i++) {
memset(&t, 0, sizeof(t));
t.qualified_name = cbm_arena_sprintf(&arena, "bulk.mod%d.Name%d", i, i % 23);
ASSERT_NOT_NULL(t.qualified_name);
t.short_name = strrchr(t.qualified_name, '.') + 1;
cbm_registry_add_type(&reg, t);
}
/* free func "helper" (x2 — different QNs), method "M.helper", free func "other". */
CBMRegisteredFunc f;
@@ -15310,6 +15326,54 @@ TEST(registry_short_name_indexes) {
cbm_registry_add_func(&reg, f);
cbm_registry_finalize(&reg);
cbm_registry_build_type_short_index(&reg);
/* Qualified-name bare segment "Trait": types 0, 4, then the bare-QN type
* 5. C-LSP applies its stricter dotted-QN predicate on top of this index. */
CBMTypeNameIter nit;
cbm_registry_types_by_short_name(&reg, "Trait", &nit);
ASSERT_EQ(cbm_type_name_iter_next(&nit), 0);
ASSERT_EQ(cbm_type_name_iter_next(&nit), 4);
ASSERT_EQ(cbm_type_name_iter_next(&nit), 5);
ASSERT_EQ(cbm_type_name_iter_next(&nit), -1);
cbm_registry_types_by_short_name(&reg, "Missing", &nit);
ASSERT_EQ(cbm_type_name_iter_next(&nit), -1);
for (int name_i = 0; name_i < 23; name_i++) {
char short_name[32];
snprintf(short_name, sizeof(short_name), "Name%d", name_i);
cbm_registry_types_by_short_name(&reg, short_name, &nit);
int linear_i = 0;
for (;;) {
int expected = -1;
while (linear_i < reg.type_count) {
int candidate = linear_i++;
const char *qn = reg.types[candidate].qualified_name;
const char *last_dot = qn ? strrchr(qn, '.') : NULL;
const char *candidate_short = last_dot ? last_dot + 1 : qn;
if (candidate_short && strcmp(candidate_short, short_name) == 0) {
expected = candidate;
break;
}
}
int actual;
for (;;) {
actual = cbm_type_name_iter_next(&nit);
if (actual < 0)
break;
const char *qn = reg.types[actual].qualified_name;
const char *last_dot = qn ? strrchr(qn, '.') : NULL;
const char *candidate_short = last_dot ? last_dot + 1 : qn;
if (candidate_short && strcmp(candidate_short, short_name) == 0)
break;
}
ASSERT_EQ(actual, expected);
if (expected < 0)
break;
}
}
/* Embed index for bare "Trait": types 1 (A) then 2 (B), ascending, B once. */
CBMTypeEmbedIter it;
@@ -15339,6 +15403,41 @@ TEST(registry_short_name_indexes) {
ASSERT_EQ(cbm_free_func_iter_next(&fit), 3);
ASSERT_EQ(cbm_free_func_iter_next(&fit), -1);
/* A type appended after finalize is covered by the iterator tail. If the
* auxiliary allocation is unavailable, the iterator falls back to the same
* complete linear scan rather than silently dropping candidates. Keep this
* after the other iterator assertions because their tails intentionally
* expose post-finalize candidates for caller-side exact filtering too. */
int tail_type_i = reg.type_count;
memset(&t, 0, sizeof(t));
t.qualified_name = "tail.Trait";
t.short_name = "Trait";
cbm_registry_add_type(&reg, t);
cbm_registry_types_by_short_name(&reg, "Trait", &nit);
ASSERT_EQ(cbm_type_name_iter_next(&nit), 0);
ASSERT_EQ(cbm_type_name_iter_next(&nit), 4);
ASSERT_EQ(cbm_type_name_iter_next(&nit), 5);
ASSERT_EQ(cbm_type_name_iter_next(&nit), tail_type_i);
ASSERT_EQ(cbm_type_name_iter_next(&nit), -1);
int *saved_type_short_buckets = reg.type_short_buckets;
reg.type_short_buckets = NULL;
cbm_registry_types_by_short_name(&reg, "Trait", &nit);
int fallback_expected[] = {0, 4, 5, tail_type_i};
int fallback_count = 0;
int candidate;
while ((candidate = cbm_type_name_iter_next(&nit)) >= 0) {
const char *qn = reg.types[candidate].qualified_name;
const char *last_dot = qn ? strrchr(qn, '.') : NULL;
const char *candidate_short = last_dot ? last_dot + 1 : qn;
if (!candidate_short || strcmp(candidate_short, "Trait") != 0)
continue;
ASSERT_TRUE(fallback_count < 4);
ASSERT_EQ(candidate, fallback_expected[fallback_count++]);
}
ASSERT_EQ(fallback_count, 4);
reg.type_short_buckets = saved_type_short_buckets;
cbm_arena_destroy(&arena);
PASS();
}