Skip to content

perf: benchmark metadata string interning strategies - #393

Open
jimezesinachi wants to merge 1 commit into
mainfrom
perf/metadata-representation-benchmarks
Open

perf: benchmark metadata string interning strategies#393
jimezesinachi wants to merge 1 commit into
mainfrom
perf/metadata-representation-benchmarks

Conversation

@jimezesinachi

Copy link
Copy Markdown
Collaborator

Summary

  • add a Criterion benchmark comparing owned metadata with integer-ID key interning and full string interning
  • measure representation build cost, equality-filter performance, response materialization, and live jemalloc allocation
  • assert equivalent filter counts and fully materialized metadata across all representations
  • add a self-contained runner for reproducible same-ID Criterion comparisons

This PR contains benchmark code only and does not change Ahnlich production storage or behavior.

Verification

  • cargo fmt --all -- --check
  • cargo clippy -p db --bench metadata_representation -- -D warnings
  • cargo check -p db --benches
  • make test (342 passed, 5 skipped)
  • ./benchmarks/run_metadata_representation_ab.sh

@jimezesinachi

jimezesinachi commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

Metadata representation experiment results

I tested three benchmark-local representations:

  • owned: current shape, HashMap<String, MetadataValue>
  • key-interned: metadata keys become per-store u32 IDs; MetadataValue remains unchanged
  • fully interned: both keys and RawString values become u32 IDs

Method

The deterministic fixture contains 1,000, 10,000, or 100,000 rows, with four metadata keys per row and eight repeated raw-string values per key. It uses Ahnlich's generated MetadataValue type. Before measuring, the benchmark asserts identical equality-filter counts and identical fully materialized metadata for all representations.

Build measures the additional storage-representation work after request decoding. Criterion excludes fixture cloning from the timed section, so the owned control is nearly free because it only moves the decoded collection.

Environment: commit a515297c0bf6bdf998bc9873197d5889510e80fe, Rust 1.97.1, Apple M1 Pro (10 cores), 16 GiB RAM. Criterion uses 10-second measurement windows and 10 samples.

Live metadata allocation

Rows Owned Key-interned Fully interned
1,000 618,744 B 453,416 B (-26.7%) 120,920 B (-80.5%)
10,000 6,400,040 B 4,703,168 B (-26.5%) 1,285,648 B (-79.9%)
100,000 63,951,048 B 47,235,576 B (-26.1%) 12,795,808 B (-80.0%)

Representation build

Rows Owned move Key-interned Difference Fully interned Difference
1,000 80.0 ns 380.4 us +475,605% 628.7 us +786,126%
10,000 437.1 ns 3.8 ms +880,308% 6.3 ms +1,440,815%
100,000 517.6 ns 38.4 ms +7,409,886% 64.0 ms +12,356,168%

Key interning is therefore 475,605-7,409,886% slower than owned representation build in this benchmark, corresponding to approximately 0.38 us of additional conversion work per row.

The percentages are unusually large and grow with the fixture because the owned representation performs an O(1) move of already-decoded metadata, while interning performs O(rows x keys) hashing and reconstruction. They describe the isolated representation-conversion regression, not the percentage impact on Ahnlich's complete Set path.

Equality filtering

Rows Owned Key-interned Fully interned
1,000 15.9 us 11.5 us 9.7 us
10,000 159.7 us 110.3 us 94.2 us
100,000 3.7 ms 1.26 ms 969.3 us

Key interning is approximately 28-66% faster for this equality-filter workload. Full interning is approximately 39-74% faster.

Materialization back to public metadata

Rows Owned Key-interned Fully interned
1,000 690.9 us 751.1 us 741.1 us
10,000 7.1 ms 7.6 ms 7.5 ms
100,000 71.8 ms 76.1 ms 76.5 ms

Key interning is approximately 6-9% slower when reconstructing public metadata.

Interpretation

Full string interning does not look suitable for production: its large memory and filter gains come with broader value conversion, storage-format complexity, and a higher ingestion cost.

Integer-ID key interning is more credible. It keeps every MetadataValue unchanged, saves about 26% of metadata allocation, and improves this equality scan while adding about 0.38 us per ingested row and 6-9% materialization overhead. That is enough to justify further measurement, but not a production change yet.

The main limitation is that this is an isolated, highly repetitive metadata workload. It does not include protobuf decoding, Raft, vector/index updates, snapshots, or the complete Set RPC. If we decide to proceed, the next decision point should be a production-shaped end-to-end Set and query benchmark before changing the stored representation.

Reproduce with:

cargo install critcmp
./benchmarks/run_metadata_representation_ab.sh

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown

Test Results

379 tests   379 ✅  11m 18s ⏱️
 42 suites    0 💤
  4 files      0 ❌

Results for commit a515297.

♻️ This comment has been updated with latest results.

@github-actions

Copy link
Copy Markdown

Benchmark Results

