Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Performance Guide

Where Chassis stands

Measured from 2026-10-08 to 2026-10-10, one thread searching, on GitHub’s server runners (AMD EPYC Zen 3, 4 and 5, Intel Xeon, Arm Neoverse-N2) and an Apple M5. The decision records hold the details and the commands.

Against hnswlib, at the same recall: queries a second, Chassis over hnswlib (ADR-0017, ADR-0018).

Vectorsx86Neoverse-N2
128 dims × 1M1.2–2.6×3.1–4.4×
960 dims × 200k0.86–1.05×; in half precision 1.0–2.1×1.65–1.77×; half 2.3–2.5×
1,536 dims × 99k0.82–1.13×; in half precision 1.0–1.7×1.85–2.33×; half 2.45–2.93×

These came before distances were computed in groups (ADR-0021), which made searches over vectors of a kilobyte or more another 11 to 60% faster out of cache.

Through Python: each engine through its Python package on one thread, every query in one call (Chassis’s search_batch), queries a second for 0.95 recall, Chassis over the other engine (2026-10-10, one run, one machine per CPU). With one call per query instead, Chassis lost a fifth to a third of its speed from Rust, 15 to 38 µs a query; in one call it is within 5% of it.

VectorsOver hnswlibOver FAISS’s HNSW
128 dims × 1MZen 3 1.13×, Xeon 1.33×, N2 3.46×Zen 3 0.98×, Xeon 1.22×, N2 1.69×
128 dims × 1M, halfZen 3 1.61×, Xeon 2.01×, N2 3.63×Zen 3 1.40×, Xeon 1.85×, N2 1.77×
1,536 dims × 99kZen 4 1.26×, Xeon 1.05×, N2 2.92×Zen 4 0.78×, Xeon 0.95×, N2 1.09×
1,536 dims × 99k, halfZen 4 1.91×, Xeon 1.55×, N2 2.92×Zen 4 1.19×, Xeon 1.40×, N2 1.09×

From a cold start (ADR-0019, Apple M5, 99,000 × 1,536 dims, the file not in memory): the first result came 0.11 s after the process started, where hnswlib, which loads its file first, gave its first at 0.5 to 0.8 s. With warm(), the hundredth result came at 0.37 s instead of 1.9 s.

The sections below are earlier measurements, from 2026-10-03 to 2026-10-05 on the M5 and mostly of file format 2: they show how Chassis got here, not where it is now.

Measured on 2026-10-03 to 2026-10-05 on an Apple M5 (10 cores, 24 GiB) running macOS 26.6 on APFS, with rustc 1.99.0 and release builds, on a machine that was also running other applications. Numbers on other machines will differ, so the commands to reproduce them are at the end. Search and build numbers below are for the code after the 33-candidate cap was removed (ADR-0004 amendment).

Against hnswlib and usearch

From bench/ann: the same settings for every engine (M 16, ef_construction 200), one thread for building and searching, 1,000 queries per dataset, recall@10 against the datasets’ ground truth. Chassis runs from Rust; hnswlib 0.8.0 and usearch 2.26.3 run through their Python batch APIs with one thread. usearch built exactly the same graph as hnswlib at these settings (identical results for every query), so it differs only in speed.

DatasetEngineBuildef 64: recall / QPSef 256: recall / QPSef 512: recall / QPSFile
SIFT-1M, 128 dimsChassis567 s0.969 / 3,7410.998 / 1,4330.999 / 9643,379 MB
hnswlib769 s0.960 / 3,9040.997 / 1,0980.999 / 633661 MB
usearch938 s0.959 / 2,1430.997 / 6420.999 / 361661 MB
GloVe, 1.18M × 100Chassis1,430 s0.788 / 3,4190.897 / 1,3280.934 / 6783,289 MB
hnswlib988 s0.763 / 2,7800.887 / 1,0090.930 / 567649 MB
usearch1,536 s0.763 / 2,0270.887 / 5880.930 / 264649 MB
dbpedia, 99k × 1536 (OpenAI)Chassis420 s0.978 / 9180.998 / 2881.000 / 158982 MB
hnswlib673 s0.970 / 4780.996 / 1540.999 / 81623 MB
usearch607 s0.970 / 5180.996 / 1400.999 / 70623 MB

