Skip to content
AI.info

Research

KNN Implementation Details Can Dramatically Change Performance: An Example from Cover Trees

Overview Research area: Nearest neighbor search, specifically exact metric-space indexing with cover trees, and the evaluation methodology used to benchmark such data structures. Technical level: Adva

KNN Implementation Details Can Dramatically Change Performance: An Example from Cover Trees
arXiv
2610.16940
Published
2026-10-12
Authors
Amol Khanna, Edward Raff

AI summary

Overview

  • Research area: Nearest neighbor search, specifically exact metric-space indexing with cover trees, and the evaluation methodology used to benchmark such data structures.
  • Technical level: Advanced. The paper assumes familiarity with metric spaces, the doubling dimension, tree-based search pruning, and benchmarking practice, though its central argument (implementation details change rankings) is accessible.
  • Scope: The authors build a single Rust codebase, RustKNN, implementing three cover tree construction variants and three query modes, and use it to ablate micro-optimizations, the cover tree base parameter, and evaluation protocols across 12 benchmark datasets.

What This Paper Is About

Papers on nearest neighbor search algorithms rarely document low-level implementation details, yet those details can change which algorithm appears fastest. The authors use the cover tree as a case study, re-implementing it in one Rust codebase so that one factor can be varied at a time, and show that query mode, the tree's base parameter, micro-optimizations, and the choice of benchmark protocol all materially alter measured performance and even the ranking of implementations.

Key Contributions

  1. Optimization ablation. Five micro-optimizations were implemented as compile-time feature flags and ablated individually and cumulatively, showing that early-exit distance computation is the only one with a consistent effect, and that its benefit grows with dimension (none on corel at d=32, 1.31 times on covtype at d=54, and 1.49 times on mnist at d=784), while the traversal-level optimizations sit within about 10% of 1.0 times.
  2. Query mode × k interaction. The optimal query strategy depends on both k and the dataset: single-tree search is fastest at k=1 on three of four largest datasets, dual-tree search is fastest at k>=5 on the three largest, and choosing the wrong query mode costs up to 7.5 times in runtime.
  3. Base parameter analysis. The conventional base of 1.3 is not a safe default for dual-tree search: the optimum is 2.0 on four of seven datasets at k=1 (five at k=10) and 3.0 on the two smallest, and the default is reported as 1.6 times slower than the optimum on covtype in the abstract and contributions (Section 5.3 gives 1.57 times at k=1 and 1.51 times at k=10).
  4. All-NN versus held-out divergence. The standard all-nearest-neighbor benchmark produces different implementation rankings than held-out evaluation, with implications for how the community evaluates nearest neighbor methods.

The paper also reports a survey of prior work (Appendix K) finding that few papers in this area distinguish a hold-out set's query performance from the original training data (26%), report both distance-comparison counts and runtime (15%), document micro-optimization details (11%), or report batch versus single-instance queries (10%).

