// 4 bytes / node, reset = bump u32 counter (O(1)):
visited: Box<[u32]>, // allocated once at reader load
visited_gen: u32, // bumped per query
// is_visited(id) = visited[id] == gen // 1 load, 1 cmp
// mark(id) = visited[id] = gen // 1 store
// reset() = gen += 1; if wrap // 1 add (real re-zero only on wraparound)
4× memory vs bitmap, but reset is O(1). u16 variant cuts to 2× and re-zeros every 65 K
queries (cheap amortised).
Round-3 perf push sub-issue (tracked under umbrella #536).
[M] Visited bitmap zero-init dominates at high
max_doc_id/ low ef_searchWhere:
laurus/src/vector/index/hnsw/searcher.rs:438—BitVec::from_elem(graph.max_doc_id() as usize + 1, false).Current behavior: 10 M-vector segment → 1.25 MB bitmap zeroed once per query. For
ef_search = 64× ~5 neighbours visited, fewer than 1000 positions are touched but everybyte of the 1.25 MB is written at query start.
Why it might be a bottleneck / risk: Memory write cost O(N), independent of search
depth. At 10 M × 1000 QPS this is ~1.2 GB/s of pure zero writes.
Reference precedent: hnswlib
VisitedListPool; FAISSVisitedTable— both usegenerational counters.
Data structure (current):
BitVec(1 bit / node, N/8 bytes).Data structure (proposed):
4× memory vs bitmap, but reset is O(1).
u16variant cuts to 2× and re-zeros every 65 Kqueries (cheap amortised).
Suggested direction: Switch to generational visited array, allocated once per reader
(sized from
max_doc_id). Live inSearchContextso threads do not contend.Risk / scope: Small. Confined to
HnswSearcher+HnswIndexReader.ID:
VS-04— see~/.claude/tasks/laurus/20260523_perf_round3_audit/task_list.mdfor the full Round-3 issue list.