AWS i4i split-MGLRU full matrix (09/23): scan-fix + rivals-ON
VMemKV re-measured on the scan-fix build (sub-page-only T2 page containment + inline scan reads, scan batching removed after churn A/B showed zero benefit). LTM 64KB Scan/YCSB-E recovered to 0903-era wins. Rival entries are reused from the 2026-09-21 run (rival code unchanged); VMemKV-vs-rival LTM headline cells were additionally anchored same-box on 2026-09-22.
Stacking Variants
各最適化の詳細は Design Document を参照。
- RocksDB: LSM-Tree のベースライン比較
- LMDB: B+Tree / mmap ベースの比較対象
- RocksDB-BlobDB: RocksDB の Blob 分離ストレージ変種(大きな Value 向け)
- LeanStore: B+Tree / pointer-swizzling ベースの比較対象 (64KB値は格納不可のため除外)
- Baseline: vmemkv 最適化なし
- +BF: + T1 Bloom Filter
- +Inline: +BF + T1 Inline Value + adaptive read policy (trifecta, production config)
- +PinRandom: +Inline のread policyをrandom-access mappingへピン留め (trifecta ablation)
- +PinSeq: +Inline のread policyをsequential mappingへピン留め (trifecta ablation)
Environment
AWS i4i.8xlarge spot, Ubuntu 24.04 (ami-0879aeb9a3801dce6), Release -O3, 1GiB LTM cgroup per cell. VMemKV/LTM: MGLRU-off + sub-page-only containment + inline (no-batching) Scan build. Rivals: 2026-09-21 numbers (MGLRU-on, code unchanged). See docs/benchmark/20260921_scan64_readahead_regression.md.
Workloads
- Insert: 順次挿入(1/4/16/32 スレッド)
- Get Hit/Miss (Zipf/Uniform): ポイントルックアップ
- Update / Delete: Zipf 分布での更新・削除
- Scan (Zipf/Uniform): レンジスキャン
- YCSB-E: 95% Scan + 5% Insert 混合(32スレッド / 30秒タイムライン)
Workload Winners & Relative Speedup
Best VMemKV variant vs. whichever rival (RocksDB, LMDB, RocksDB-BlobDB) is FASTEST at that cell, at 32 threads (max concurrency tested) -- not the best-of-any-thread-count-or-rival figure, since both axes are known to shrink (sometimes invert) the ratio if picked favorably. The rival actually being compared against is named in small text under each badge.
| Workload | 8B In-Memory | 1KB In-Memory | 1KB LTM | 64KB LTM |
|---|---|---|---|---|
| Insert |
✅ WIN (1.53x)
vmemkv (+Inline)
vs RocksDB-BlobDBR1R4
|
✅ WIN (1.43x)
vmemkv (+Inline)
vs RocksDB-BlobDBR1R4
|
✅ WIN (1.34x)
vmemkv (+Inline)
vs RocksDB-BlobDBR1R4
|
✅ WIN (1.83x)
vmemkv (+PinRandom, +PinSeq 同着)
vs RocksDB-BlobDBR1R9
|
| Update (Zipf) |
✅ WIN (1.50x)
vmemkv (Baseline)
vs RocksDB-BlobDBR2
|
✅ WIN (1.50x)
vmemkv (Baseline, +Inline 同着)
vs RocksDB-BlobDBR2
|
✅ WIN (1.26x)
vmemkv (+Inline)
vs RocksDBR2R9
|
✅ WIN (1.93x)
vmemkv (+Inline)
vs LMDBR2R9
|
| Delete |
✅ WIN (1.73x)
vmemkv (+Inline)
vs RocksDB-BlobDBR3R4
|
✅ WIN (1.77x)
vmemkv (Baseline, +BF, +Inline 同着)
vs RocksDBR3
|
✅ WIN (1.91x)
vmemkv (Baseline, +BF, +PinRandom, +PinSeq 同着)
vs RocksDB-BlobDBR3R9
|
✅ WIN (3.69x)
vmemkv (+PinSeq)
vs RocksDB-BlobDBR3R9
|
| Get (Miss) |
✅ WIN (2.22x)
vmemkv (+BF)
vs RocksDBR5
|
✅ WIN (2.44x)
vmemkv (+BF)
vs LMDBR5
|
✅ WIN (2.35x)
vmemkv (+PinRandom, +PinSeq 同着)
vs LMDBR5R6R8
|
✅ WIN (2.07x)
vmemkv (+BF, +PinSeq 同着)
vs LMDBR5R6R8
|
| Get (Hit, Zipf) |
✅ WIN (1.39x)
vmemkv (Baseline)
vs LMDBR1
|
✅ WIN (1.22x)
vmemkv (Baseline)
vs LMDBR1
|
≈ EVEN (0.99x)
vmemkv (+Inline, +PinRandom 同着)
vs RocksDB-BlobDBR1R6R7R8
|
WIN (1.07x)
vmemkv (+Inline)
vs RocksDBR1R4R8
|
| Get (Hit, Uniform) |
WIN (1.09x)
vmemkv (Baseline)
vs LMDBR1
|
✅ WIN (1.18x)
vmemkv (Baseline)
vs LMDBR1
|
WIN (1.07x)
vmemkv (+Inline, +PinRandom 同着)
vs RocksDB-BlobDBR1R4R7R8
|
≈ EVEN (0.97x)
vmemkv (+Inline)
vs RocksDB-BlobDBR1R6R8
|
| Scan (Zipf) |
✅ WIN (1.15x)
vmemkv (+Inline)
vs LMDBR4
|
≈ EVEN (1.04x)
vmemkv (Baseline, +BF, +Inline 同着)
vs LMDB
|
✅ WIN (1.32x)
vmemkv (Baseline)
vs RocksDBR6R8
|
WIN (1.12x)
vmemkv (+Inline)
vs RocksDBR6R4R7R8
|
| Scan (Uniform) |
WIN (1.14x)
vmemkv (+Inline)
vs LMDBR4
|
WIN (1.09x)
vmemkv (Baseline, +BF, +Inline 同着)
vs LMDBR10
|
✅ WIN (1.43x)
vmemkv (Baseline, +BF, +PinSeq 同着)
vs RocksDBR6R8
|
WIN (1.08x)
vmemkv (+Inline, +PinSeq 同着)
vs RocksDB-BlobDBR6R10R7R8
|
| YCSB-E |
✅ WIN (3.56x)
vmemkv (+Inline)
vs RocksDB-BlobDBR4
|
✅ WIN (1.70x)
vmemkv (Baseline)
vs RocksDBR1
|
✅ WIN (1.19x)
vmemkv (Baseline, +Inline, +PinSeq 同着)
vs RocksDBR6R8
|
WIN (1.06x)
vmemkv (Baseline)
vs RocksDBR6R10R8
|
Why each cell wins -- reason legend by concept (hover or focus a badge to highlight its cells)
C1 Key-Value Separation (Two-Region Index) -- キーと値の分離。キーのみをTier 1 インデックスに置き、メモリ上に収めることでディスクI/OなしでValueの位置を特定する。
- R1 Direct index-to-log path (Bitcask-style): 書きはT2追記+T1 append追加、読みは二層探索+T2直読み。圧縮も版管理もしない素直な経路。 (docs/specification/low_level_design.md 3.1)
C2 In-Place Updates (Mutable/Stable Split) -- mutable / immutable regions を分離することで、in-place update を可能にして、hot data の読み書きを高速に行う。
- R2 alloc内 in-place Update: 割当内に収まる更新はT2追記・T1書換なしで完結する。Zipf Updateの主因。 (docs/specification/low_level_design.md 3.3)
- R3 tombstone-only Delete: DeleteはT1のtombstone書換のみでT2に触らない。Deleteの主因。 (docs/specification/low_level_design.md 3.4)
C3 Index-Only Reads -- Value を並べたTier 2に直接触れず、Tier 1インデックスだけで完結するreadを提供する。
- R4 T1 Inline Value: 8バイト以下の値はT2をバイパスしT1 payloadに格納する。8Bシナリオと混在コーパスの8B分を底上げする。Scanのinline分はclean boundsでper-entry再検査を省略する(効果は+Inline差に織り込み済みで分離不可)。 (docs/specification/low_level_design.md 7.2)
- R5 Bloom filter: sorted_regionのmissをO(1)で打ち切り、T2への不要なランダムリードを回避する。Get_Missの主因。 (docs/specification/low_level_design.md 7.3)
C4 Trifecta Read Policy over Immutable Base -- immutable base はロック無しで読める。サイズ・アクセスパターン別にmmap二つとpreadの3つの経路を使い分け、OSのページ機構を活用する。
- R6 base/tail trifecta read policy: base領域をレコードサイズ・reader別に読む(主mmap / sequential mmap / pread+mincore)。LTMのScan・大レコードGetの主因。+PinSeq/+PinRandomはそのablation。trifecta自体は常時有効のため、この表からの分離は不可で根拠は過去の測定文書による。 (docs/specification/low_level_design.md 7.5, docs/benchmark/20260807_scan_t2_base_tail_io_uring_read.md, docs/benchmark/20260810_get_base_mmap_fast_path.md)
- R7 T2 page-contained placement: 1ページ以下のレコードはページ跨ぎなし、大レコードは詰めて配置。cold readのfault数を最小化し、Scanのreadaheadマージを保つ。 (docs/specification/low_level_design.md 2.2, docs/benchmark/20260919_t2_page_containment_ab.md)
C5 Paging Regime -- classic-LRU推奨とsplit-MGLRU測定条件。機構ではなく再現条件。
- R8 split-MGLRU paging condition: LTMはsplit-MGLRU条件(VMemKV classic-LRU / rival MGLRU-on)で測定しており、条件差自体が差分の一部になり得る。 (docs/specification/low_level_design.md 7.6, docs/benchmark/20260919_mglru_sensitivity.md, docs/benchmark/20260920_split_condition_publication.md)
Annotations -- 技術概念ではなく状態の注記。再測定対象。
- R9 要調査(再測定対象): variant間に実装で説明できない乱高下があり、このセルでは勝因を断定しない。再測定で分散を確認する。 (cell_reasons.json R9)
- R10 僅差(要再測定): 全storeが団子で誤差と区別困難。WIN表示を残しつつ再測定対象とする。 (cell_reasons.json R10)
- LTMセルはsplit-MGLRU条件(VMemKV classic-LRU / rival MGLRU-on)で測定しており、差分の一部はページング条件差を含む。詳細は low_level_design.md 7.6 と docs/benchmark/20260920_split_condition_publication.md を参照。
- セル名の+Inline/+BF/+PinSeqはそのセルで最速だったvariantであり、必ずしも勝因そのものではない(近接variant間の差が小さい場合は incidental)。1%以内の同着はセル内に併記する。勝因の検証は同レポート内のablation差分(Baseline vs +BF vs +Inline)で行うこと。R9のセルは差分自体が不安定なため理由を断定しない。R10のセルは全storeが団子のため誤差と区別できない。
- R9の割付基準: 同一セル内でvariant間に実装で説明できない落差(+BF 0.33〜0.64x、+Inline 0.73〜0.77x、+PinRandom 0.51〜0.77x)があり、1.03〜1.09x級の効果主張がノイズと区別できない場合。Bloomは読み専用ゲート(src/t1_index/t1_index.hpp find_sorted)のためwrite系の落差を説明できない。
Background Jobs: reorganize() / checkpoint() / defragment()
Fixed reference point (1KB values, 10,000,000 records; in-memory unconstrained, LTM cgroup-constrained to the same memory budget as the rest of the suite) -- not swept across the CRUD matrix's 4 scenario/value-size combos. Duration is a single call in isolation; the three degradation columns are the percentage drop in concurrent Insert/Update/Scan QPS while that one call runs, measured over a matched-duration window on both sides (see run_background_jobs_probe.sh). reorganize() forces every shard through a synchronous merge in one call -- a manual escape hatch (pre-backup flush, capacity planning), not what happens during ordinary operation. The last rows summarize the organic per-shard split alternative that actually runs day to day (averaged across every split observed in that run; see the detail section further down for the full breakdown and charts).
| Job / Scenario | Duration (1 call) | Insert QPS degradation | Update QPS degradation | Scan QPS degradation |
|---|---|---|---|---|
| reorganize() (forced, all shards) / in-memory | 1.277s | 56% | -18% | 6% |
| reorganize() (forced, all shards) / LTM | 0.476s | 99% | 3% | 15% |
| checkpoint() / in-memory | 2.536s T1 merge 1595ms · WAL rotate 128ms · msync 589ms | 49% | -3% | 9% |
| checkpoint() / LTM | 4.827s T1 merge 2271ms · WAL rotate 0ms · msync 0ms | 68% | 20% | 14% |
| defragment() (forced, one cycle) / in-memory | 0.002s | 43% | 17% | 21% |
| defragment() (forced, one cycle) / LTM | 0.000s | 52% | 20% | 0% |
| per-shard split (organic) / in-memory | 213ms avg of 8 splits | 81% | n/a | n/a |
| per-shard split (organic) / LTM | 225ms avg of 4 splits | 80% | n/a | n/a |
Organic Per-Shard Splits
Writer-visible pause per automatic per-shard split during 90s sustained inserts.
Writer-visible pause duration
Insert Workload
Update (Zipf) Workload
Delete (Zipf) Workload
Get Miss (Zipf) Workload
Get Hit (Zipf) Workload
Get Hit (Uniform) Workload
Scan (Zipf) Workload
Scan (Uniform) Workload
YCSB-E — Scan + Insert 混合スループット(30秒タイムライン)
YCSB-E ワークロード: 95% Scan(100件レンジスキャン)+ 5% Insert を 32スレッドで 30秒間実行。
藍色の点線: 自然発生のT1 Reorganize(Adaptive Soft Limitによる自動トリガー、全バリアント共通)。
強制トリガーはバリアントごとに独立集計(全バリアント共通の1本の線には集約しない): t=5秒予定のreorganize()(細かい点線)、t=10秒・t=25秒予定のcheckpoint()(粗い点線)を、そのバリアント自身の色+マーカー形状(スキャンQPS線をホバーした際の点と同じ形。凡例のポイント形状も対応)で描画。
実際に発火する秒(線の位置)は予定秒とは限らない(将棋倒しに対するガード無し、書き込み負荷次第で数十秒遅延することがある)。8B In-Memoryのように挿入スループットが極端に高いワークロードでは、t=5秒予定のreorganize()自体が20秒以上かかり、後続のcheckpoint()トリガー(t=10s/25s)がこの30秒間に一度も発火しないまま終わることもある(この場合、線は1本しか出ない)。カーソルを線に合わせると、その秒に発火したバリアント・種類・予定秒・実発火秒・所要時間を表示。
Scan QPS タイムライン
32 threads / 30 secondsInsert Workload
Update (Zipf) Workload
Delete (Zipf) Workload
Get Miss (Zipf) Workload
Get Hit (Zipf) Workload
Get Hit (Uniform) Workload
Scan (Zipf) Workload
Scan (Uniform) Workload
YCSB-E — Scan + Insert 混合スループット(30秒タイムライン)
YCSB-E ワークロード: 95% Scan(100件レンジスキャン)+ 5% Insert を 32スレッドで 30秒間実行。
藍色の点線: 自然発生のT1 Reorganize(Adaptive Soft Limitによる自動トリガー、全バリアント共通)。
強制トリガーはバリアントごとに独立集計(全バリアント共通の1本の線には集約しない): t=5秒予定のreorganize()(細かい点線)、t=10秒・t=25秒予定のcheckpoint()(粗い点線)を、そのバリアント自身の色+マーカー形状(スキャンQPS線をホバーした際の点と同じ形。凡例のポイント形状も対応)で描画。
実際に発火する秒(線の位置)は予定秒とは限らない(将棋倒しに対するガード無し、書き込み負荷次第で数十秒遅延することがある)。8B In-Memoryのように挿入スループットが極端に高いワークロードでは、t=5秒予定のreorganize()自体が20秒以上かかり、後続のcheckpoint()トリガー(t=10s/25s)がこの30秒間に一度も発火しないまま終わることもある(この場合、線は1本しか出ない)。カーソルを線に合わせると、その秒に発火したバリアント・種類・予定秒・実発火秒・所要時間を表示。
Scan QPS タイムライン
32 threads / 30 secondsInsert Workload
Update (Zipf) Workload
Delete (Zipf) Workload
Get Miss (Zipf) Workload
Get Hit (Zipf) Workload
Get Hit (Uniform) Workload
Scan (Zipf) Workload
Scan (Uniform) Workload
YCSB-E — Scan + Insert 混合スループット(30秒タイムライン)
YCSB-E ワークロード: 95% Scan(100件レンジスキャン)+ 5% Insert を 32スレッドで 30秒間実行。
藍色の点線: 自然発生のT1 Reorganize(Adaptive Soft Limitによる自動トリガー、全バリアント共通)。
強制トリガーはバリアントごとに独立集計(全バリアント共通の1本の線には集約しない): t=5秒予定のreorganize()(細かい点線)、t=10秒・t=25秒予定のcheckpoint()(粗い点線)を、そのバリアント自身の色+マーカー形状(スキャンQPS線をホバーした際の点と同じ形。凡例のポイント形状も対応)で描画。
実際に発火する秒(線の位置)は予定秒とは限らない(将棋倒しに対するガード無し、書き込み負荷次第で数十秒遅延することがある)。8B In-Memoryのように挿入スループットが極端に高いワークロードでは、t=5秒予定のreorganize()自体が20秒以上かかり、後続のcheckpoint()トリガー(t=10s/25s)がこの30秒間に一度も発火しないまま終わることもある(この場合、線は1本しか出ない)。カーソルを線に合わせると、その秒に発火したバリアント・種類・予定秒・実発火秒・所要時間を表示。
Scan QPS タイムライン
32 threads / 30 secondsInsert Workload
Update (Zipf) Workload
Delete (Zipf) Workload
Get Miss (Zipf) Workload
Get Hit (Zipf) Workload
Get Hit (Uniform) Workload
Scan (Zipf) Workload
Scan (Uniform) Workload
YCSB-E — Scan + Insert 混合スループット(30秒タイムライン)
YCSB-E ワークロード: 95% Scan(100件レンジスキャン)+ 5% Insert を 32スレッドで 30秒間実行。
藍色の点線: 自然発生のT1 Reorganize(Adaptive Soft Limitによる自動トリガー、全バリアント共通)。
強制トリガーはバリアントごとに独立集計(全バリアント共通の1本の線には集約しない): t=5秒予定のreorganize()(細かい点線)、t=10秒・t=25秒予定のcheckpoint()(粗い点線)を、そのバリアント自身の色+マーカー形状(スキャンQPS線をホバーした際の点と同じ形。凡例のポイント形状も対応)で描画。
実際に発火する秒(線の位置)は予定秒とは限らない(将棋倒しに対するガード無し、書き込み負荷次第で数十秒遅延することがある)。8B In-Memoryのように挿入スループットが極端に高いワークロードでは、t=5秒予定のreorganize()自体が20秒以上かかり、後続のcheckpoint()トリガー(t=10s/25s)がこの30秒間に一度も発火しないまま終わることもある(この場合、線は1本しか出ない)。カーソルを線に合わせると、その秒に発火したバリアント・種類・予定秒・実発火秒・所要時間を表示。