2026-11-08 Lock-free Full Matrix Benchmark Report
Full 5-parallel matrix validating lock-free T2 ownership (Treiber stacks), in-place base_boundary bypass (eliminating SSD page faults on checkpointed records), and accelerated checkpoint probe pre-split under AWS Spot i4i.8xlarge.
Stacking Variants
各最適化の詳細は Design Document を参照。
- RocksDB: LSM-Tree のベースライン比較
- LMDB: B+Tree / mmap ベースの比較対象
- RocksDB-BlobDB: RocksDB の Blob 分離ストレージ変種(大きな Value 向け)
- LeanStore: B+Tree / pointer-swizzling ベースの比較対象 (64KB値は格納不可のため除外)
- Default: Bloom filter、T1 inline value、adaptive read policy を有効にした VMemKV 構成
- +PinRandom: Default のbase-region readをrandom-access mappingへ固定
- +PinSeq: Default のbase-region readをsequential mappingへ固定
- +PinNormal: Default のbase-region readをplain mappingへ固定
- +PinPread: Default のbase-region readをpreadへ固定
Environment
- AWS Spot i4i.8xlarge, ap-northeast-1, Ubuntu 24.04, NVMe instance store. LTM under MemoryHigh 1GiB / MemoryMax 2GiB / SwapMax 1TiB. VMemKV with Bloom filter, T1 inline values (1-8B), lock-free T2 ownership, base boundary bypass, A2 pacing gate, 64MiB chunked msync.
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.56x)
vmemkv (Default)
vs RocksDB-BlobDB
|
✅ WIN (1.25x)
vmemkv (Default)
vs RocksDB-BlobDB
|
LOSE (0.93x)
vmemkv (Default)
vs RocksDB-BlobDB
|
✅ WIN (1.74x)
vmemkv (Default)
vs RocksDB-BlobDB
|
| Update (Zipf) |
✅ WIN (1.45x)
vmemkv (Default)
vs RocksDB
|
≈ EVEN (0.99x)
vmemkv (Default)
vs RocksDB
|
✅ WIN (1.58x)
vmemkv (Default)
vs RocksDB
|
✅ WIN (1.81x)
vmemkv (Default)
vs LMDB
|
| Delete |
✅ WIN (1.73x)
vmemkv (Default)
vs RocksDB
|
✅ WIN (1.59x)
vmemkv (Default)
vs RocksDB
|
✅ WIN (1.30x)
vmemkv (Default)
vs RocksDB-BlobDB
|
✅ WIN (3.97x)
vmemkv (Default)
vs RocksDB-BlobDB
|
| Get (Miss) |
✅ WIN (2.19x)
vmemkv (Default)
vs RocksDB
|
✅ WIN (2.36x)
vmemkv (Default)
vs RocksDB
|
✅ WIN (2.33x)
vmemkv (Default)
vs LMDB
|
✅ WIN (2.02x)
vmemkv (Default)
vs LMDB
|
| Get (Hit, Zipf) |
✅ WIN (1.35x)
vmemkv (Default)
vs LMDB
|
✅ WIN (1.19x)
vmemkv (Default)
vs LMDB
|
≈ EVEN (1.03x)
vmemkv (+PinPread)
vs RocksDB-BlobDB
|
WIN (1.14x)
vmemkv (+PinPread)
vs RocksDB
|
| Get (Hit, Uniform) |
WIN (1.06x)
vmemkv (Default)
vs LMDB
|
WIN (1.14x)
vmemkv (Default)
vs LMDB
|
WIN (1.08x)
vmemkv (Default)
vs RocksDB-BlobDB
|
LOSE (0.94x)
vmemkv (Default 同着)
vs RocksDB-BlobDB
|
| Scan (Zipf) |
❌ LOSE (0.80x)
vmemkv (Default)
vs LMDB
|
≈ EVEN (1.01x)
vmemkv (Default)
vs LMDB
|
✅ WIN (1.24x)
vmemkv (+PinSeq)
vs RocksDB
|
WIN (1.12x)
vmemkv (Default 同着)
vs RocksDB
|
| Scan (Uniform) |
❌ LOSE (0.80x)
vmemkv (Default)
vs LMDB
|
WIN (1.05x)
vmemkv (Default)
vs LMDB
|
✅ WIN (1.17x)
vmemkv (Default 同着)
vs RocksDB
|
≈ EVEN (0.99x)
vmemkv (+PinSeq)
vs RocksDB-BlobDB
|
| Scan (Zipf, shuffled load) |
❌ LOSE (0.82x)
vmemkv (Default)
vs LMDB
|
≈ EVEN (1.00x)
vmemkv (Default)
vs LMDB
|
✅ WIN (1.87x)
vmemkv (Default)
vs RocksDB
|
✅ WIN (1.23x)
vmemkv (+PinNormal)
vs LMDB
|
| Scan (Uniform, shuffled load) |
LOSE (0.88x)
vmemkv (Default)
vs LMDB
|
≈ EVEN (1.03x)
vmemkv (Default)
vs LMDB
|
✅ WIN (2.15x)
vmemkv (+PinSeq)
vs RocksDB
|
WIN (1.05x)
vmemkv (+PinSeq)
vs RocksDB-BlobDB
|
| YCSB-E |
✅ WIN (4.72x)
vmemkv (Default)
vs RocksDB
|
≈ EVEN (1.01x)
vmemkv (Default)
vs RocksDB
|
❌ LOSE (0.85x)
vmemkv (Default)
vs RocksDB
|
≈ EVEN (0.95x)
vmemkv (Default)
vs RocksDB
|
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 Quadfecta Read Policy over Immutable Base -- immutable base はロック無しで読める。サイズ・アクセスパターン別にmmap三つとpreadの四つの経路を使い分け、OSのページ機構を活用する。
- R6 base/tail quadfecta read policy: base領域をレコードサイズ・reader別に読む(random_mmap / sequential_mmap / normal_mmap / pread_fd+mincore)。LTMのScan・大レコードGetの主因。+PinSeq/+PinRandomはそのablation。quadfecta自体は常時有効のため、この表からの分離は不可で根拠は low_level_design.md 7.5 による。 (docs/specification/low_level_design.md 7.5)
- R7 T2 page-contained placement: 1ページ以下のレコードはページ跨ぎなし、大レコードは詰めて配置。cold readのfault数を最小化し、Scanのreadaheadマージを保つ。 (docs/specification/low_level_design.md 2.2)
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)
- LTMセルはsplit-MGLRU条件(VMemKV classic-LRU / rival MGLRU-on)で測定しており、差分の一部はページング条件差を含む。詳細は low_level_design.md 7.6 を参照。
- 僅差の勝利セルは±CVで定量化する。再測定により誤差幅が確定したため僅差マーカーは廃止した。
- セル内の±は同一セル複数run(per-cell隔離)のCV。quiesce前4run+quiesce標準後の走行を混合し、nはセル毎に1〜8。quiesce前の分散を含むため保守的(広め)に出る。
- write系のrun間分散の原因は特定済み: clone残滓(quiesce標準化で解消)+短窓標本分散(30秒窓で収束確認)+T2書戻し/WAL fsync競合(WAL分離で解消・標準デプロイ化)。Delete/Insert/Update 64KBの分離走行で全variant収束を確認した。
Background Jobs: reorganize() / checkpoint()
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 | 0.573s | 54% | 64% | -2% |
| reorganize() (forced, all shards) / LTM | 0.249s | 99% | 44% | 100% |
| checkpoint() / in-memory | 2.569s T1 merge 1436ms · WAL rotate 77ms · msync 874ms | 53% | 55% | 77% |
| checkpoint() / LTM | 1.817s T1 merge 1344ms · WAL rotate 2ms · msync 389ms | 55% | 70% | 100% |
| per-shard split (organic) / in-memory | 212ms avg of 8 pause samples; 8 completed splits | 79% | n/a | n/a |
| per-shard split (organic) / LTM | 226ms avg of 3 pause samples; 3 completed splits | 83% | n/a | n/a |
Organic Per-Shard Splits
Writer-visible pause per automatic per-shard split during 90s sustained inserts.
Writer-visible pause duration
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.326s | 56% | -17% | 6% |
| reorganize() (forced, all shards) / LTM | 0.595s | 99% | 4% | 14% |
| checkpoint() / in-memory | 4.055s T1 merge 2232ms · WAL rotate 50ms · msync 1526ms | 48% | -1% | 3% |
| checkpoint() / LTM | 4.314s T1 merge 1627ms · WAL rotate 0ms · msync 0ms | 52% | 47% | 17% |
| defragment() (forced, one cycle) / in-memory | 0.002s | 51% | 7% | 29% |
| defragment() (forced, one cycle) / LTM | 0.000s | 49% | 12% | 0% |
| per-shard split (organic) / in-memory | 217ms avg of 8 splits | 73% | n/a | n/a |
| per-shard split (organic) / LTM | 230ms avg of 3 splits | 55% | n/a | n/a |
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
Scan (Zipf, shuffled load) Workload
Scan (Uniform, shuffled load) 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
Scan (Zipf, shuffled load) Workload
Scan (Uniform, shuffled load) 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
Scan (Zipf, shuffled load) Workload
Scan (Uniform, shuffled load) 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
Scan (Zipf, shuffled load) Workload
Scan (Uniform, shuffled load) 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本しか出ない)。カーソルを線に合わせると、その秒に発火したバリアント・種類・予定秒・実発火秒・所要時間を表示。