What these numbers do and don’t show:

  • One thread is not how builds usually run. On every core (see Parallel builds), Chassis and hnswlib build 2.4 to 5.2 times faster, and Chassis’s lead on SIFT holds.
  • This is an ARM machine. hnswlib’s SIMD distance code only targets x86, so here it runs compiler-vectorized loops (about 264 ns per 1,536-dim distance, against 149 ns for Chassis’s NEON code). On x86 the gap at 1,536 dims may close. usearch has ARM SIMD and was still slower here.
  • Chassis searches warm memory-mapped pages. The harness runs an untimed pass after each open; a cold first pass after opening was about half as fast on SIFT at ef 16.
  • These Chassis rows are file format 2. Its files and memory were much larger (peak memory about 4.2 GB against 0.8–0.9 GB on SIFT and GloVe), from fixed 2,192-byte node records, and GloVe’s build spent 568 of its 1,430 s in the kernel, moving the graph within the file and remapping it as it grew. Format 3 removes both; see Format 3.
  • Removing the 33-candidate cap made dbpedia’s build 19% slower (420 s against 352 s), in exchange for higher recall at every ef; on SIFT and the 20k example, build time didn’t increase.

End to End

From examples/recall.rs: uniform random vectors in [-1, 1], default IndexOptions (max_connections 16, ef_construction 200), build time including the final flush(), 200 queries, recall@10 against brute force.

VectorsDimsBuildef_searchRecall@10Mean search
20,0001287.5–7.6 s (2 runs)500.58112–132 µs
2000.90325–341 µs
20,000153692.1 s500.371.2 ms
2000.693.6 ms

Uniform random vectors are close to the worst case for HNSW. Real embeddings usually reach higher recall at the same settings, so measure on your own data before choosing ef_search.

Deletes

Same 20,000 × 128 index with part of it deleted, from the last lines of examples/recall.rs (ef_search 50, two runs on 2026-10-05 while the machine was busy, so latency is relative to the same run’s undeleted search, and noisy):

DeletedRecall@10 (live vectors)Search time vs. none deletedFlush with these deletes
0%0.581×3.9 ms
10%0.59–0.600.9–1.2×17 ms (2,000 deletes)
50%0.72–0.741.1–1.8×30–42 ms (8,000 deletes)

Search still walks through deleted vectors to keep the graph connected, so it slows down as deletes accumulate; recall rises because it explores further to collect enough live results. Deleted vectors keep their space until the index is rebuilt.

Storage

From storage_bench, 768-dimensional vectors, in a temp dir on APFS.

OperationTime
Insert, no flush17 µs (12–22 µs)
Insert, then flush3.9 ms
10 inserts, then one flush4.1 ms
100 inserts, then one flush4.3 ms
1,000 inserts, then one flush7.9 ms
Read a cached vector85 ns
Read a vector after reopening79 µs
Read 1,000 vectors in order118 µs
Grow an empty file to 1,000 vectors6.8 ms

flush() costs milliseconds because it waits for the disk (F_FULLFSYNC on macOS, fsync elsewhere). Insert in batches and flush once per batch. A temp dir on tmpfs makes fsync free, so benchmarks run there understate this cost.

Distance Kernels

From distance_bench, NEON on this machine (AVX2 on x86_64).

DimsSIMDScalarSpeedup
648.1 ns21.5 ns2.7x
12815.3 ns59.1 ns3.9x
38435.5 ns239 ns6.7x
76876.4 ns613 ns8.0x
1536149 ns1.39 µs9.4x
3072315 ns2.88 µs9.1x

SIMD throughput is about 10 billion elements per second at 384 dimensions and up.

Other Benchmarks