Main Findings

  • No single method dominates. At k=1, the C++ reference has the lowest median on 9 of 12 datasets, while RustKNN single-tree search wins on optdigits, corel, and mnist by 1.7 times, 1.6 times, and 3.7 times over the C++ reference. At k=10 the C++ reference still wins every dataset with n <= 20,000, but RustKNN dual-tree search wins the three largest, by 1.9 times on corel, 2.2 times on covtype, and 3.6 times on mnist.
  • The query mode crossover is sharp with k. On mnist, single-tree search is 1.8 times faster than dual-tree at k=1 but 3.1–3.5 times slower at every k>=5. Corel shows the same pattern at smaller scale (single-tree 1.7 times faster at k=1, dual-tree 1.2–1.6 times faster at k>=5). On optdigits single-tree leads through k=10, the two tie at k=25, and dual-tree leads at k=50.
  • The largest mode-choice penalty appears on covtype. Dual-tree search is best at every k, and single-tree search is 7.0–7.5 times slower at every k.
  • Batch single-tree is never the fastest mode on the four datasets traced across k; at k>=5 it is within 30% of the best only on optdigits.
  • Early exit is the one consistent optimization. Disabling it produces no effect on corel (confidence interval [0.97, 1.00]), 1.31 times on covtype, and 1.49 times on mnist. Disabling it leaves the dual-tree distance count unchanged (159.6M calls on corel either way), so the traversal is identical and only the cost per rejected candidate differs. None of the five flags changes the distance count by more than 0.1%.
  • Other micro-optimizations have no consistent effect. Halfsort, SmallVec, the k=1 fast path, and the triangle pre-filter are within 4% of 1.0 times on covtype and mnist, and only corel's halfsort (1.10 times) exceeds that on corel. The authors treat effects up to about 10% as code-layout noise.
  • Small-dataset results are not supportable. Per-allocation timings on pima and pendigits are bimodal; on pendigits, allocations of the same binary cluster near 170 ms or near 260 ms, so sub-1.0 entries in those columns (for example pendigits SmallVec at 0.65 times) reflect which mode the allocations landed in. The authors draw no conclusions on micro-optimizations from small datasets.
  • Fine-grained bases win. Across all twelve datasets, b=1.3 is optimal in only one of 24 (dataset, k) cases. The optimum rarely shifts with k: in Table 6 only mnist moves, from 1.3 at k=1 to 2.0 at k=10, and there base 2.0 is 10% slower than 1.3 at k=1 and within 1% of it at k=10.
  • All-NN and held-out rankings diverge. In all-NN, mlpack's two query modes are within about 2 times of each other on every dataset and dual-tree is faster on five; held out, mlpack-Dual is 7.8 times slower than mlpack-Single on pendigits and 250 times slower on covtype at n_q = 512. On mnist, single-tree wins all-NN at k=1, but held out at n_q = 512 dual-tree is 1.8 times faster than single-tree (8.4 versus 15.1 s). On covtype the C++ reference ties RustKNN's best mode in all-NN at k=1 (13.0K versus 13.6K ms, intervals overlapping) but is 1.5 times faster held out at n_q = 512 (53.0 versus 81.1 ms, intervals disjoint).
  • Where the authors' implementation loses. On covtype held-out, the C++ reference is fastest at every n_q and RustKNN's best mode (dual-tree) is 1.2–1.5 times slower. RustKNN-Single is slower than mlpack-Single at n_q >= 128 (176 versus 142 ms at n_q = 512), although RustKNN-Dual is 2.2 times faster than RustKNN-Single at n_q = 512 and 1.75 times faster than mlpack-Single.
  • Distance counts differ enormously by mode. On mnist the single-tree all-NN issues 6 times fewer distance computations than the dual-tree traversal (312M versus 1.87B).
  • Measured time is noisy across allocations. Repeated runs within one allocation agree to 1–2%, but two allocations of a byte-identical binary can differ by 16–29% on small, fast datasets; between measurement campaigns, medians can shift by 20–40% due to node placement. On an idle node, an mnist all-NN slows 1.4–1.5 times when 15 copies share the node and 2.0–2.1 times with 60.

Methodology in Plain English

The authors re-implement the cover tree from scratch in Rust as a single codebase, RustKNN (about 13,000 lines), covering three construction variants (simplified, nearest ancestor, and parallel) and three query modes (single-tree, batch single-tree, and dual-tree). Building everything in one codebase lets them turn one factor on or off at a time, which cross-language or cross-library comparisons cannot do because compiler behavior, memory layout, and distance-computation strategy all differ at once.

Five micro-optimizations are implemented as compile-time feature flags so each can be disabled in isolation or all at once. A sixth optimization, sorted-vector batch tracking, is implemented but not evaluated because the ablation infrastructure targets the all-NN benchmark in dual-tree mode. After construction, the pointer-based tree is packed into a contiguous structure in depth-first order for cache locality, with each packed node occupying about 40 bytes so it fits in a single 64-byte cache line.

For timing, every configuration (dataset × algorithm × variant × query mode × k) is run as 20 independent SLURM allocations, meaning 20 separately scheduled jobs rather than 20 repetitions inside one job. Each allocation is reduced to a single number (the median of its internal timed runs), and the paper reports the median over the 20 allocations with a bootstrap 95% confidence interval using 4,000 seeded resamples. Distance-computation counts come from a separately instrumented binary compiled with --features instrument, which increments a thread-local counter on every call to the distance function, including calls that exit early before computing all dimensions.

The evaluation covers 12 benchmark datasets: eleven from the UCI Machine Learning Repository and MNIST from the original LeCun distribution. Sizes span from n = 150 to 581,012 and dimensionalities from 4 to 784, all using Euclidean (L2) distance. Comparisons are against three baselines: the original C++ cover tree reference implementation adapted to produce JSON output, mlpack 4.7.1 single-tree, and mlpack 4.7.1 dual-tree, both at base 1.3. The five smallest UCI sets (n <= 351) are excluded from the ablation because their millisecond-scale runtimes are noise-dominated; the performance conclusions rest on corel, covtype, and mnist. Appendix J adds non-tiny synthetic data (n = 20,000) whose intrinsic dimension is known by construction. The content provided also indicates the authors added a median-split KD-tree inside RustKNN reusing the same harness, but the section is truncated mid-sentence and its results are not reported in the supplied text.

