Building an efficient hash table in Java

2025/12/14 2:41

Building an efficient hash table in Java

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

要約

SwissTable は、Google の Abseil ライブラリから派生したオープンアドレッシングハッシュテーブル設計で、小さな「制御バイト」(メタデータ)をキー/値ストレージと分離しています。まず制御バイトをスキャンし、h1(グループ選択)と h2(制御バイトに格納されたフィンガープリント)の分割を利用することで、SIMD スタイルのフィンガープリントの一括比較が可能になり、最小限のキー比較で高速なプローブ検索が実現します。

Java では SwissMap が JDK の Vector API(

jdk.incubator.vector
)を用いてこの概念を実装しています。
ByteVector
を使用して複数の制御バイトを一度にロードし、各ベクトルを h2 マッチングと EMPTY/DELETED チェックの両方で再利用することでメモリトラフィックを削減します。制御配列の末尾にセントネルパディング領域があるため、境界チェックなしで固定幅ベクトルロードが可能です。テムストーン(DELETED マーカー)は挿入時に再利用され、カウントが閾値を超えると同容量リハッシュがトリガーされてプローブチェーンを短く保ちます。サイズ変更は新しいキャパシティに対して h1/h2 を再計算する単一の線形走査で行われ、重複作業を回避します。

SwissMap のイテレーションはモジュラーステップ置換(奇数ステップサイズ)を使用し、すべてのスロットを正確に一度ずつ訪問しながらループをタイトでキャッシュフレンドリーに保ちます。この手法は従来のハッシュマップでは使われません。

Windows 11 上で Eclipse Temurin JDK 21.0.9 と AMD Ryzen 5 5600 を使用したベンチマークでは、SwissMap は

HashMap
、fastput、Object2ObjectOpenHashMap、および UnifiedMap よりも競争力があり、高負荷率でも優れた性能を示し、小さなペイロードシナリオで最大 50 % のヒープ保持量削減を達成しました。

SwissTable はすでに Rust の

hashbrown
と Go の
map
を動かしており、最大 60 % の速度向上を提供しています。Java プロトタイプは Vector API が成熟すれば本番環境へ移行できる見込みで、現在の
HashMap
を置き換えるか補完する可能性があります。広く採用されれば、エンタープライズアプリケーションのヒープ使用量を削減し、スケーラビリティを向上させ、言語横断的にコレクション実装の新しい標準となるでしょう。

本文

1) SwissTable プロジェクト ― 基本的な説明

SwissTable は、Google の研究から生まれたオープンアドレッシング型ハッシュテーブルの設計で、新しい C++ ハッシュテーブルとして発表され(後に Abseil に組み込まれました)。
基本構造は従来と同じで、

hash(key)
を算出し、開始スロットを決めて線形探索を行い、キーまたは空スロットを見つけるというものです。

ここでのひねりは、メタデータ(小さな「制御バイト」)と実際のキー/値ストレージを分離し、制御バイトだけでほぼすべてのコストのかかるキー比較を回避する点にあります。
冷たいキャッシュ状態のキー配列(ポインタが多い)は触らずに、まず密度が高くキャッシュフレンドリーな制御バイト配列をスキャンします。
これは小さなブルームフィルターと似ており、「ここに候補があるかもしれない」という低コストの判定を先に行う仕組みです。

探査を高速化するため、SwissTable はハッシュを 2 部分に分割します。

h1
は探索開始位置(どのグループから見るか)を決める役目で、
h2
は制御バイトに格納される小さなフィンガープリントです。
完全なハッシュではなく、実際のキーにアクセスする前に候補を絞り込むだけのビット数です。

検索時にはまず

(h1, h2)
を算出し、
h1
によってグループへジャンプします。
そのグループ内の制御バイトすべてと
h2
を比較し、一致したらのみキーを触ります。
つまり、多くのミス(および多くのヒット)で実際にキーメモリをタッチせず、メタデータが「候補がある」と判断するまで待ちます。

探査コストが低いので、SwissTable は高負荷率(約 87.5 % = 7/8)でもパフォーマンス崩壊に陥りにくく、メモリ効率も向上します。
結果として、キャッシュミスやキー比較回数が減るだけでなく、オーバーフローバケットなどの副構造を少なくできる設計となっています。


2) SwissTable が複数言語で「デフォルト感覚」になるまで

世代を超える設計といえば、それがライブラリのトリックから標準ライブラリへ登場する瞬間です。

  • Rust – Rust 1.36.0 以降、

    std::collections::HashMap
    は SwissTable ベースの
    hashbrown
    実装に切り替わりました。
    二次プロービングと SIMD ラックアップを使用しており、精神的・技術的には SwissTable にほぼ沿っています。

  • Go – Go 1.24 で Swiss Table デザインに基づく新しいビルトインマップ実装が導入されました。
    マイクロベンチマークでは Go 1.23 より最大 60 % 高速、全体アプリケーションベンチでは約 1.5 % の CPU 時間改善を報告しています。

この時点で SwissTable は「C++ のクールなトリック」ではなく、現代のベースラインになっていました。
Rust が採用し、Go が ship した…なら Java も同じ設計に挑戦すべきだと直感的に思えました。
最新 CPU と強力 JIT、Vector API の到来で、技術的不可能性ではなく「やらなければならない」という itch が芽生えたのです。


3) SwissTable の秘密兵器と Java Vector API

SwissTable の高速さは、比較を広く(ワイドに)行うことから来ています。
複数の制御バイトを一度にチェックし、ブランチを減らす SIMD ワークロードです。
これは「1 バイトずつループして分岐する」スカラーコードよりもはるかに効率的です。

Java でこれを実装するには、以前は auto‑vectorization に頼ったり、

Unsafe
を使ったり、JNI を書いたり、あるいは純粋なスカラー処理を受け入れたりしました。
しかし Vector API は「Java がベクトル計算を表現できるように設計された incubator」で、サポート CPU で SIMD 命令へコンパイルされます。

Java 25 の Vector API はまだ incubating ですが、

jdk.incubator.vector
にあります。
私にとって重要だったのは「最終版かどうか」ではなく、「SwissTable の制御バイトスキャンをクリーンに表現できるほど使えるか」という点でした。

設計の核は次の通りです:

  1. 制御配列(コンパクト)+別々のキー/値ストレージ
  2. h1
    で初期グループを選択、
    h2
    はそのグループ内の制御バイトに格納される小さなフィンガープリント
  3. 探索は 2 段階パイプライン:
    • ベクトルスキャンで
      h2
      と一致するビットを探す
    • 一致したインデックスで実際のキー比較を行う

挿入も同じスキャンを再利用し、既存キーが無い場合は最初に見つかった空スロットへ格納します。

Java ならではのレイアウト課題

  • C++ ではキー/値を密にパックできますが、Java ではオブジェクト参照の配列になるためキーアクセス自体がキャッシュミスを招く可能性があります。
    そのため「キーはできるだけ遅れてタッチし、必要なときには最小限にする」設計が重要です。

  • 削除は tombstone(削除済みだが空ではない)マーカーを用います。
    ただし tombstone が蓄積するとパフォーマンス低下につながるため、クリーンアップ戦略も必要です。

  • リサイズは「全体再ハッシュ」がコスト高ですが、Go のテーブル分割や拡張可能ハッシュのように成長戦略を工夫すれば改善できます。

Vector API は「魔法ではなく最適化ツール」として扱い、ロード・比較・マスク・イテレーションという明確なループ構造で実装しました。


4) SwissMap において本当に重要だった要素

要素なぜ重要か
制御バイト & レイアウト非プリミティブキーの場合、実際のコストは「数バイト読み」ではなく「ポインタ追跡」です。
equals()
が冷たいオブジェクトを歩くとキャッシュミスが発生します。SwissMap は制御配列でまず候補を絞り、キー/値へのアクセスを最小限に抑えます。
負荷率従来のオープンアドレッシングは負荷率上昇でプローブチェーンが急増しますが、SwissTable は制御バイトスキャンが主コストなので「1 つ多いグループ」は単なるベクトルロード+比較にすぎません。最大負荷率 ~0.875 を目指し、混雑時でも高速を保ちます。
セントネルパディングSIMD は固定幅(16/32 バイト)ロードを好むため、配列末尾のグループでオーバーリードを防ぐ小さなパディングが必要です。これにより JIT が予測しやすくなります。
H1/H2 分離
h1
は開始グループ選択、
h2
は「ここに候補があるかもしれない」低コストフィルタです。制御バイトは 7 ビットで
h2
を保持し、残りのビットを EMPTY/DELETED の状態管理に使用します。
ロード済みベクトル再利用同じ
ByteVector
ロードを 1 回だけ行い、
h2
マスクと EMPTY/DELETED 判定の両方で使います。これが実際のパフォーマンス差を生む重要なポイントです。
トゥームストーン削除時に DELETED マーカーを置き、再利用可能な空スロットを記録します。探査チェーンを短く保ち、リサイズ圧力を低減します。
同容量リハッシュでのクリーンアップトゥームストーンが一定割合を超えると、DELETED を EMPTY に戻す同容量リハッシュを実行し、論理内容は変えずにチェーン長を短くします。
リサイズ時の冗長チェック回避リサイズは
h1
がキャパシティに依存するため再ハッシュが必須です。旧テーブルを走査しつつ、同じ制御バイトプローブロジックで新テーブルへ挿入します。
イテレーションモジュラーステップ(奇数ステップ)で全インデックスを 1 回ずつ訪問し、キャッシュ分布とループ密度を保ちます。

制御バイト中心の検索ループ(簡略版)

protected int findIndex(Object key) {
    if (size == 0) return -1;
    int h = hash(key);
    int h1 = h1(h);
    byte h2 = h2(h);
    int nGroups = numGroups();
    int visitedGroups = 0;
    int mask = nGroups - 1;
    int g = h1 & mask; // 効率的な modulo
    for (;;) {
        int base = g * DEFAULT_GROUP_SIZE;
        ByteVector v = loadCtrlVector(base);
        long eqMask = v.eq(h2).toLong();
        while (eqMask != 0) {
            int bit = Long.numberOfTrailingZeros(eqMask);
            int idx = base + bit;
            if (Objects.equals(keys[idx], key)) { // 見つかった
                return idx;
            }
            eqMask &= eqMask - 1; // LSB をクリア
        }
        long emptyMask = v.eq(EMPTY).toLong(); // 同じベクトルを再利用
        if (emptyMask != 0) { // 空があれば確定ミス
            return -1;
        }
        if (++visitedGroups >= nGroups) {
            return -1; // 無限ループ防止(全 tombstone のケース)
        }
        g = (g + 1) & mask;
    }
}

5) ベンチマーク

以下は高負荷率でのプロービングとポインタ多いキーを重視した JMH 設定です。
実行環境:Windows 11 x64、Eclipse Temurin JDK 21.0.9、AMD Ryzen 5 5600(6C/12T)。

シナリオSwissMap hitSwissMap missHashMap hitHashMap miss
put****
get****

主な結果

  • 高負荷率で SwissMap は他のオープンアドレッシングテーブルと競争力があり、JDK
    HashMap
    と同等かそれ以上のスループットを実現。
  • メモリ使用量はフラットレイアウト(バケット/オーバーフロー無し)+最大負荷率 0.875 により、小さいペイロードでは
    HashMap
    よりも 50 % 以上メモリ節約。

注意:Vector API はまだ incubating、テーブルは高負荷・参照キー向けに最適化されているため、プリミティブ専用マップや低負荷構成では結果が異なる可能性があります。


6) 簡単な備考

ベンチマークセクションで「SWAR‑style SwissTable」が突然登場するのは、後続調整で同じ制御バイトワークフローを SWAR(SIMD Within a Register)で実装し、Vector API を使わずに高速化したためです。
SWAR は「レジスタ内で SIMD」を意味し、64 ビット単位で複数の制御バイトを比較する手法です。
結果として同じ高速パスが、よりポータブルかつ JDK バージョンに依存せず実装できます。

この投稿は 3 本に分けるべき内容をまとめたもので、次回で SWAR の詳細(設計・実装)を公開予定です。ぜひご期待ください。


P.S. コードが欲しい方へ

この記事は HashSmith と呼ばれる JVM 向け高速メモリ効率のハッシュテーブル集を公表している試作プロジェクトの物語版です。
SwissMap(SwissTable 風、Vector API を用いた SIMD 探索)と SwissSet の両方が含まれています。
実験的で本番向けではありませんが、JMH ベンチマークやドキュメントを付属しているため、数値再現や実装詳細の検証が可能です。

ベンチマークを走らせたい、エッジケースを確認したい、あるいはより良い探査/リハッシュ戦略を提案したい場合はぜひ issue / PR をお寄せください。


P.P.S. 見逃せない講演

Google のエンジニア Matt Kulukundis らが CppCon で行った SwissTable に関するトークは非常に分かりやすく、設計の核心を掴める内容です。
ぜひご覧ください:https://www.youtube.com/watch?v=ZzQ0j3pRkJ4

同じ日のほかのニュース

一覧に戻る →

2025/12/16 6:37

Fix HDMI-CEC weirdness with a Raspberry Pi and a $7 cable

## Japanese Translation: > **概要:** > Samsung S95B TV(論理アドレス 0x00)、Denon AVR‑X1700H(0x05)、Apple TV、PS5、Xbox Series X、Nintendo Switch 2、および `/dev/cec0` をリッスンする Raspberry Pi 4 が含まれるホームシアター構成で、テレビの入力にのみ切り替えるコンソールが原因となるオーディオルーティング問題を著者は解決します。 > Pi(論理アドレス 0x01)から AVR に「System Audio Mode Request」パケット(`15:70:00:00`)を送信することで、受信機は ARC を有効化し、すべてのコンソールオーディオをテレビではなく自身経由でルーティングします。 > 著者は Python スクリプト `cec_auto_audio` でこれを実装しており、長時間稼働する `cec-client -d 8` を起動し、TRAFFIC 行から Active Source イベント(オペコード 0x82)を解析し、以前に Set System Audio Mode(オペコード 0x72)が検出されていない場合に毎回ウェイク時にパケットを送信します。 > スクリプトは systemd サービス `cec_auto_audio.service` としてパッケージ化され、起動時に開始されます。これにより、多層の HomeKit/Eve オートメーションと比べて低レイテンシで軽量な代替手段を提供します。 > トラブルシューティングガイドには、スキャン(`echo "scan" | cec-client -s`)、トラフィック監視(`cec-client -m`)、および欠落オペコード(0x82, 0x84, 0x70, 0x72)の良いケースと悪いケースの比較が含まれます。 > 残るエッジケースとして、コンソールのスタンバイがテレビチューナーを起動させる場合や HomeKit オートメーションがアクティブなソースなしでテレビをオンにする場合などには、追加の状態機械ロジックが必要になる可能性があります。著者はコミュニティメンバーに対し、より広範なトラブルシューティングのために CEC パケットトレースを共有してもらうよう呼びかけています。

2025/12/11 8:54

Nature's many attempts to evolve a Nostr

## Japanese Translation: **要約** 人気のあるアプリケーションの普遍的な設計は、ユーザーのデータと暗号鍵を所有する単一クラウドサーバーに集中しています(「あなたの鍵がないなら、あなたのデータではない」)。この中央集権化は封建制や寡占構造を生み出します。サーバーは橋を上げてユーザーを切り離す城のような存在です。フェデレーション(例:Mastodon、Matrix)はサーバー間で通信できるようにしますが、鍵とデータは依然としてサーバーの管理下にあり、ネットワーク理論はそのようなフェデレートシステムがスケールフリー分布へ収束し、支配的なハブを生み出すと予測しています。これはGmail/ProtonMail のメール寡占や Facebook Threads の ActivityPub ノードが Fediverse を支配する現象として観察されています。 セルフホスティングは居住IPの禁止やインフラコストにより多くのユーザーが個人サーバーから離れるため、非実用的になります。ピアツーピアネットワークはユーザー所有鍵を提供しますが、拡張性、信頼できないノード、スーパーpeer の中央集権化、複雑な最終的一致メカニズム、および長い多ホップルーティング遅延に悩まされます。 Nostr プロトコルは「リレーモデル」を提案します。単純で信頼できないリレーは署名されたメッセージを転送するだけで、相互通信しません。これにより \(N^2\) スケーリング問題を回避します。ユーザーは数個(通常 2–10)のリレーユーザーに購読し、自分のデータと鍵を完全に制御でき、リレーが失敗または停止した場合でも信頼性高く離脱できます。広く採用されれば、これはユーザーに真の所有権と単一点障害への耐久性を与え、中央集権サーバーに依存する企業に対し、よりユーザー中心で分散型アーキテクチャとの競争を強いるでしょう。これにより、ソーシャルメディアやメッセージングは真の分散モデルへと再構築される可能性があります。

2025/12/12 15:47

“Are you the one?” is free money

## 日本語訳: --- ## 要約 この記事は、番組「Are You the One?」の参加者が数学モデルを用いて、最終エピソード前にほぼ確実に全ての正しいカップルを推測できる方法を説明しています。戦略的にトゥルーブースとエピソード終了時のマッチアップデータを活用することで達成されます。 - **ゲーム設定**:10人の男性と10人の女性が、色でのみ明らかになる10組の完璧なペアに分けられます。参加者はすべてのペアを正しく推測し、100万ドルを獲得します。 - **情報源**: - *トゥルーブース* は特定のペアが成立しているかどうか(バイナリ結果)を確認します。 - *エピソードマッチアップ* はそのラウンドで正しいペアの総数のみを明らかにします。 「ブラックアウト」エピソード(0件マッチ)は、そのラウンド内のすべてのペアについて否定的な情報を提供し、複数のトゥルーブースと同等の効果があります。 - **モデル**:著者は OR‑Tools の最適化フレームワークを構築し、シーズン開始時に約400万件の有効マッチング(≈4 百万)を追跡し、各イベント後に更新します。シーズン1ではエピソード8でモデルが「解読」されました。 - **情報理論**:各イベントは約1〜1.6ビットの情報量を提供します。シミュレーションでは ~1.23 bits/イベント、実際の番組データでは ~1.39 bits/イベント、最適戦略で最大 1.59 bits/イベントが得られます。全検索空間は約22ビット(10!)を必要とするため、完璧な戦略には平均して約1.1 bits/イベントが十分です。 - **結果**: - ランダムペアリングでは、カップル数に関係なく平均正解スコアは約1になります。 - 100シーズンのランダムシミュレーションでモデルを使用した成功率は74%でしたが、情報理論戦略では98%に上昇します。 - 実際の番組データ(7シーズン)では71%の成功率と約1.39 bits/イベントとなり、純粋なランダムよりわずかに優れていますが、理論的最適値にはまだ届きません。 - **今後の作業**:著者はインタラクティブなウェブツールを開発予定で、ユーザーが異なる戦略を試し、必要な情報ビット数を確認し、実際のデータとパフォーマンスを比較できるようにします。 **影響** 本研究は参加者やプロデューサーに対して効率的な質問設計のための具体的なアルゴリズムフレームワークを提供し、エンターテインメントにおける組合せ最適化とベイズ推論の実用例を示すとともに、研究者にリアルワールドケーススタディとしてさらなる探求の機会を与えます。

Building an efficient hash table in Java | そっか~ニュース