search_bench, link_bench, graph_io_bench and hnsw_node_bench are microbenchmarks. search_bench searches hand-wired graphs (single-layer, except the greedy-descent bench) over vectors with at most 16 non-zero dimensions, not graphs built by VectorIndex, so its latencies are far below real search. Use examples/recall.rs for end-to-end numbers.

Index Size

Each slot takes a 24-byte slot header, its vector, and a level-0 record of 8 + 4 × M0 bytes (136 with the defaults). The few nodes above layer 0 add 4 × M bytes per extra layer in the upper heap, about 4 bytes per vector on average. For 1M vectors with 768 dimensions that is 3.07 GB of vectors and 0.16 GB of everything else.

Segments double in size up to 256 MiB, so the file can be up to one segment larger than the data written. That unused end is never written; whether it stays sparse depends on the file system (on APFS, ranges above about 16 MiB did and smaller ones sometimes didn’t; ext4 is untested). Nothing is ever copied to grow the file.

Format 3

ADR-0008 “Implementation Status” has the full results. In short, on the same graphs converted from format 2:

  • Search returns identical results, ids and distances, for every query at every ef on SIFT-1M and dbpedia, and is 1.5–12% faster (median paired ratio per ef), from smaller records and a smaller working set. Re-measured after the reader changes to the search path: no slower.
  • Files are 4.7× smaller on SIFT-1M (713 MB against 3,379 MB) and 1.2× on dbpedia (821 MB against 982 MB). Converting SIFT-1M took 9.4 s.
  • Opening a 10M-slot file takes under 1 ms; it only maps the file, so pages load on first touch. An index with its own ids builds its id table on the first add or delete in each process: at 10M ids, a median of 0.47 s with the file in the page cache and 0.91 s right after copying it, on a machine with other load. Filling the hash table is over 90% of that.

Built from scratch on format 3 (one thread, same settings as the table above, with other applications running, so build times are indicative):

DatasetBuildIn the kernelPeak memoryFileRecall at ef 64 / 256 / 512
SIFT-1M527 s10.6 s1.09 GB713 MB0.968 / 0.998 / 0.999
dbpedia431 s6.4 s1.22 GB821 MB0.978 / 0.998 / 1.000

Peak memory includes the training vectors the harness holds (0.51 GB for SIFT, 0.61 GB for dbpedia). Format 2 peaked at about 4.2 GB on SIFT, and GloVe’s format 2 build spent 568 s in the kernel.

Parallel builds

Measured on 2026-10-07 on the same M5, which has 4 performance and 6 efficiency cores and was in Low Power Mode, with other applications running. One build at a time, M 16, ef_construction 200; “every core” is Chassis’s add_batch and hnswlib’s num_threads=-1. Search is one thread.

DatasetEngineOne threadEvery coreSpeedupef 64, every core: recall / QPS
SIFT-1MChassis545 s110 s5.0×0.968 / 4,111
hnswlib682 s131 s5.2×0.959 / 2,878
dbpedia, 99k × 1536Chassis417 s176 s2.4×0.978 / 841
hnswlib596 s163 s3.7×0.970 / 481

Graphs built on every core reach the same recall as one-thread builds of either engine (within 0.002 from ef 32 up, and 0.007 below). Their QPS varied by up to 40% either way between single passes, so these runs don’t compare search speed. An earlier parallel dbpedia run the same day took 159 s for Chassis and 158 s for hnswlib, so builds vary by about 10% between runs. The 1,536-dimension build scales less for both engines; ADR-0010 has the details.

Reproduce

cargo run --release --example recall -- 20000 128
cargo bench --bench storage_bench
cargo bench --bench distance_bench

# Against hnswlib and usearch (downloads about 1.9 GB of datasets)
pip install numpy h5py pyarrow hnswlib usearch
python bench/ann/prepare.py
cargo run --release --example ann -- bench/ann/data sift-128          # add `batch` for every core
python bench/ann/reference_bench.py hnswlib bench/ann/data sift-128   # add `0` for every core
python bench/ann/reference_bench.py usearch bench/ann/data sift-128

Criterion reports go to target/criterion/.