Go 1.17 で導入されたレジスタベースの関数呼び出し規約 (以下、レジスタベース ABI) は多くのケースでパフォーマンス向上をもたらしていますが、利用可能なレジスタ数には制約があるはずなので引数や戻り値がいくつまでならレジスタベース ABI の恩恵を受けられるのか調べました。
なお、この記事は Go コンパイラの内部ドキュメントをもとに作成しました。
内部ドキュメントではレジスタベース ABI の詳細や根拠について説明されています。
※ 特に明記されていない限り 2022 年 3 月 27 日時点の情報になります。
短い結論
引数や戻り値の個数に関係なく対応アーキテクチャであればレジスタベース ABI の恩恵を受けられます。
とはいえ整数レジスタ 9 個、浮動小数点レジスタ 12 個に収まる範囲にした方がよさそうです。
(引数や戻り値がいくつレジスタを使用するのかは型によって異なります)
利用可能なレジスタ数
利用可能なレジスタ数は CPU アーキテクチャによって異なりますが OS による違いはありません。
また、すべての CPU アーキテクチャに対応しているわけでもありません。
CPU アーキテクチャ | 整数 | 浮動小数点 | サポートバージョン |
---|---|---|---|
amd64 | 9 | 15 | 1.17 |
arm64 | 16 | 16 | 1.18 |
ppc64, ppc64le | 12 | 12 | 1.18 |
riscv64 | 16 | 16 | 1.19 (予定) |
amd64 で利用可能な整数レジスタの数が少ないように見えますが Kubernetes のコードをもとにした事前調査 1 では 9 個でも十分な効果があるようです。
レジスタの割り当て
引数や戻り値が単純な整数や浮動小数点としてあつかえない場合、次のようなルールでレジスタが割り当てられます。
- 構造体
- 構造体の場合は各フィールドに対して再帰的にレジスタの割り当てを試みます。
- 文字列 (
string
)- 文字列の場合はバイト列のポインタと長さを示す 2 つの整数レジスタが割り当てられます。
- インターフェース (
interface
)- インターフェースの場合は 2 つの整数レジスタが割り当てられます。
- スライス
- スライスの場合はポインタと長さ、キャパシティを示す 3 つの整数レジスタが割り当てられます。
- 配列
- 固定長配列は長さによって処理が異なります。
- 0: 何もしない。
- 1: 再帰的にレジスタの割り当てを試みます。
- 2 以上: インデックスアクセスが必要なのでレジスタへの割り当てを行わず、スタックを使用します。
- 固定長配列は長さによって処理が異なります。
- ポインタ、マップ、チャンネル、関数
- これらは 1 つの整数レジスタが割り当てられます。
レジスタは引数と戻り値で共有されており引数で使用されたレジスタが戻り値でも使用されます。
逆に複数の値を 1 個のレジスタに格納 (パック) してやり取りすることはありません。
レジスタの数が不足しており前述のルールで割り当てられない場合はスタックが使用されます。
逆に言うと引数がいくつであっても利用可能なレジスタ数の範囲でレジスタベース ABI の恩恵を受けられることになります。
コンパイラでの定義
利用可能なレジスタの定義は SSA コンパイラ用のコードジェネレータで定義されています。
amd64 の場合は次のコードで定義された ParamIntRegNames
と ParamFloatRegNames
が利用可能なレジスタの名前です。
上記の定義をもとに生成されたのが次のコードにある paramIntRegAMD64
と paramFloatRegAMD64
で registersAMD64
と対応しています。
また、利用可能なレジスタの数は次のコードで定数 IntArgRegs
と FloatArgRegs
として定義されています。