4 ポイント 投稿者 GN⁺ 2024-10-19 | 1件のコメント | WhatsAppで共有

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.sAddFloat32_amd64.s ファイルを通じて、SSE対応の有無を確認し、float32 の加算演算を実行する方法を説明する。

性能と今後

  • Goのソフトウェア実装とPlan9 SIMD実装の性能差を示すチャートから、およそ200〜450%の高速化を確認した。
  • このメモが、Plan9とSIMDを使ったプロジェクトの着想につながることを願っている。

# GN⁺のまとめ

  • この記事は、Goの並行性と並列性を活用して性能を最大化する方法を紹介している。
  • Plan9アセンブリ言語とSIMD命令を用いて、ハードウェアレベルで並列計算を実行する方法を説明している。
  • この記事はGo開発者に対してPlan9とSIMDの活用可能性を示し、性能向上のための新しいアプローチを探るうえで有用となりうる。
  • 類似した機能を持つプロジェクトとして、RustのSIMD対応ライブラリやC++のSIMD関連ライブラリを勧めている。

1件のコメント

 
GN⁺ 2024-10-19
Hacker Newsのコメント
  • Goアセンブリについていくつか指摘すると、amd64ではあのintは実際には64ビットです
    int32を使うとパラメータリスト内でワードアラインされますが、落とし穴があります。64ビットシステムでは、戻り値は常にダブルワードアラインされたオフセットから始まります
    NOSPLITはGoコンパイラが自動で提供するtextflag.hに定義されています。ただ、読んだ限りではNOSPLITruntime.XX関数でしか尊重されないようで、ここでは何もせず、必要でもありません
    NOSPLITは、スタックがあふれる可能性があり分割が必要かどうかを確認するコードをコンパイラに挿入させない、という意味です。スタック領域を必要としない関数なら技術的には不要で、基本的にはスタック分割を検査する関数自身の中に、その検査コードが注入されるのを防ぐために存在します

  • 4が「NOSPLIT」を表し、なぜか必要だとされている点が気になる人のために補足すると、通常、フレームサイズ(NOSPLITの後ろのパラメータ)の後には引数サイズが続き、この2つはマイナス記号で区切られます
    これは減算ではなく、独特の構文にすぎません。フレームサイズ$24-8は、その関数が24バイトのフレームを持ち、呼び出し元フレーム上の8バイトの引数で呼び出される、という意味です
    TEXTNOSPLITを指定しない場合、引数サイズは必ず指定しなければなりません。Goのプロトタイプを持つアセンブリ関数の場合、go vetが引数サイズが正しいかを検査します
    出典: https://go.dev/doc/asm

  • 「GoはPlan9という独自の内部アセンブリ言語を使っている」とありますが、その言語は本当にそう呼ばれているのでしょうか?

    • いいえ。単にGoアセンブリです
      Plan 9由来の構文ではありますが、私たちはGoアセンブリと呼んでいます
      https://go.dev/doc/asm を参照
    • もっともな疑問です。最初は私もそのまま正しいと思っていました。このテーマを調べた人がそんなところを間違えるはずはないと思ったし、プロジェクトを少し知っていれば、その名前もある程度筋が通っているように感じられたからです
      しかし調べれば調べるほど、これはLLMの幻覚のように思えてきました
      アセンブリ形式のドキュメントは正式な名前を付けておらず、単にgo assemblerと呼んでいます
      この幻覚の出どころは、おそらく最初の段落でしょう。「アセンブラはPlan 9アセンブラの入力スタイルを基にしており…現在の文書では、その構文の要約と相違点、Goと相互作用するアセンブリコードを書く際の特記事項を説明する」
    • 特別な名前があるわけではありません。Plan 9はオペレーティングシステムで、このアセンブリ構文スタイルはそのOSで使われていたアセンブラに由来します
      「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

    • 実際に効果はありました。Goは、新しいプログラミング言語ならクロスコンパイルを基本能力として備えるべきだという期待値を作り、当時それをうまくこなす言語はほとんどありませんでした
    • ちなみにSpiderMonkeyも約25年にわたってほぼ同じことをしてきましたし、他のJavaScript仮想マシンもそうだと思います
  • 「スライスにSIMD演算を行う関数が必要だった」とありましたが、実際にはどう使うのか気になりました
    記事全体を検索しても、スライスに対して行われる演算はどこにもありませんでした
    追記: リンク先のドキュメントで見つけました: https://pkg.go.dev/github.com/pehringer/simd#pkg-index
    基本的に、足し合わせたいスライスが2つあるなら、forループの代わりにSIMDで並列処理して、simd.AddInt32(slice1, slice2, result)のように使えます

  • 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/

    • 実際には、2008年ごろ以降のすべてのチップがSSE4.1をサポートしていると見てよいでしょう
  • 筆者はこの部分を混同しているようなので、参考リンクを残しておきます: https://en.wikipedia.org/wiki/Plan_9_from_Bell_Labs

  • 私も、これはコードを LLM が誤って解釈した結果 のように見えると言おうとしていたところだった
    Plan 9 という用語を知っていて、アセンブリにまで踏み込んでいるのに、自分がどんな領域に入り込んでいるのか分かっていない、というのは他の形ではあまり想像できない。他の人たちも同じことを考えていたのを見て分かった
    もしこれが正しいなら、書き手には恥ずかしがったり「見つかった」と感じたりせず、正直でいてほしい。そうすればこちらも学べる。こういう「LLM 露呈」タイプについて確信を得たいのだが、どれほど明白に見えても、本人が認めるケースはほとんどないように見える
    もちろん、ここでは明白というわけではなく、かなり早まった決めつけ気味の推測にすぎない

    • 正直に言うと? いや。LLM が きちんとこなすべき作業 をしなくて済む近道だと思ったその傲慢さは、恥じるべきだ
      こういうのは本当に腹が立つ
  • 自分のレベルを少し超える内容だったが、記事が読者を一緒に連れていく進め方は楽しかった
    こういう試みが初めてのはずはないのでは? SIMD に飢えている Gopher は文字どおり何十人もいそうだ。より一般的なパターンは CGO を使うことなのだろうか?

    • cgo の問題は関数呼び出しのオーバーヘッドが大きい点。かなり大きな処理の塊にだけ使うのがよい
      Go からアセンブリ関数を呼び出すほうがずっと安い
      https://pkg.go.dev/github.com/grailbio/base/simd に、この方向で自分が取り組んだものがある
    • 昔から人々は間違いなく試してきたと思う。ほぼ10年ほど前、兄弟が Go で SIMD ライブラリを作ろうとしていたときに(Skype だったかな?)通話していた記憶がある
      記憶が正しければ、当時は複数の AVX 命令 が Go の Plan 9 アセンブラではエンコードすらできず、バイト列として直接エンコードする必要があった [0]
      私が見た中で最も完成度の高いライブラリは、使ったことはないが、CGO を部分的に使いつつそのオーバーヘッドを避けるきれいなハックを使っている [1]
      [0]: https://github.com/slimsag/rand/blob/f1e8d464c0021a391d5cd64...
      [1]: https://github.com/alivanz/go-simd/
    • こうしたものを標準ライブラリに追加しようという提案は却下されたが、背景を理解する助けになる: https://github.com/golang/go/issues/53171
  • Go のアセンブリプログラムを書くなら、Avo(https://github.com/mmcloughlin/avo) を確認することを勧める
    型安全性を提供し、有効なアセンブリを出力しているかいくつか検査してくれる。レジスタを動的に割り当てることができ、スタックやフレームサイズなども自分で計算せずに済むようにしてくれる
    呼び出し規約の細部も処理してくれるので、引数を望むレジスタや位置に簡単にロードできる
    最近、Go の crypto ライブラリにある amd64 アセンブリをすべて Avo に移植したが、こうした作業には非常に有用なライブラリだ