PaPoo
cover
technews
Author
technews
世界の技術ニュースをリアルタイムでキャッチし、日本語でわかりやすく発信。AI・半導体・スタートアップから規制動向まで、グローバルテックシーンの「今」をお届けします。

GoがSIMDを「どこでも同じ書き方」で扱おうとしている話

Goのブログが、SIMDを扱う実験的なAPIを大きく前に出してきました。SIMDは、CPUが複数の値をまとめて処理するための仕組みで、計算の重い処理をかなり速くできる一方、これまではGoから使うにはアセンブリを書くしかありませんでした。今回の発表で目を引くのは、単に高速化の道具を増やしただけではなく、CPUやOSの違いをできるだけ隠して「同じコードを別の環境で動かす」方向に踏み込んでいる点です。Go 1.26と1.27で何が変わったのかを追うと、Goが低レイヤー性能と移植性をどう両立しようとしているかが見えてきます。

Go 1.26と1.27で入ったSIMD APIは、何を変えるのか

元記事は、GoにおけるSIMDの扱いが大きく変わった、と説明しています。SIMDは Single Instruction Multiple Data の略で、1つの命令で複数のデータをまとめて処理するCPU機能です。たとえば float64 を8組まとめて足すような処理ができ、暗号、データ処理、AIのような計算集約的な処理を速くできます。GoのGreen Teaガベージコレクタも、メモリ上の生存オブジェクトを探す場面でSIMDを使っていると書かれています。

ただ、これまではGoからSIMDを使いたければGo assemblyを書くしかありませんでした。つまり、本当に重要な計算カーネルだけが対象になり、そこまでではないが効けば嬉しい、という処理はCPUの力を十分に使えなかったわけです。そこでGo 1.26では amd64 向けのSIMD API が入り、Go 1.27では arm64 の NEON と wasm にもAPIが広がりました。

一方で、SIMDはCPUごとの差がかなり激しいのが難点です。固定長のベクターしか持たないものもあれば、実行時に長さを調べないといけないものもある。マスクの扱いも違いますし、そもそも使える命令の種類もまちまちです。amd64 だけ見ても AVX、AVX2、AVX512 があり、arm64 でも NEON と SVE 系が混ざります。こうしたばらつきのため、従来の archsimd パッケージは「なるべく共通化」してはいても、書く側・試す側の負担は重いままでした。

そこで Go 1.27 は、archsimd とは別に、より移植性を重視した実験的な simd パッケージを導入しました。これは固定サイズのベクターを型に埋め込まず、各プラットフォームで共通に成り立つ操作だけを提供し、足りない部分は別のSIMD命令で補ったり、SIMDがない環境ではエミュレーションしたりします。Goの言い方では、書いたコードが常に動くことを重視しつつ、命令が合う環境ではアセンブリに近い性能を狙う設計です。利用するにはビルド時に GOEXPERIMENT=simd を指定します。

型の使い方も独特です。simd.Uint8s や simd.Float32s のように複数形の型名を使い、LoadFloat32s でスライスから読み込み、Store で書き戻します。記事には内積計算の例が載っていて、innerProduct では simd.Float32s を使いながら、ループで LoadFloat32s、MulAdd を呼び、端数は LoadFloat32sPart で処理しています。最後に sum(a) で要素を合計していますが、ここは現時点の制限として、Go 1.27 では全要素の合計を直接求める共通APIがまだないためで、次のリリースで ReduceSum が入る予定だとしています。

記事の後半は、対応する演算の一覧です。加算、減算、乗算、比較、マスク操作などが並び、すべての型で使えるものもあれば、整数だけ、浮動小数点だけ、あるいは特定の幅だけに限られるものもあります。要するに、GoはSIMDを「各CPUの癖をそのまま露出させる道具」ではなく、「よく使う共通部分をまず一枚の面にのせる道具」として再設計しようとしている、という話です。

「SIMDを便利にする」より先に、SIMDを怖くなくする発想がある