Why This Matters

Impact on research. The paper argues that papers evaluating nearest neighbor systems need to document and evaluate implementation details carefully, or justify why only a subset matters for their use case. Because no single method dominates and rankings flip with k, dataset, base parameter, and evaluation protocol, comparisons that hold these factors fixed can reach conclusions that do not transfer. The authors note the complexity exists within the cover tree literature alone, which suggests the same questions should be asked of the broader literature.

Real-world applications named in the paper:

  • Anomaly detection over raw features, where exact metric-space k-NN remains necessary when the distance itself is the object of interest.
  • Protein-structure comparison under graph kernels.
  • Retrieval under Wasserstein distance.
  • Information retrieval, clustering and visualization, and Large Language Models via retrieval-augmented generation and vector databases.

Industry relevance. RustKNN is the first Rust-native cover tree and integrates directly with the Rust data-processing ecosystem without foreign-function interface overhead, filling a gap in a toolchain that includes Hugging Face tokenizers, candle, burn, Qdrant, LanceDB, kiddo, and hora. Those projects address approximate search or coordinate-specific structures; RustKNN provides exact search over arbitrary metric spaces with the full algorithmic suite from Izbicki and Shelton. The paper also connects to maintainability and replicability concerns, and is released open source at https://github.com/CrowdStrike/RustKNN.

Future Directions

  • Sweep the base rather than defaulting to 1.3. The authors recommend sweeping b in {1.3, 2.0, 3.0}, which costs seconds on most datasets and roughly 10–15 minutes per base on mnist. They explicitly do not generalize their base finding beyond dual-tree search and present per-implementation base sensitivity curves.
  • Report both all-NN and held-out results. The paper recommends the all-NN benchmark for stressing scalability and held-out evaluation to approximate serving latency, since the two produce different rankings.
  • Test whether the methodological finding generalizes beyond cover trees. Section 5.5 begins this by adding a median-split KD-tree inside RustKNN under the same harness; the supplied content is truncated there, so its results are not reported.
  • Resolve the theoretical and empirical variants more broadly. The paper notes a history of nuanced theoretical runtime errors and fixes around cover trees, and that no two existing implementations agree on which variants, query modes, and micro-optimizations they include, leaving cross-codebase attribution of performance differences unresolved.

Target Audience

Researchers and practitioners who benchmark or deploy nearest neighbor search systems, including authors of approximate and exact nearest neighbor papers, maintainers of metric-space indexing libraries, and engineers building vector search or retrieval infrastructure. It is most useful to readers who already understand tree-based pruning and want a concrete demonstration that implementation and evaluation choices, not just algorithmic families, determine measured performance. Readers evaluating other index structures such as KD-trees, ball trees, HNSW, or LSH will find the methodological framing transferable even though the detailed experiments concern cover trees.

Authors’ abstract

Nearest neighbor search algorithms are widely used and have a long history, but we find that key implementation details are seldom reviewed in articles about them. These details can materially impact performance, and thus change the conclusions one makes about a given algorithm's superiority. In this article, we highlight these impacts by reviewing the Cover Tree as an example, producing RustKNN, a unified Rust implementation of three construction variants of the Cover Tree with three query modes. Our analysis reveals: (1) the optimal query strategy depends on $k$ and on the dataset (single-tree search is fastest at $k{=}1$ on three of our four largest datasets, while dual-tree search is fastest at $k{\ge}5$ on the three largest), and choosing the wrong query mode can cost up to a $7.5\times$ factor in runtime; (2) the cover tree base parameter, set to 2 in theory and 1.3 in practice, is not a safe default for dual-tree search: 1.3 is optimal on only one of seven datasets, and on covtype it is $1.6\times$ slower than the optimum; (3) among five micro-optimizations, only early-exit distance computation has a consistent effect, and that effect grows with dimension (none at $d{=}32$, $1.31\times$ at $d{=}54$, and $1.49\times$ at $d{=}784$); and (4) all-nearest-neighbor and held-out benchmarks yield different implementation rankings. All main timing tables report medians over 20 independent allocations with bootstrap 95% confidence intervals. RustKNN is open source at https://github.com/CrowdStrike/RustKNN.

Read the original paper