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

コードの高速化: AMD64で16バイトを超える構造体を渡してはいけない

  • Neat言語の性能向上のため、配列を1つの構造体パラメータとして渡す代わりに、3つのポインタパラメータとして渡す方式に変更した。
  • Neatの配列がD言語の配列より遅かった理由は、24バイトの配列サイズが16バイトを超えており、その結果パラメータの渡し方が異なるためだった。
  • SystemV AMD64 ABI仕様によれば、16バイトを超えるすべての構造体はポインタ経由で渡される。

ベンチマークによる問題の確認

  • ベンチマークを通じて、構造体を渡す方式と個別のフィールドを渡す方式の性能差を確認した。
  • 構造体を渡す場合はスタックへの割り当てとコピーの過程が必要だが、個別のフィールドを渡す場合はSSEレジスタを通じてそのまま渡される。
  • 個別のフィールドを渡す方式は、構造体を渡す方式と比べて約2倍高速だった。

言語設計者の選択

  • C APIを呼び出す際にはC ABIに従う必要があるが、内部で使われる高級型を構造体として表現する必要はない。
  • 言語設計者は、配列、タプル、和型などがどのように渡されるかを決めることができる。
  • 16バイトを超える型を個別のフィールドとして渡すことは、性能向上に役立つ可能性がある。

GN⁺の意見

  • この記事は、ソフトウェア最適化に関心のある開発者にとって非常に有益だ。
  • 特に、性能に敏感なアプリケーションを開発する際に、構造体のサイズと受け渡し方法が重要な影響を与えうることを示している。
  • 言語設計者やAPI開発者は、この情報を活用して性能を改善できる機会を得られる。

1件のコメント

 
GN⁺ 2024-01-06
Hacker Newsのコメント
  • SysV amd64 ABIの問題に関連して、言語内部のABIをSysV以外に設定することは可能。SysVのC呼び出し元に公開されない限り、望む呼び出し規約を使える。NeatLangの違いはLLVMの呼び出し規約を変更するよりもはるかに複雑に見え、作者はCプログラムに一定の呼び出し規約で型を公開したいのかもしれない。
  • 引数受け渡しコストへの理解が不足していることは多く、それについて書かれたこの記事は有益。たとえばGoogleでは、24バイトのオブジェクトを値渡しする慣行はプロファイラには現れないが、あらゆる関数でコストが発生する。
  • x64へ移行する際、vec3オブジェクト(3xfloat)が12バイトから16バイトに拡張されることを懸念してグラフィックスエンジンをベンチマークした。16バイトを使うほうが8バイト読み取りに揃うため速いことがわかった。結果としてvec3はvec4のように扱われるようになった。常に全体的なベンチマークを行うことを勧める。
  • レジスタに事前ロードされた引数はスタック書き込みより高性能で、スタック操作はヒープ確保されたものよりさらに高速。これはグローバル変数が多い複雑なコードが高速に動き、エレガントな再帰関数やタプル/構造体/リスト引数が遅い理由でもある。前者は密なアセンブリループに最適化しやすい。
  • MSVCでは8バイトを超える構造体はスタックに渡される。これは移植可能なコードで依存すべきでないABIの詳細。ただし、頻繁に呼ばれない関数ならあまり気にしすぎる必要はなく、頻繁に呼ばれる小さな関数ならコンパイラがコードをインライン化できるようにして、レジスタで引数を渡す以上に有用な最適化を有効にするとよい。
  • Windowsでデフォルトのcdecl呼び出し規約を使う場合、8バイトより大きい構造体はレジスタ渡しされない。
  • amd64でsysv amd64 ABIを使って16バイトを超える構造体を値渡し・値返しするのは遅いが、コードを明確にするうえでしばしば価値がある。もちろんこのケースには当てはまらないが、たとえば各C++コンパイラ、Golang、OCaml、SBCLのように、独自言語内でカスタムABIを使うことはできる。
  • C++では、非プリミティブ型はよほどの理由がない限り参照(必要ならポインタ)で渡すべきだという経験則がある。これはABIのためでもあり、コピーコンストラクタやムーブコンストラクタを避けるためでもある。性能最適化を望むなら、C++で注意すべき退屈な低レベル詳細の1つ。
  • 記事は非常に特殊なベンチマークへのリンクを示しており、そこではJava(JIT)がC++より速く、Scalaよりも速い。Julia HOとは何で、なぜそんなに速いのか、PythonとPypyの速度差が大きいこと、Pypyを使わない理由があるのか、標準になるべきなのかといった疑問が出る。
  • 提示された例では、呼び出し側に影響を与えずに「struct Vector」パラメータ型を「const struct Vector &」参照渡しへ変更することで修正できる。ポインタのバグが存在する多くのC++コードは、ポインタを不必要に使っていたのであり、参照渡しのほうがより簡単かつ安全に使えたはず。