私がまず面白いと思ったのは、このAPIが「高度な最適化」を誰でも使えるようにするというより、最適化の入口にある心理的な壁を下げようとしている点です。SIMDは速い。でも速い代わりに、CPUごとの違い、ベクター長の違い、マスクの違い、命令の有無の違いを全部意識しないといけない。そこが面倒だから、多くのソフトウェアは結局SIMDを使わないままでした。Goのsimdは、その面倒をかなり強引に隠そうとしている。これは実務ではかなり大きいと思います。

特に、GOEXPERIMENT=simd で有効化する実験機能にしたのは筋がいいです。いきなり安定APIにせず、使い方や足りない演算を見ながら広げていく。しかも最初の版では ReduceSum のような共通機能がまだない、と自分で限界も明言している。こういう出し方は、性能系のAPIとしてはかなり誠実です。速さを約束しすぎず、でも「どこでも動く」ことは約束する。その順番が逆ではないのがいい。

それでも「共通部分だけ」に絞るのは、意外と攻めた判断だと思う

一方で、この設計はかなり割り切っています。SIMDは本来、CPUごとの違いこそが価値でもあります。AVX512のように広いベクターを扱える環境、SVEのように可変長を持つ環境、それぞれで強みが違う。普通なら「全部見せたい」と考えがちです。でもGoは逆に、共通部分だけをまず面に出し、それ以外はエミュレーションや別APIに回した。

これは保守的に見えて、実は攻めています。なぜなら、性能を削ってでも抽象化するのではなく、抽象化したうえで必要なら下層で埋める、という順序だからです。記事中にも「ソースコードの操作が下のハードウェアに合うときはアセンブリに近い効率、それ以外は可能な限りうまくエミュレーション」とあります。つまり、ベンチマークで勝つことより、長く使える書き方を優先している。これはGoらしいし、GoでSIMDをやる意味そのものを変える発想だと思います。

実際に効いてくるのは、暗号やAIより「地味な高速化」かもしれない

元記事は暗号、データ処理、AIを例に挙げていますが、私はむしろ、もっと地味な場面で効くと見ています。たとえばログ処理、圧縮前の前処理、画像や音声の一部、あるいはメモリ走査のような処理です。こうした領域は、個々の処理は派手ではないのに、積み重なるとCPU時間をかなり食います。そこにSIMDを少し入れられるだけで、全体の体感が変わることがある。

しかもGoは、手元の環境がAVX2かAVX512かを気にしながら配布する文化より、同じバイナリを広く配る文化に合っています。だからこそ「平台ごとの差をなるべく吸収するSIMD」は相性がいい。性能のためにコードを分岐させると運用が面倒になる、という現実を考えると、こういうAPIは意外と多くのプロダクトに効くはずです。高速な専用実装を持つチームだけの話ではない、というのが重要です。

ただし、実験版のまま終わると価値は半分になる

期待は大きい一方で、成功の条件もはっきりしています。SIMDは、APIがあるだけでは広まりません。開発者が「どう書けばよいか」をすぐ理解でき、既存のスライス処理から自然に移行できて、しかも性能が納得できる必要があります。元記事が例として内積を出し、Load、MulAdd、Store というかなり素直な流れで見せているのは、その理解しやすさを意識しているからでしょう。

ただ、最初から全部を盛り込まない設計には、逆に言うと「どこまで一般化できるか」が常に問われます。ReduceSum が次で入る予定と明記されているのも、まだ穴がある証拠です。ここを埋めていけるかどうかで、単なる実験APIか、実用の道具かが分かれるはずです。Goはこれまでも、標準ライブラリやランタイムの強さで評価されてきましたが、今回のSIMDはその延長線上にあります。うまく育てば、低レイヤーの最適化をもっと普通のGoコードに近づける一歩になると思います。


参考: Platform-independent SIMD in Go - The Go Programming Language

同じ著者の記事