私たちの 64 ビット Intel および AMD プロセッサは数十年にわたって進化してきました。Go プログラムを 64 ビット Intel または AMD プロセッサ向けにコンパイルすると、コンパイラはデフォルトで約 20 年前の命令セットをターゲットにします。生成されるバイナリは事実上すべての x64 チップで動作しますが、2003 年以降に追加された命令はすべて利用されません。

私たちはしばしば マイクロアーキテクチャレベル について言及します。各レベルは、存在を前提にできる命令セット拡張のセットをまとめています:

Level Adds (roughly)
v1 元の AMD64 ベースライン(SSE2)
v2 popcnt、SSE4.2
v3 AVX2
v4 AVX-512 (F/BW/DQ/VL)

私の見解では、この階梯はすでにやや時代遅れです。2020 年頃に固定されており、ハードウェアはそれ以降も進化しています。最新の AVX-512 サブ拡張(VBMI、VBMI2、VNNI、BF16、FP16、VPOPCNTDQ など)を追加する必要があり、最近のサーバーおよびコンシューマーチップはこれらをサポートしていますが、v4 では必須ではありません。v1 から v4 までは有用な共通言語ですが、今日の「CPU が提供するすべてを使う」という現実的なターゲットには少なくとも v5 が必要であり、全体の方式をより細粒度の機能検出に置き換えるべきだという意見もあります。

いずれにせよ、Go ツールチェーンは GOAMD64 環境変数を通じて v1 から v4 の階梯を公開しています。GOAMD64=v3 を設定すると、コンパイラは AVX2 までを含むすべての機能を使用できます。デフォルトは最低共通分母である v1 です。

これにより、明らかな疑問が生じます。実際のパフォーマンス重視のライブラリを各レベルで再コンパイルした場合、実際にどれほどの利得が得られるでしょうか? 私はデータベースや検索エンジンで使用される圧縮ビットセットデータ構造である Roaring Bitmaps を選びました。

Roaring Bitmap は 32 ビット整数の集合を格納します。32 ビット空間を 65,536 値のチャンクに分割し、上位 16 ビットをキーとして、各チャンクを下位 16 ビットのみを保持する コンテナ に格納します。コンテナには 3 種類の形状があり、ライブラリは常に最も小さいものを保持します:

  • 配列コンテナ:16 ビット値のソート済みリストで、チャンクが疎(最大で数千要素程度)の場合に使用
  • ビットマップコンテナ:平坦な 8 KB ビットベクトル(65,536 ビット、各可能な値に 1 ビット)で、チャンクが密な場合に使用
  • ランコンテナ:[start, length] 区間のリストで、セットビットが連続したランに集まる場合に使用

ライブラリの最新リリースを取得し、独自のベンチマークスイートをレベルごとに 4 回実行し、それぞれ 8 サンプルを収集しました。これは、Go 1.26.2 および Roaring v2.18.2 の下で、4 つのレベルすべて(AVX-512 を含む)をサポートする単一の Intel Xeon Gold 6548N(Emerald Rapids)上で実施しました。

ポピュレーションカウント(または popcount、ハミング重みとも呼ばれる)は、単にマシンワード内の 1 に設定されたビットの数です。Roaring はこれを頻繁に使用します。ビットマップコンテナのカーディナリティ、つまり保持する値の数は、その 1024 個の 64 ビットワードのポピュレーションカウントの合計です。現代の x86 チップにはこれを単一の操作で実行する専用の popcnt 命令がありますが、これは v2 レベル(SSE4.2、2008 年)で利用可能になりました。これがない場合、コンパイラは複数命令のビット操作シーケンスにフォールバックする必要があります。

最も明確な単一の結果はポピュレーションカウントです。ビットマップコンテナ内のセットビットの数を数えます。v1 のベースラインは popcnt 命令を使用できないため、Go はソフトウェアフォールバックを出力します。v2 に移行すると、popcnt が利用可能になり、時間はほぼ半分に短縮されます:

これは 43% の削減であり、無料です。ソース変更は不要で、コンパイラフラグを指定するだけです。ただし、v3v4 はこれ以上何ももたらさないことに注意してください。単一の popcnt 命令はすでに最適であり、Go コンパイラの観点では AVX2 および AVX-512 は何も追加しません。

ポピュレーションカウントは簡単な勝利です。ライブラリの残りの部分はどうでしょうか?

もう一つの明確な勝利は、密なビットマップからコンテナを構築することです。FromDense array ベンチマークは生の 8 KB ビットベクトルを受け取り、それに対して最もコンパクトなコンテナを構築します。各ワードをポピュレーションカウントしてカーディナリティを調べ、セットビットの位置をスキャンします。このワード単位の popcount-and-scan ループは、256 ビットレジスタが利用可能になるとコンパイラが自動ベクトル化できるものであり、v2 を超えても利得が続きます:

v2 はスカラー popcnt/tzcnt 命令を使用することですでに 21% を削減し、v3(AVX2)はそれをほぼ 2 倍の 38% 削減にします。popcount と同様に、v4 は何も追加しません。

集合演算も同じパターンを示します。IntersectionCardinality ベンチマークは、2 つのビットマップに共通する値の数を数えます。ビットマップコンテナの場合、単語ごとに AND を取り、結果をポピュレーションカウントすることで、交差を実際に生成することなく処理します。ここでは v2 は実質的に何もせず(スカラー popcnt はすでに内側のループに含まれている)、v3 によりコンパイラが AND-and-count ループを 256 ビットレジスタに拡張でき、時間を 22% 短縮します:

要点:

  1. 現代のハードウェアでは、誰もが v2 以上を使用すべきです。生成されるバイナリは、どのデータセンターや古くないラップトップでも動作します。
  2. v3 レベルは調査する価値があるかもしれません。
  3. v4 レベルは一部のベンチマークで役立つはずでしたが、役立ちませんでした。Go コンパイラがまだ十分に優れていないのではないかと思います。

(当然ながら:独自のベンチマークを実行してください。)