Skip to content

perf(vector/search): Candidate / ResultCandidate are 16 bytes; can be packed to 8 #666

Description

@mosuka

Round-3 perf push sub-issue (tracked under umbrella #536).

[S] Candidate / ResultCandidate are 16 bytes; can be packed to 8

  • Where: hnsw/searcher.rs:262-311struct Candidate { id: u64, distance: f32 }
    / ResultCandidate { id: u64, distance: f32 }. Default repr(Rust) pads to 16 bytes.

  • Current behavior: 16 bytes per heap slot. ef_search=64 × ~5 neighbours ≈ 5 KB heap.

  • Why it might be a bottleneck / risk: Heap push/pop touches 5 cache lines instead of
    2-3 if packed to 8.

  • Reference precedent: hnswlib (unsigned int, float) = 8 bytes; Lucene
    NeighborQueue.encode packs (score, docId) into one long.

  • Data structure (proposed):

    // Doc ids ≤ u32::MAX (laurus already implicitly assumes this elsewhere).
    // f32 has order-preserving bits for positive values; sign-flip negative for total order.
    #[repr(transparent)]
    struct PackedCandidate(u64);
    // bits 63..32 = encoded f32 distance (order-preserving)
    // bits 31..0  = u32 doc_id
    // Native `Ord` on u64 → correct heap order without partial_cmp / NaN guards.
    
  • Suggested direction: Bound doc_id to u32 (current vector_count <= u32::MAX). Use
    packed candidate. Heap operations ~1.5× faster in microbench.

  • Risk / scope: Small. Doc id widening to u32 is the only API touch.


ID: VS-23 — see ~/.claude/tasks/laurus/20260523_perf_round3_audit/task_list.md for the full Round-3 issue list.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions