KNN Implementation Details Can Dramatically Change Performance: An Example from Cover Trees
Organizations: CrowdStrike · University of Maryland, Baltimore County · Syracuse University
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 and on the dataset (single-tree search is fastest at on three of our four largest datasets, while dual-tree search is fastest at on the three largest), and choosing the wrong query mode can cost up to a 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 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 , at , and at ); 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.
Figures & tables
| Disabled optimization | pima | pendigits | corel | covtype | mnist |
|---|---|---|---|---|---|
| ( ) | ( ) | ( ) | ( ) | ( ) | |
| Early-exit distance | 0.89 [0.73, 1.24] | 0.82 [0.63, 0.99] | 0.98 [0.97, 1.00] | 1.31 [1.27, 1.36] | 1.49 [1.39, 1.73] |
| Halfsort | 1.28 [1.02, 1.42] | 0.96 [0.75, 1.10] | 1.10 [1.09, 1.11] | 1.03 [1.03, 1.04] | 1.02 [1.02, 1.03] |
| SmallVec | 0.97 [0.89, 1.09] | 0.65 [0.61, 0.95] | 0.99 [0.98, 1.04] | 1.01 [1.00, 1.02] | 1.02 [1.01, 1.03] |
| fast path | 1.06 [0.95, 1.28] | 0.78 [0.69, 0.99] | 1.04 [0.97, 1.06] | 1.03 [1.03, 1.04] | 1.01 [1.01, 1.02] |
| Triangle pre-filter | 1.40 [1.28, 1.52] | 0.75 [0.71, 0.79] | 1.01 [0.98, 1.03] | 0.98 [0.97, 0.99] | 1.03 [0.96, 1.04] |
| Dataset | Source | Dataset | Source | ||||
|---|---|---|---|---|---|---|---|
| iris | 150 | 4 | UCI | optdigits | 5,620 | 64 | UCI |
| wine | 178 | 13 | UCI | pendigits | 10,992 | 16 | UCI |
| glass | 214 | 9 | UCI | letter | 20,000 | 16 | UCI |
| bupa | 345 | 6 | UCI | corel | 37,749 | 32 | UCI |
| ionosphere | 351 | 34 | UCI | mnist | 60,000 | 784 | LeCun |
| pima | 768 | 8 | UCI | covtype | 581,012 | 54 | UCI |
| RustKNN | C++ | mlpack | mlpack | ||||
|---|---|---|---|---|---|---|---|
| Dataset | Dual | Single | Batch | Ref | Single | Dual | |
| iris | 4 | 0.82 [0.80, 0.84] | 0.37 [0.36, 0.38] | 0.71 [0.68, 0.72] | 0.33 [0.28, 0.35] | 1.24 [1.03, 1.30] | 1.32 [1.16, 1.35] |
| wine | 13 | 0.82 [0.74, 0.85] | 0.66 [0.57, 0.68] | 0.74 [0.66, 0.76] | 0.35 [0.27, 0.35] | 1.18 [0.94, 1.20] | 1.01 [0.81, 1.02] |
| glass | 9 | 1.77 [1.41, 1.86] | 0.96 [0.83, 1.05] | 1.36 [1.15, 1.50] | 0.54 [0.48, 0.60] | 2.51 [2.23, 2.75] | 2.22 [2.02, 2.46] |
| bupa | 6 | 4.02 [3.48, 4.21] | 2.07 [1.77, 2.21] | 3.13 [2.58, 3.23] | 1.27 [1.18, 1.41] | 6.02 [5.64, 6.47] | 6.17 [5.60, 6.77] |
| pima | 8 | 7.29 [6.62, 8.66] | 9.05 [8.09, 10.8] | 6.68 [5.67, 7.38] | 2.43 [2.26, 2.50] | 13.5 [12.5, 13.9] | 13.2 [11.9, 13.3] |
| Dataset | Mode | |||||
|---|---|---|---|---|---|---|
| optdigits | Dual | 887 [886, 888] | 1.09K [1.08, 1.1] | 935 [837, 1,184] | 979 [963, 1,106] | 1.13K [1.12, 1.2] |
| Single | 160 [159, 165] | 933 [885, 1,156] | 772 [736, 902] | 976 [962, 1,077] | 1.21K [1.2, 1.26] | |
| Batch | 979 [973, 1,129] | 1.16K [1.06, 1.19] | 987 [969, 1,143] | 1.14K [1.13, 1.21] | 1.34K [1.32, 1.42] | |
| corel | Dual | 4.96K [4.88, 5.17] | 6.55K [6.3, 6.84] | 7.58K [7.27, 7.86] | 9.42K [8.97, 9.75] | 11.5K [10.8, 11.9] |
| Single | 2.93K [2.69, 2.96] | 8.12K [6.78, 8.32] | 9.88K [8.22, 10.2] | 13.1K [10.8, 13.5] | 18.1K [13.5, 18.4] | |
| Batch | 17.2K [17.1, 17.4] | 21.4K [16.9, 21.5] | 23.6K [19, 24] | 28.1K [22, 28.9] | 32.8K [25.5, 34.1] |
Appendix figures & tables33 assets
Supplementary material from the paper’s appendix.
Appendix
| RustKNN Packed | C++ | mlpack | mlpack | ||||
|---|---|---|---|---|---|---|---|
| Dataset | Dual | Single | Batch | Ref | Single | Dual | |
| pima | 32 | 0.13 [0.11, 0.13] | 0.10 [0.09, 0.10] | 0.17 [0.16, 0.18] | 0.09 [0.08, 0.09] | 0.23 [0.22, 0.24] | 0.69 [0.63, 0.70] |
| 128 | 0.57 [0.53, 0.57] | 0.39 [0.36, 0.39] | 0.52 [0.49, 0.53] | 0.31 [0.28, 0.31] | 0.93 [0.86, 0.94] | 1.64 [1.51, 1.66] | |
| 384 | 2.17 [2.00, 2.18] | 1.15 [1.05, 1.16] | 1.43 [1.30, 1.43] | 0.79 [0.73, 0.80] | 2.72 [2.51, 2.73] | 3.59 [3.31, 3.64] | |
| pendigits | 32 | 0.93 [0.79, 1.05] | 0.59 [0.52, 0.67] | 1.05 [0.93, 1.16] | 0.63 [0.52, 0.67] | 1.66 [1.41, 1.78] | 31.0 [27.7, 34.1] |
| 128 | 3.35 [2.97, 3.55] | 2.32 [2.01, 2.49] | 3.92 [3.45, 4.19] | 2.22 [1.97, 2.34] | 6.27 [5.51, 6.79] | 76.1 [69.3, 81.7] | |
| Dataset | Optimal | Time (ms) | cost | Optimal | Time (ms) | cost |
|---|---|---|---|---|---|---|
| glass | 3.0 | 1.04 | 1.67 | 3.0 | 2.08 | 1.55 |
| ionosphere | 3.0 | 2.79 | 1.57 | 3.0 | 5.00 | 1.39 |
| optdigits | 2.0 | 653 | 1.02 | 2.0 | 804 | 1.11 |
| pendigits | 2.0 | 165 | 1.14 | 2.0 | 273 | 1.17 |
| corel | 2.0 | 4,773 | 1.11 | 2.0 | 6,942 | 1.17 |
| Configuration | pima | pendigits | corel | covtype | mnist |
|---|---|---|---|---|---|
| All off | 1.00 [0.83, 1.21] | 1.00 [0.93, 1.08] | 1.00 [0.97, 1.03] | 1.00 [0.98, 1.02] | 1.00 [0.88, 1.13] |
| +Triangle | 0.88 [0.77, 1.14] | 1.00 [0.92, 1.03] | 1.02 [0.99, 1.03] | 1.03 [1.01, 1.04] | 1.12 [0.98, 1.13] |
| + fast | 1.14 [0.97, 1.52] | 0.99 [0.92, 1.03] | 1.01 [0.96, 1.02] | 1.02 [1.01, 1.03] | 1.12 [0.98, 1.16] |
| +SmallVec | 0.91 [0.79, 1.26] | 1.00 [0.93, 1.04] | 1.01 [0.99, 1.03] | 1.06 [1.02, 1.09] | 1.12 [0.98, 1.14] |
| +Halfsort | 1.24 [0.87, 1.59] | 0.96 [0.78, 1.23] | 1.08 [1.05, 1.09] | 1.02 [0.97, 1.05] | 1.06 [0.86, 1.14] |
| +Early-exit | 1.11 [0.99, 1.31] | 0.78 [0.72, 0.83] | 1.06 [1.03, 1.07] | 1.33 [1.31, 1.34] | 1.58 [1.39, 1.60] |
| corel ( ) | covtype ( ) | mnist ( ) | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Disabled | ||||||||||||
| Early-exit | 0.98 | 0.99 | 0.98 | 0.98 | 1.31 | 1.24 | 1.24 | 1.18 | 1.49 | 1.28 | 1.26 | 1.18 |
| Halfsort | 1.10 | 0.99 | 1.01 | 1.02 | 1.03 | 0.97 | 0.98 | 0.98 | 1.02 | 1.00 | 0.99 | 0.98 |
| SmallVec | 0.99 | 1.02 | 1.03 | 1.05 | 1.01 | 0.98 | 0.99 | 0.98 | 1.02 | 0.99 | 0.99 | 0.99 |
| fast | 1.04 | 0.91 | 0.91 | 0.94 | 1.03 | 0.96 | 0.98 | 0.98 | 1.01 | 0.99 | 0.99 | 0.99 |
| Triangle | 1.01 | 1.01 | 1.01 | 1.03 | 0.98 | 0.98 | 0.98 | 0.99 | 1.03 | 0.99 | 0.99 | 0.99 |
| Dataset | Packed | NA Pk | NA | Simpl. | Parallel | C++ |
|---|---|---|---|---|---|---|
| iris | 5.27K | 5.22K | 5.22K | 6.62K | 7.63K | 2.63K |
| wine | 3.75K | 4.25K | 4.25K | 9.47K | 11.7K | 2.64K |
| glass | 12.9K | 11.9K | 11.9K | 19.5K | 22.2K | 5.63K |
| bupa | 36.7K | 37.6K | 37.6K | 44.6K | 60.9K | 17.9K |
| pima | 77.1K | 88.8K | 88.8K | 208K | 192K | 41.9K |
| ionosphere | 43.2K | 50K | 50K | 45.8K | 59.7K | 18.1K |
| RustKNN | C++ | mlpack | mlpack | ||||
|---|---|---|---|---|---|---|---|
| Dataset | Dual | Single | Batch | Ref | Single | Dual | |
| iris | 4 | 1.33 [1.15, 1.37] | 0.94 [0.77, 0.97] | 1.46 [1.21, 1.48] | 0.93 [0.80, 0.94] | 2.67 [2.30, 2.68] | 2.69 [2.38, 2.72] |
| wine | 13 | 1.34 [1.11, 1.39] | 1.44 [1.18, 1.51] | 1.69 [1.40, 1.70] | 1.10 [0.91, 1.16] | 3.27 [2.67, 3.50] | 2.33 [2.01, 2.53] |
| glass | 9 | 2.83 [2.50, 3.28] | 2.46 [2.14, 2.79] | 2.86 [2.50, 3.28] | 1.70 [1.51, 1.91] | 5.51 [4.57, 5.94] | 5.05 [4.17, 5.31] |
| bupa | 6 | 6.46 [5.66, 6.88] | 5.64 [4.80, 5.85] | 5.44 [4.80, 5.82] | 3.65 [3.42, 4.09] | 11.4 [10.7, 13.6] | 10.3 [9.51, 12.6] |
| pima | 8 | 12.3 [10.5, 13.7] | 20.0 [17.2, 22.5] | 12.3 [10.9, 13.4] | 7.50 [6.81, 7.55] | 30.4 [28.7, 31.2] | 23.6 [23.4, 25.1] |
| Same codebase | Independent | ||||
|---|---|---|---|---|---|
| Dataset | KD-tree | CT single | CT dual | kiddo | |
| pendigits | 16 | 71.9 | 171 | 199 | 7.77 [6.27, 9.80] |
| corel | 32 | 254 | 3,248 | 5,702 | 59.1 [55.3, 70.8] |
| covtype | 54 | 4,465 | 100,590 | 13,722 | 12,846 [12,777, 12,943] |
| mnist | 784 | 4.09M | 334,487 | 628,683 | 8.31M [8.29M, 8.34M] |
| Disabled optimization | pima | pendigits | corel | covtype | mnist |
|---|---|---|---|---|---|
| ( ) | ( ) | ( ) | ( ) | ( ) | |
| Early-exit distance | 0.90 | 0.98 | 0.99 | 0.97 | 1.11 |
| Halfsort | 0.62 | 1.04 | 1.00 | 1.00 | 1.01 |
| SmallVec | 1.02 | 1.01 | 0.99 | 0.98 | 1.00 |
| fast path | 1.09 | 1.00 | 1.00 | 1.01 | 1.00 |
| Triangle pre-filter | 0.70 | 1.00 | 1.00 | 0.99 | 1.02 |
| Dataset | KD-tree distance evaluations | |
|---|---|---|
| iris | 4 | 1,126 |
| wine | 13 | 1,207 |
| glass | 9 | 1,534 |
| bupa | 6 | 2,914 |
| pima | 8 | 7,605 |
| ionosphere | 34 | 3,773 |
| Dataset | Full (ms) | Partial (ms) | Speedup | distance calls | |
|---|---|---|---|---|---|
| pima | 8 | 5.2 | 4.6 | 1.14 | 589,824 |
| ionosphere | 34 | 4.3 | 3.0 | 1.41 | 123,201 |
| pendigits | 16 | 1,019 | 1,110 | 0.92 | 120,824,064 |
| optdigits | 64 | 1,047 | 1,065 | 0.98 | 31,584,400 |
| letter | 16 | 3,519 | 3,678 | 0.96 | 400,000,000 |
| corel | 32 | 51,901 | 20,603 | 2.52 | 1,424,987,001 |
| Sweep | Configuration | CT time (ms) | CT distance comps. | KD distance comps. |
|---|---|---|---|---|
| A: vary ( ) | 669 | 5,404,434 | 267,253 | |
| 1,203 | 8,782,805 | 267,248 | ||
| 3,182 | 22,633,024 | 267,248 | ||
| B: vary ( ) | 501 | 9,765,110 | 267,248 | |
| 918 | 10,985,580 | 267,248 | ||
| 1,203 | 8,782,805 | 267,248 |
| Paper | Micro-opt. | Runtime & dist. counts | Batch vs. single | Set vs. held-out |
|---|---|---|---|---|
| [ 42 ] | 0 | 0 | 0 | 0 |
| [ 43 ] | 0 | 0 | 0 | 0 |
| [ 44 ] | 0 | 0 | 0 | 0 |
| [ 10 ] | 0 | 0 | 0 | 0 |
| [ 45 ] | 0 | 0 | 0 | 0 |
| [ 1 ] | 0 | 0 | 0 | 1 |