group                                                        main                                   pr
-----                                                        ----                                   --
predicate_query_with_index/size_100                          1.00      3.2±0.00µs        ? ?/sec    1.01      3.2±0.00µs        ? ?/sec
predicate_query_with_index/size_1000                         1.00     31.3±0.04µs        ? ?/sec    1.01     31.8±0.06µs        ? ?/sec
predicate_query_with_index/size_10000                        1.00    385.7±2.34µs        ? ?/sec    1.01    388.4±0.84µs        ? ?/sec
predicate_query_with_index/size_100000                       1.31     14.9±0.41ms        ? ?/sec    1.00     11.4±1.80ms        ? ?/sec
predicate_query_without_index/size_100                       1.00      6.6±0.03µs        ? ?/sec    1.02      6.8±0.02µs        ? ?/sec
predicate_query_without_index/size_1000                      1.00     71.7±0.22µs        ? ?/sec    1.03     74.0±0.16µs        ? ?/sec
predicate_query_without_index/size_10000                     1.00   1131.7±5.54µs        ? ?/sec    1.00   1134.1±3.88µs        ? ?/sec
predicate_query_without_index/size_100000                    1.00     33.2±1.73ms        ? ?/sec    1.29     42.7±3.39ms        ? ?/sec
store_batch_insertion_without_predicates/size_100            1.03    225.2±0.93µs        ? ?/sec    1.00    217.6±2.79µs        ? ?/sec
store_batch_insertion_without_predicates/size_1000           1.00  1446.9±16.69µs        ? ?/sec    1.09  1571.1±253.82µs        ? ?/sec
store_batch_insertion_without_predicates/size_10000          1.00     15.1±0.09ms        ? ?/sec    1.02     15.4±0.88ms        ? ?/sec
store_batch_insertion_without_predicates/size_100000         1.00    153.9±9.77ms        ? ?/sec    1.06   163.9±11.14ms        ? ?/sec
store_retrieval_linear_cosine_similarity/size_100            1.00     22.8±0.16µs        ? ?/sec    1.00     22.8±0.11µs        ? ?/sec
store_retrieval_linear_cosine_similarity/size_1000           1.00    206.0±0.28µs        ? ?/sec    1.01    208.8±0.54µs        ? ?/sec
store_retrieval_linear_cosine_similarity/size_10000          1.00  1661.0±15.29µs        ? ?/sec    1.18  1959.7±25.61µs        ? ?/sec
store_retrieval_linear_cosine_similarity/size_100000         1.00     25.2±0.30ms        ? ?/sec    1.15     29.0±0.32ms        ? ?/sec
store_retrieval_linear_dot_product/size_100                  1.01     20.6±0.02µs        ? ?/sec    1.00     20.4±0.04µs        ? ?/sec
store_retrieval_linear_dot_product/size_1000                 1.00    167.2±2.11µs        ? ?/sec    1.00    167.7±0.59µs        ? ?/sec
store_retrieval_linear_dot_product/size_10000                1.12  1773.3±82.09µs        ? ?/sec    1.00  1578.1±33.16µs        ? ?/sec
store_retrieval_linear_dot_product/size_100000               1.03     25.9±0.95ms        ? ?/sec    1.00     25.1±0.50ms        ? ?/sec
store_retrieval_linear_euclidean_distance/size_100           1.00     21.9±0.02µs        ? ?/sec    1.00     22.0±0.02µs        ? ?/sec
store_retrieval_linear_euclidean_distance/size_1000          1.02    194.6±0.81µs        ? ?/sec    1.00    190.9±0.61µs        ? ?/sec
store_retrieval_linear_euclidean_distance/size_10000         1.00  1754.8±75.43µs        ? ?/sec    1.02  1793.9±35.69µs        ? ?/sec
store_retrieval_linear_euclidean_distance/size_100000        1.00     25.7±0.22ms        ? ?/sec    1.04     26.6±0.50ms        ? ?/sec
store_retrieval_no_condition/size_100                        1.00     22.8±0.03µs        ? ?/sec    1.01     23.0±0.06µs        ? ?/sec
store_retrieval_no_condition/size_1000                       1.01    210.0±0.51µs        ? ?/sec    1.00    207.3±0.91µs        ? ?/sec
store_retrieval_no_condition/size_10000                      1.00      2.0±0.05ms        ? ?/sec    1.16      2.3±0.18ms        ? ?/sec
store_retrieval_no_condition/size_100000                     1.03     28.0±0.64ms        ? ?/sec    1.00     27.3±0.40ms        ? ?/sec
store_retrieval_non_linear_hnsw/size_100                     1.00    176.3±0.19µs        ? ?/sec    1.01    178.5±0.52µs        ? ?/sec
store_retrieval_non_linear_hnsw/size_1000                    1.00    359.4±0.79µs        ? ?/sec    1.04    372.5±4.56µs        ? ?/sec
store_retrieval_non_linear_hnsw/size_10000                   1.00    539.2±1.22µs        ? ?/sec    1.00    541.0±5.75µs        ? ?/sec
store_retrieval_non_linear_hnsw/size_100000                  1.00    676.5±5.99µs        ? ?/sec    1.31   884.3±32.15µs        ? ?/sec
store_retrieval_non_linear_kdtree/size_100                   1.00    197.6±0.17µs        ? ?/sec    1.00    197.7±0.13µs        ? ?/sec
store_retrieval_non_linear_kdtree/size_1000                  1.01   1109.1±2.12µs        ? ?/sec    1.00   1103.6±4.49µs        ? ?/sec
store_retrieval_non_linear_kdtree/size_10000                 1.00     11.6±0.33ms        ? ?/sec    1.03     11.9±0.37ms        ? ?/sec
store_retrieval_non_linear_kdtree/size_100000                1.02    136.0±0.54ms        ? ?/sec    1.00    133.6±1.06ms        ? ?/sec
store_sequential_insertion_without_predicates/size_100       1.00    268.4±0.72µs        ? ?/sec    1.00    269.2±0.33µs        ? ?/sec
store_sequential_insertion_without_predicates/size_1000      1.00      2.7±0.01ms        ? ?/sec    1.00      2.7±0.00ms        ? ?/sec
store_sequential_insertion_without_predicates/size_10000     1.00     26.9±0.11ms        ? ?/sec    1.00     26.9±0.10ms        ? ?/sec
store_sequential_insertion_without_predicates/size_100000    1.01    270.3±0.65ms        ? ?/sec    1.00    268.5±0.27ms        ? ?/sec

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant