
2026/02/23 3:28
Go における並行ハッシュマップ実装のベンチマーク ---
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
記事は、Go 1.26 を使用し、AMD Ryzen 9 7900(12 コア/24 スレッド)で実際のワークロードを用いて、5 つの Go 並列マップライブラリ―
sync.Map、xsync.Map、cornelk/hashmap、alphadose/haxmap、および orcaman/concurrent-map ― をベンチマークしています。
設計
: 16 分岐の HashTrieMap。原子操作によるロックフリー読み取り、ノードごとのミューテックスで書き込み、遅延成長を採用。sync.Map
: キャッシュラインサイズのバケットハッシュテーブル。各バケットは独自のミューテックスで書き込みを保護し、完全にロックフリーな読み取り、SWAR キー フィルタリング、および協調リサイズ機能を備える。xsync.Map
: ロックフリーハッシュテーブルインデックス+ソート済みリンクリスト、原子 CAS 変化、>50 % の充填率でバックグラウンドリサイズ、効率的なトラバーサルを実装。cornelk/hashmap
: Harris のロックフリーリストアルゴリズムとハッシュテーブルインデックス、xxHash ハッシュ、遅延削除、自動リサイズ(>50 % 負荷時)を採用。alphadose/haxmap
: 32 個のシャードを持ち、各シャードはorcaman/concurrent-map
で保護された Go マップ。キーは FNV‑32 によりハッシュ化され、固定シャード数がスケーラビリティを制限する。sync.RWMutex
ベンチマーク – ワークロードは読み取り 100 % から 75 % 読み取り(12.5 % ストア、12.5 % ディレート)までで、WarmUp(事前にデータを投入)と NoWarmUp(空状態開始)の両方が含まれる。テストされたマップサイズは 100、1,000、100,000、および 1,000,000 エントリ。
結果 – すべてのマップは読み書きベンチマークで操作あたりゼロ割り当てを報告するが、バイト単位の割り当て(B/op)は異なる:
sync.Map が最高 B/op を持ち、xsync.Map は非シャード設計中で最も軽量、orcaman/concurrent-map は Go マップ上書き動作によりゼロを示す。cornelk/hashmap のパフォーマンスは 1,000 エントリを超えると低下するため、100 と 1,000 のサイズのみが評価された。
含意 – この研究は、並列マップを選択する開発者に対して具体的な指針を提供する:シンプルさ vs オーバーヘッド(
sync.Map)、読み取り中心ワークロードでの低 B/op(xsync.Map)、ロックフリー トラバーサル(cornelk/hashmap、alphadose/haxmap)、またはスケーラビリティのためのシャーディング(orcaman/concurrent-map)。将来的な作業では、大きなマップ、高い GOMAXPROCS 設定、およびより書き込み中心のシナリオを探求し、スケーラビリティをさらに検証できる。本文
go-concurrent-map-bench
Go における同時実行ハッシュマップ実装のベンチマークです。
免責事項
本リポジトリでベンチマーク対象にしているライブラリの一つ、の作者です。できる限り中立的かつ公平なベンチマークになるよう努めました。もし不備や改善点があれば、ぜひ issue を開いてください。xsync
対象ライブラリ
| ライブラリ | 説明 |
|---|---|
(stdlib) | Go 1.24 以降は に置き換えられています。これは各レベルで 16‑way の枝分かれを持つ同時実行ハッシュトライです。読み取りはトライノードのアトミックポインタ走査によりロックフリーで、書き込みはノードごとのミューテックスを取得します(影響は小さなサブツリーのみ)。エントリが挿入されるとトライは遅延生成されます。 |
| キャッシュラインサイズのバケットに分割されたハッシュテーブルです。各バケットには最大 5 エントリを保持し、書き込み時に個別ミューテックスを取得しますが、読み取りは完全にロックフリーでアトミックロードのみで済みます。キー検索はエントリごとのメタデータバイトを用いた SWAR(SIMD Within A Register)技術で高速化しています。テーブルのサイズ変更は協調的に行われ、すべてのゴルーチンが増加時にバケット移行を手伝います。 |
| ハッシュテーブルインデックスと衝突解決用のソート済みリンクリストを組み合わせたロックフリーハッシュマップです。挿入・更新・削除はすべてアトミック CAS 操作で行われます。バッファーゴルーチンが埋め込み率が 50 % を超えるとテーブルリサイズを開始します。ソートされたリストの順序により、同時走査が効率的です。 |
| Harris のロックフリーリンクリストアルゴリズムにハッシュテーブルインデックス層を組み合わせたものです。xxHash を使用し、すべての変更操作でアトミック CAS を利用します。削除は遅延的(論理的にマークされた後に物理削除)で、ロードファクタが 50 % を超えると自動リサイズされます。 |
| シンプルなシャーディング設計で、32 個の固定シャードを持ちます。各シャードは で保護された標準 Go マップです。キーは FNV‑32 ハッシュでシャードに割り当てられます。固定シャード数は実装を簡素化し予測可能ですが、高い並列度ではスケーラビリティが制限されます。 |
ワークロード
各ベンチマークはパーミル(‰)でランダムに操作を選択します。
| 読み取り割合 | ロード | ストア | デリート |
|---|---|---|---|
| 100 % 读取 | 全てロード (ウォームアップのみ) | – | – |
| 99 % 读取 | 99 % ロード | 0.5 % | 0.5 % |
| 90 % 读取 | 90 % ロード | 5 % | 5 % |
| 75 % 读取 | 75 % ロード | 12.5 % | 12.5 % |
| 競合下での範囲探索 | すべてのゴルーチンがマップをイテレート | バックグラウンドゴルーチンがランダムキーを継続的に更新 |
キータイプ
(長いプレフィックス付きでハッシュ計算を負荷させる)stringint
マップサイズ
| サイズ | 推定フットプリント | 目的 |
|---|---|---|
| 100 | 約 15 KB | L1 に収まる |
| 1,000 | 約 150 KB | L2 に収まる |
| 100,000 | 約 15 MB | L3 に収まる |
| 1,000,000 | 約 150 MB | RAM を超える |
備考:
はサイズが大きいと性能が著しく低下するため、100 と 1,000 のみでベンチマークしました。cornelk/hashmap
ウォームアップバリアント
- WarmUp – ベンチマーク開始前にマップを事前に埋め込みます(すべてのワークロード)。
- NoWarmUp – マップは空から始まり、混合ワークロードのみ実行します(100 % 读取はスキップ)。
実行方法
# すべてのベンチマークを走らせる go test -bench . -benchtime 5s # 特定ライブラリだけを走らせる go test -bench BenchmarkXsyncMapOf -benchtime 5s # string キーのみのベンチマークを走らせる go test -bench 'StringKeys' -benchtime 5s # サイズ=1000 のウォームアップベンチマークだけを走らせる go test -bench 'WarmUp.*size=1000' -benchtime 5s
実行環境
本リポジトリに収録されているベンチマーク結果は、以下の構成で取得しました。
- CPU: AMD Ryzen 9 7900 12‑Core Processor (24 スレッド)
- OS: Linux (amd64)
- Go: go1.26.0
- GOMAXPROCS: 1、4、8、12
各ベンチマークは
-benchtime 3s -count 3 で実行し、GOMAXPROCS ごとに結果を分離して収集しました。
結果
各プロットは Y 軸に ops/s(1 秒あたりの操作数)、X 軸に GOMAXPROCS を示しています。読み書きワークロードでは行が読み取り割合、列がマップサイズを表します。範囲探索用ベンチマークは 1 行で、列がマップサイズです。
備考:
はサイズが大きいと性能が著しく低下するため、100 と 1,000 のみでベンチマークしました。cornelk/hashmap
アロケーション率
すべての読み書きベンチマークにおいて、各ライブラリは 0 allocs/op を報告しています。ウォームアップバリアントについては B/op(1 回あたりに割り当てられるバイト数)を表す表を以下に示します。値はマップサイズと GOMAXPROCS に関係なく一貫しています。範囲探索ベンチマークはイテレーションオーバーヘッド(クロージャ、内部スナップショット、チャネルバッファ)で割り当てが発生するため除外します。
string キー
| ライブラリ | 100 % 读取 | 99 % 读取 | 90 % 读取 | 75 % 读取 |
|---|---|---|---|---|
| 0 | 0 | 3 | 9 |
| 0 | 0 | 1 | 2 |
* | 0 | 0 | 1 | 4 |
| 0 | 0 | 1 | 3 |
| 0 | 0 | 0 | 0 |
int キー
| ライブラリ | 100 % 读取 | 99 % 读取 | 90 % 读取 | 75 % 读取 |
|---|---|---|---|---|
| 0 | 0 | 3 | 8 |
| 0 | 0 | 0 | 2 |
* | 0 | 0 | 1 | 4 |
| 0 | 0 | 1 | 3 |
| 0 | 0 | 0 | 0 |
*
はサイズが大きいと性能が著しく低下するため、100 と 1,000 のみでベンチマークしました。cornelk/hashmap
orcaman/concurrent-map がゼロ割り当てを示すのは、シャードが標準 Go マップを使用しており、既存キーを書き換える際に追加割り当てが発生しないためです。他のライブラリは書き込み時に内部データ構造上少量のメモリ割り当てを行います。sync.Map が最も高い 1 回あたりの割り当てコストを持ち、xsync.Map は非シャード設計の中で最低です。
要約
| ライブラリ | 強み | 弱み |
|---|---|---|
| 標準ライブラリ、依存なし。読み取りスケーリングが優秀で、Go 1.24 以降は堅牢。 | 書き込み時の割り当てコストが高く、全ワークロードで より遅い。 |
| ほぼすべてのシナリオで最速。読み取り・書き込み・イテレーションスケーリングが優秀。非シャード設計中で割り当ては最低。 | 外部依存、書き込み時に割り当てが発生。 |
| 小規模かつ読み取り中心のワークロードで競合力がある。 | サイズ 100 K 以上で性能が大幅低下し、実務では小マップに限定される。 |
| 小規模読み取り専用で良好な性能を示す。ロックフリー設計。 | 書き込みスケーリングが悪く、高並列度では遅い。 |
| 割り当てゼロ、実装は単純で予測可能。シングルスレッド性能も良好。 | 固定 32 シャードによりスケーラビリティが制限され、読み取り専用のスループットが最悪。書き込みスケーリングも早期に plateau、イテレーションはチャネルベースで遅い。 |