SwissTable

Google が Abseil C++ ライブラリ (absl::flat_hash_map / absl::flat_hash_set) で実装した高性能ハッシュテーブルアルゴリズム。制御バイト配列をデータと分離し、SIMD 命令で一括探索することで、従来のオープンアドレッシング法を大幅に上回る性能を実現する。

2017年に Google 社内で開発され、CppCon 2017 の Matt Kulukundis による講演 “Designing a Fast, Efficient, Cache-friendly Hash Table, Step by Step” で公開された。現在は Rust (hashbrown)、Go (go1.24 の map 実装)、Zig など複数言語に移植されている。

従来手法の問題

チェイン法 (Separate Chaining)

bucket[0] → [K,V] → [K,V] → null
bucket[1] → null
bucket[2] → [K,V] → null

従来のオープンアドレッシング (線形探索)

[K,V,meta] [K,V,meta] [K,V,meta] [K,V,meta] ...

SwissTable の核心: メタデータ分離 + SIMD

SwissTable はオープンアドレッシングだが、メタデータ (制御バイト) をデータスロットから分離して密に配置する。

メモリレイアウト

┌─────────────────────────────────────┐
│ Control Bytes (1 byte × N)          │  ← 密に連続配置
│ C0  C1  C2  C3  C4  C5  ...  C_N-1 │
├─────────────────────────────────────┤
│ Data Slots (sizeof(K,V) × N)        │  ← 各スロットに K,V ペア
│ [K,V]₀ [K,V]₁ [K,V]₂ ...          │
└─────────────────────────────────────┘

※ hashbrown (Rust 実装) ではデータスロットが逆順に配置されるなど、細部は実装により異なる。

制御バイト (Control Bytes)

各スロットに 1バイト のメタデータが対応する。3つの状態をエンコード:

ビットパターン名称意味
0b1111_1111 (0xFF)EMPTY未使用スロット
0b1000_0000 (0x80)DELETED削除済み (トゥームストーン)
0b0xxx_xxxxFULL使用中。下位7ビットに H2 ハッシュ を格納

最上位ビットだけで「空か使用中か」を即座に判定できる。

従来手法との比較: 従来のオープンアドレッシングではメタデータが各スロットにインラインで埋め込まれ、1スロットあたり 8バイト以上のオーバーヘッドになることが多い。SwissTable は 1スロットあたり 1バイト のみ。

ハッシュの分割: H1 / H2

64ビットハッシュ値を2つの用途に分割する:

hash (64 bits)
├── H1 (上位57ビット) → テーブル内の探索開始位置 (グループインデックス)
└── H2 (下位 7ビット) → 制御バイトに格納する「指紋」

H2 が一致してもキーが異なる確率 (偽陽性) は 約 1/128。つまり大半の不一致は 7ビット比較だけで排除でき、実際のキー比較はほぼ真のマッチ時のみ発生する。

グループ探索 (SIMD Probing)

SwissTable の性能の核心。制御バイトを グループ 単位で SIMD 命令を使い一括比較する。

グループサイズ

プラットフォームグループ幅使用命令
x86/x86_64 (SSE2)16_mm_cmpeq_epi8 + _mm_movemask_epi8
ARM (NEON)16vceqq_u8 等
LoongArch (LSX)16LSX ベクトル命令
ポータブル (fallback)8ビット演算で模擬

探索の流れ

1. hash = hasher(key)
2. group_index = H1(hash) & bucket_mask
3. h2 = H2(hash)   // 7ビット指紋

4. loop {
     // グループの制御バイト 16個を SIMD レジスタにロード
     ctrl = load_group(control_bytes[group_index])

     // H2 を 16個複製したベクトルと 1命令で並列比較
     matches = simd_cmpeq(ctrl, broadcast(h2))
     //        → 16スロット同時に「H2 が一致するか」判定

     // マッチしたスロット位置のビットマスクを取得
     for slot in bitmask_iter(matches) {
       if key == data[slot].key {
         return Found(slot)     // 真のマッチ
       }
       // H2 一致だがキー不一致 → 偽陽性 (約 1/128)
     }

     // EMPTY スロットがあるなら探索終了 (キーは存在しない)
     if has_empty(ctrl) {
       return NotFound
     }

     // 次のグループへ (三角数探索)
     group_index = next_group(group_index, stride)
   }

1回の SIMD 命令で 16スロットを同時検査できるため、従来の1スロットずつの線形探索と比べて探索速度が劇的に向上する。

なぜキャッシュ効率が良いのか

制御バイト: [C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF]
             ← ──────── 16バイト = 1キャッシュライン未満 ──────── →

三角数探索 (Triangular Probing)

グループ間の移動はリニアではなく、三角数列に基づくステップで行う:

stride = 0
pos = H1(hash) & bucket_mask

反復ごと:
  stride += Group::WIDTH
  pos = (pos + stride) & bucket_mask

ステップ列: 0, W, 2W, 3W, 4W, ... (W = グループ幅)

テーブルサイズが 2の冪であることと合わせて、全グループを正確に1回ずつ訪問することが数学的に保証される。これにより、最悪ケースでも全テーブルを網羅的に探索できる。

ロードファクター

ハッシュテーブル実装最大ロードファクター
std::unordered_map (C++)100% (チェイン法)
Robin Hood (旧 Rust std)〜90%
SwissTable87.5% (7/8)
従来リニア探索〜70%

87.5% という高いロードファクターが成立するのは、SIMD グループ探索により衝突コストが低いため。メモリ効率と速度の両立を達成している。

削除とトゥームストーン

オープンアドレッシングでは削除が探索チェーンを壊す問題がある。SwissTable の戦略:

トゥームストーンは新しい挿入時に再利用される。ただし growth_left カウンターはトゥームストーンでは減少しない (容量の人工的枯渇を防止)。

大量削除が発生するワークロードでは、トゥームストーン蓄積が探索を遅延させる。このとき rehash (in-place) でトゥームストーンを一掃する。

空テーブル最適化

空のテーブルは、静的に確保された EMPTY バイト配列 (グループアライメント済み) を参照する。最初の挿入までヒープアロケーションは一切発生しない。HashMap::new() のコストは実質ゼロ。

各言語での採用状況

言語採用実装
C++ (Abseil)absl::flat_hash_map / absl::flat_hash_setオリジナル実装
Ruststd::collections::HashMap (1.36〜)hashbrown クレート経由
Go標準 map (go1.24〜)internal/runtime/maps で Swiss Table 実装
Zigstd.HashMapSwissTable ベース

パフォーマンス比較 (概数)

操作SwissTableRobin Hoodstd::unordered_map (C++)
lookup (hit)1.0x (基準)1.5〜2x 遅い2〜3x 遅い
lookup (miss)1.0x1.5〜2x 遅い3〜5x 遅い
insert1.0x1.2〜1.5x 遅い2〜3x 遅い
メモリ/エントリ1 byte overhead8 bytes overheadポインタ + ノード

※ miss (不在キーの探索) での差が特に大きい。SwissTable は EMPTY を見つけた瞬間に探索を終了でき、SIMD でそれを高速に検出できるため。

押さえどころ(カード化候補)

関連

Graph