HN公開: Go Plan9メモ
(pehringer.info)Goの並行性と並列性の活用
- Goの並行性と並列性を活用して、数値計算能力を向上させることを目指すプロジェクトの紹介。
- SIMD(Same Instruction Multiple Data) 命令を使用して、ハードウェアレベルで並列計算を実行できる。
- GoのコンパイラはSIMDを活用せず、適切な汎用SIMDパッケージも見つからなかったため、自分でパッケージを開発することにした。
Plan9アセンブリ言語
- GoはPlan9という独自のアセンブリ言語を使用しており、特定プラットフォームの命令やレジスタを少し修正して利用する。
- x86 Plan9とARM Plan9は互いに異なる。
- Plan9の簡単な例を通じて基本的な使い方を説明する。
Plan9の例
AddInts_amd64.sファイルとmain.goファイルを通じて、Plan9における基本的な関数宣言と使い方を説明する。- Goの呼び出し規約に従って、関数の引数と戻り値をスタックに保存する方法を説明する。
パッケージ設計計画
- 算術およびビット演算のSIMD処理に対する薄い抽象化レイヤーを提供するパッケージを設計する。
- アーキテクチャごとのPlan9実装を含む内部パッケージを作成し、初期化関数を通じてこれを設定する。
SIMDの例
- x86 SIMD Plan9関数の例を通じて、SIMDの使い方を説明する。
Supported_amd64.sとAddFloat32_amd64.sファイルを通じて、SSE対応の有無を確認し、float32の加算演算を実行する方法を説明する。
性能と今後
- Goのソフトウェア実装とPlan9 SIMD実装の性能差を示すチャートから、およそ200〜450%の高速化を確認した。
- このメモが、Plan9とSIMDを使ったプロジェクトの着想につながることを願っている。
# GN⁺のまとめ
- この記事は、Goの並行性と並列性を活用して性能を最大化する方法を紹介している。
- Plan9アセンブリ言語とSIMD命令を用いて、ハードウェアレベルで並列計算を実行する方法を説明している。
- この記事はGo開発者に対してPlan9とSIMDの活用可能性を示し、性能向上のための新しいアプローチを探るうえで有用となりうる。
- 類似した機能を持つプロジェクトとして、RustのSIMD対応ライブラリやC++のSIMD関連ライブラリを勧めている。
1件のコメント
Hacker Newsのコメント
Goアセンブリについていくつか指摘すると、amd64ではあの
intは実際には64ビットですint32を使うとパラメータリスト内でワードアラインされますが、落とし穴があります。64ビットシステムでは、戻り値は常にダブルワードアラインされたオフセットから始まりますNOSPLITはGoコンパイラが自動で提供するtextflag.hに定義されています。ただ、読んだ限りではNOSPLITはruntime.XX関数でしか尊重されないようで、ここでは何もせず、必要でもありませんNOSPLITは、スタックがあふれる可能性があり分割が必要かどうかを確認するコードをコンパイラに挿入させない、という意味です。スタック領域を必要としない関数なら技術的には不要で、基本的にはスタック分割を検査する関数自身の中に、その検査コードが注入されるのを防ぐために存在します4が「NOSPLIT」を表し、なぜか必要だとされている点が気になる人のために補足すると、通常、フレームサイズ(NOSPLITの後ろのパラメータ)の後には引数サイズが続き、この2つはマイナス記号で区切られますこれは減算ではなく、独特の構文にすぎません。フレームサイズ
$24-8は、その関数が24バイトのフレームを持ち、呼び出し元フレーム上の8バイトの引数で呼び出される、という意味ですTEXTにNOSPLITを指定しない場合、引数サイズは必ず指定しなければなりません。Goのプロトタイプを持つアセンブリ関数の場合、go vetが引数サイズが正しいかを検査します出典: https://go.dev/doc/asm
「GoはPlan9という独自の内部アセンブリ言語を使っている」とありますが、その言語は本当にそう呼ばれているのでしょうか?
Plan 9由来の構文ではありますが、私たちはGoアセンブリと呼んでいます
https://go.dev/doc/asm を参照
しかし調べれば調べるほど、これはLLMの幻覚のように思えてきました
アセンブリ形式のドキュメントは正式な名前を付けておらず、単に
go assemblerと呼んでいますこの幻覚の出どころは、おそらく最初の段落でしょう。「アセンブラはPlan 9アセンブラの入力スタイルを基にしており…現在の文書では、その構文の要約と相違点、Goと相互作用するアセンブリコードを書く際の特記事項を説明する」
「GNU Compiler CollectionはUnixという独自の内部アセンブリ言語を使っている」と言うのに似ています
Goチームがなぜこの専用アセンブリ形式を採用したのか気になるなら、Rob Pikeが2016年の発表でGoアセンブラの設計を扱っています [1][2]
要点は、ほとんどのアセンブリ言語はおおむね似ているので、「マシンの最下層と対話しながらも新しい構文を学ぶ必要がない」共通アセンブリ言語を作ろう、という観察だったようです
また、新しいアーキテクチャの命令マニュアルPDFを入力として受け取り、動作するアセンブラを自動生成できるようにもします
[1]: https://www.youtube.com/watch?v=KINIAgRpkDA
[2]: https://go.dev/talks/2016/asm.slide#1
「スライスにSIMD演算を行う関数が必要だった」とありましたが、実際にはどう使うのか気になりました
記事全体を検索しても、スライスに対して行われる演算はどこにもありませんでした
追記: リンク先のドキュメントで見つけました: https://pkg.go.dev/github.com/pehringer/simd#pkg-index
基本的に、足し合わせたいスライスが2つあるなら、
forループの代わりにSIMDで並列処理して、simd.AddInt32(slice1, slice2, result)のように使えますhttps://github.com/pehringer/simd/blob/main/simd_test.go
Goがサポートするプロセッサに関しては、このセクションが関連します (1)。基本的なx64サポートにはSSEとSSE2が含まれます
ただし、Goコンパイラが実際にそれを生成するかは分かりません。性能を最優先するgccのような非常に複雑なコンパイラとは異なり、GoコンパイラはWirth流(2)のシンプルで高速なコンパイラを好みます
(1) https://go.dev/wiki/MinimumRequirements#amd64
(2) https://irreal.org/blog/?p=7075
https://smartgo.blog/2024/01/06/niklaus-wirth/
筆者はこの部分を混同しているようなので、参考リンクを残しておきます: https://en.wikipedia.org/wiki/Plan_9_from_Bell_Labs
私も、これはコードを LLM が誤って解釈した結果 のように見えると言おうとしていたところだった
Plan 9 という用語を知っていて、アセンブリにまで踏み込んでいるのに、自分がどんな領域に入り込んでいるのか分かっていない、というのは他の形ではあまり想像できない。他の人たちも同じことを考えていたのを見て分かった
もしこれが正しいなら、書き手には恥ずかしがったり「見つかった」と感じたりせず、正直でいてほしい。そうすればこちらも学べる。こういう「LLM 露呈」タイプについて確信を得たいのだが、どれほど明白に見えても、本人が認めるケースはほとんどないように見える
もちろん、ここでは明白というわけではなく、かなり早まった決めつけ気味の推測にすぎない
こういうのは本当に腹が立つ
自分のレベルを少し超える内容だったが、記事が読者を一緒に連れていく進め方は楽しかった
こういう試みが初めてのはずはないのでは? SIMD に飢えている Gopher は文字どおり何十人もいそうだ。より一般的なパターンは CGO を使うことなのだろうか?
Go からアセンブリ関数を呼び出すほうがずっと安い
https://pkg.go.dev/github.com/grailbio/base/simd に、この方向で自分が取り組んだものがある
記憶が正しければ、当時は複数の AVX 命令 が Go の Plan 9 アセンブラではエンコードすらできず、バイト列として直接エンコードする必要があった [0]
私が見た中で最も完成度の高いライブラリは、使ったことはないが、CGO を部分的に使いつつそのオーバーヘッドを避けるきれいなハックを使っている [1]
[0]: https://github.com/slimsag/rand/blob/f1e8d464c0021a391d5cd64...
[1]: https://github.com/alivanz/go-simd/
Go のアセンブリプログラムを書くなら、Avo(https://github.com/mmcloughlin/avo) を確認することを勧める
型安全性を提供し、有効なアセンブリを出力しているかいくつか検査してくれる。レジスタを動的に割り当てることができ、スタックやフレームサイズなども自分で計算せずに済むようにしてくれる
呼び出し規約の細部も処理してくれるので、引数を望むレジスタや位置に簡単にロードできる
最近、Go の crypto ライブラリにある amd64 アセンブリをすべて Avo に移植したが、こうした作業には非常に有用なライブラリだ