[インデックス 11793] ファイルの概要
このコミットは、Go言語の6gコンパイラ(AMD64アーキテクチャ向けのGoコンパイラ)における「レジスタ不足」のバグを修正するものです。具体的には、レジスタの再利用戦略を改善し、かつて特定の用途(m
とg
、すなわちM(マシン)とG(ゴルーチン)のポインタ)に予約されていたR14およびR15レジスタを解放することで、レジスタの枯渇を防ぎ、コンパイルエラーを解消します。
コミット
commit ca5da31f83517780893423e46665d48149e545ee
Author: Russ Cox <rsc@golang.org>
Date: Fri Feb 10 22:19:34 2012 -0500
6g: fix out of registers bug
Fix it twice: reuse registers more aggressively in cgen abop,
and also release R14 and R15, which are no longer m and g.
Fixes #2669.
R=ken2
CC=golang-dev
https://golang.org/cl/5655056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ca5da31f83517780893423e46665d48149e545ee
元コミット内容
このコミットは、以下の2つの主要な変更によってレジスタ不足のバグを修正しています。
cgen abop
(非対称二項演算のコード生成)において、レジスタの再利用をより積極的に行うように変更。- R14およびR15レジスタの予約を解除。これらのレジスタは、GoランタイムのM(マシン)とG(ゴルーチン)のポインタを保持するために使用されていましたが、このコミットの時点ではその役割を終えており、汎用レジスタとして利用可能になったため、解放されました。
この修正は、Go issue #2669 に対応するものです。
変更の背景
Goコンパイラ(特に6g、AMD64アーキテクチャ向け)において、特定のコードパターンがコンパイル時に「out of registers」(レジスタ不足)エラーを引き起こす問題が発生していました。これは、コンパイラがコードを機械語に変換する際に、利用可能なCPUレジスタを使い果たしてしまうために起こります。
この問題の根本原因は複数ありました。一つは、コンパイラのコード生成フェーズ、特に非対称二項演算(abop
)の処理において、レジスタの割り当てと解放が最適化されていなかったことです。もう一つは、Goランタイムの進化に伴い、かつてMとGのポインタを保持するために予約されていたR14とR15レジスタが、もはやその目的で使用されなくなっていたにもかかわらず、コンパイラがそれらを予約済みとして扱っていたことです。これにより、利用可能な汎用レジスタの数が不必要に制限され、レジスタ不足を悪化させていました。
このバグは、特に複雑な式やループ内で多くの変数が使用される場合に顕著に現れ、開発者がGoプログラムをコンパイルできないという深刻な問題を引き起こしていました。
前提知識の解説
1. CPUレジスタ
CPUレジスタは、CPU内部にある高速な記憶領域で、CPUが現在処理しているデータや命令のアドレスを一時的に保持するために使われます。レジスタの数は限られており、効率的なレジスタの使用はプログラムのパフォーマンスに直結します。コンパイラは、ソースコードの変数をこれらのレジスタに割り当てることで、高速なデータアクセスを実現します。レジスタが不足すると、コンパイラは変数をメモリに退避させる必要があり(スピル)、これはパフォーマンスの低下を招くだけでなく、場合によってはコンパイルエラーの原因にもなります。
2. Goコンパイラ (6g)
Go言語のコンパイラは、Goのソースコードを機械語に変換するツールです。6g
は、Go 1.x系の時代にAMD64(x86-64)アーキテクチャ向けのコンパイラを指す名称でした。Goのコンパイラは、フロントエンド(構文解析、型チェックなど)、ミドルエンド(中間表現の最適化)、バックエンド(コード生成、レジスタ割り当てなど)から構成されます。このコミットは、主にバックエンドのレジスタ割り当てとコード生成に関する問題に対処しています。
3. レジスタ割り当て (Register Allocation)
レジスタ割り当ては、コンパイラの重要な最適化フェーズの一つです。プログラム中の変数を、限られた数のCPUレジスタに効率的に割り当てることを目的とします。理想的には、すべての変数をレジスタに保持したいのですが、レジスタの数が足りない場合、一部の変数はメモリに退避されます。レジスタ割り当てのアルゴリズムは、レジスタの利用可能性、変数のライフタイム、使用頻度などを考慮して、最適な割り当てを決定します。
4. GoランタイムのMとG (Machine and Goroutine)
Goランタイムは、Goプログラムの実行を管理するシステムです。その中核には、M(Machine、OSスレッドを表す)とG(Goroutine、Goの軽量スレッド)という概念があります。かつて、Goランタイムは特定のCPUレジスタ(AMD64ではR14とR15)を、現在のMとGのポインタを高速に参照するために予約していました。これにより、コンテキストスイッチやスケジューリングの際にこれらの重要な情報に素早くアクセスできました。しかし、ランタイムの設計変更や最適化により、これらのレジスタを専用に予約する必要がなくなりました。
5. Go Issue #2669
GoのIssueトラッカーで報告されたバグは、Go開発における重要な情報源です。Issue #2669は、このコミットが修正しようとしている具体的なレジスタ不足のバグに関する報告です。通常、Issueにはバグの再現手順、期待される動作、実際の動作、そして議論が含まれており、開発者が問題の背景と影響を理解するのに役立ちます。
技術的詳細
このコミットの技術的詳細は、Goコンパイラのバックエンドにおけるレジスタ管理とコード生成のメカニズムに深く関わっています。
cgen abop
におけるレジスタ再利用の改善
cgen.c
ファイルの変更は、abop
(asymmetric binary operation、非対称二項演算)のコード生成ロジックに焦点を当てています。非対称二項演算とは、例えば a = b + c
のような演算で、オペランド(b
とc
)と結果(a
)が異なるレジスタに割り当てられる可能性がある場合を指します。
変更前のコードでは、regalloc
関数(レジスタを割り当てる関数)の呼び出しにおいて、左右のオペランド(nl
とnr
)に対してレジスタを割り当てる際に、res
(結果を格納するレジスタ)のヒントが適切に利用されていませんでした。
--- a/src/cmd/6g/cgen.c
+++ b/src/cmd/6g/cgen.c
@@ -387,9 +387,9 @@ abop: // asymmetric binary
regalloc(&n2, nr->type, N);\n
cgen(nr, &n2);\n
} else {\n
-\t\tregalloc(&n2, nr->type, N);\n
+\t\tregalloc(&n2, nr->type, res);\n
\t\tcgen(nr, &n2);\n
-\t\tregalloc(&n1, nl->type, res);\n
+\t\tregalloc(&n1, nl->type, N);\n
\t\tcgen(nl, &n1);\n
}\n
gins(a, &n2, &n1);\n
この変更により、右オペランド nr
のレジスタ割り当て時に、結果を格納するレジスタ res
をヒントとして渡すようになりました (regalloc(&n2, nr->type, res)
)。これにより、コンパイラは nr
の値を計算したレジスタを、そのまま結果 res
のレジスタとして再利用できる可能性が高まります。これは、レジスタのライフタイムを短くし、レジスタの枯渇を防ぐための一般的な最適化手法です。
R14およびR15レジスタの解放
gsubr.c
ファイルの変更は、Goランタイムの進化に伴うレジスタの役割変更を反映しています。
--- a/src/cmd/6g/gsubr.c
+++ b/src/cmd/6g/gsubr.c
@@ -287,8 +287,6 @@ static int resvd[] =\n D_CX,\t// for shift\n D_DX,\t// for divide\n D_SP,\t// for stack\n-\tD_R14,\t// reserved for m\n-\tD_R15,\t// reserved for u\n };\n```
この変更により、`resvd` 配列から `D_R14` と `D_R15` が削除されました。`resvd` は予約済みレジスタのリストであり、ここに記載されたレジスタは汎用レジスタ割り当ての対象外となります。これらを削除することで、R14とR15が汎用レジスタとして利用可能になり、コンパイラが使用できるレジスタの総数が増加します。
また、`regalloc` 関数内のエラーハンドリングも改善されています。以前は `yyerror` を呼び出してエラーメッセージを出力していましたが、この変更では `fatal` 関数を呼び出すことで、より明確にコンパイルを停止させるようになりました。さらに、レジスタ不足の場合に、どのレジスタがどこで割り当てられたかを示すデバッグ情報 (`regpc`) を出力するようになりました。これは、将来的なデバッグや問題特定に役立ちます。
```diff
--- a/src/cmd/6g/gsubr.c
+++ b/src/cmd/6g/gsubr.c
@@ -372,11 +372,15 @@ regalloc(Node *n, Type *t, Node *o)\n goto out;\n }\n for(i=D_AX; i<=D_R15; i++)\n-\t\t\tif(reg[i] == 0)\n+\t\t\tif(reg[i] == 0) {\n+\t\t\t\tregpc[i-D_AX] = (uintptr)getcallerpc(&n);\n \t\t\t\tgoto out;\n+\t\t\t}\n \n-\t\tyyerror(\"out of fixed registers\");\n-\t\tgoto err;\n+\t\tflusherrors();\n+\t\tfor(i=0; i+D_AX<=D_R15; i++)\n+\t\t\tprint(\"%d %p\\n\", i, regpc[i]);\n+\t\tfatal(\"out of fixed registers\");\n```
`regfree` 関数にも、解放されたレジスタの `regpc` をクリアするロジックが追加されています。
### テストケースの追加
`test/fixedbugs/bug410.go` は、このレジスタ不足のバグを再現するための新しいテストケースです。このテストケースは、複雑なループと配列操作を含むGoコードを含んでおり、以前のコンパイラではレジスタ不足エラーを引き起こしていました。このテストケースが追加されたことで、将来的に同様の回帰バグが発生しないことを保証できます。
特に注目すべきは、`x[i] ^= k[i-arr[s].num%0]` のような行です。`arr[s].num%0` はゼロ除算エラーを引き起こす可能性がありますが、これはコンパイラがこのコードを最適化しようとした際に、レジスタ割り当ての複雑さを増すための意図的な記述であると考えられます。このテストケースは、コンパイラが極端なケースでもレジスタを適切に管理できることを確認するために設計されています。
## コアとなるコードの変更箇所
### `src/cmd/6g/cgen.c`
```diff
--- a/src/cmd/6g/cgen.c
+++ b/src/cmd/6g/cgen.c
@@ -387,9 +387,9 @@ abop: // asymmetric binary
regalloc(&n2, nr->type, N);\n
cgen(nr, &n2);\n
} else {\n
-\t\tregalloc(&n2, nr->type, N);\n
+\t\tregalloc(&n2, nr->type, res);\n
\t\tcgen(nr, &n2);\n
-\t\tregalloc(&n1, nl->type, res);\n
+\t\tregalloc(&n1, nl->type, N);\n
\t\tcgen(nl, &n1);\n
}\n
gins(a, &n2, &n1);\n
src/cmd/6g/gsubr.c
--- a/src/cmd/6g/gsubr.c
+++ b/src/cmd/6g/gsubr.c
@@ -287,8 +287,6 @@ static int resvd[] =\n D_CX,\t// for shift\n D_DX,\t// for divide\n D_SP,\t// for stack\n-\tD_R14,\t// reserved for m\n-\tD_R15,\t// reserved for u\n };\n \n void\n@@ -340,6 +338,8 @@ anyregalloc(void)\n return 0;\n }\n \n+static\tuintptr\tregpc[D_R15+1 - D_AX];\n+\n /*\n * allocate register of type t, leave in n.\n * if o != N, o is desired fixed register.\n@@ -372,11 +372,15 @@ regalloc(Node *n, Type *t, Node *o)\n goto out;\n }\n for(i=D_AX; i<=D_R15; i++)\n-\t\t\tif(reg[i] == 0)\n+\t\t\tif(reg[i] == 0) {\n+\t\t\t\tregpc[i-D_AX] = (uintptr)getcallerpc(&n);\n \t\t\t\tgoto out;\n+\t\t\t}\n \n-\t\tyyerror(\"out of fixed registers\");\n-\t\tgoto err;\n+\t\tflusherrors();\n+\t\tfor(i=0; i+D_AX<=D_R15; i++)\n+\t\t\tprint(\"%d %p\\n\", i, regpc[i]);\n+\t\tfatal(\"out of fixed registers\");\n \n \tcase TFLOAT32:\n \tcase TFLOAT64:\n@@ -388,18 +392,14 @@ regalloc(Node *n, Type *t, Node *o)\n \t\tfor(i=D_X0; i<=D_X7; i++)\n \t\t\tif(reg[i] == 0)\n \t\t\t\tgoto out;\n-\t\tyyerror(\"out of floating registers\");\n-\t\tgoto err;\n+\t\tfatal(\"out of floating registers\");\n \n \tcase TCOMPLEX64:\n \tcase TCOMPLEX128:\n \t\ttempname(n, t);\n \t\treturn;\n \t}\n-\tyyerror(\"regalloc: unknown type %T\", t);\n-\n-err:\n-\tnodreg(n, t, 0);\n+\tfatal(\"regalloc: unknown type %T\", t);\n \treturn;\n \n out:\n@@ -424,6 +424,8 @@ regfree(Node *n)\n \tif(reg[i] <= 0)\n \t\tfatal(\"regfree: reg not allocated\");\n \treg[i]--;\n+\tif(reg[i] == 0 && D_AX <= i && i <= D_R15)\n+\t\tregpc[i - D_AX] = 0;\n }\n \n /*\n```
### `test/fixedbugs/bug410.go`
```diff
--- /dev/null
+++ b/test/fixedbugs/bug410.go
@@ -0,0 +1,24 @@\n+// $G $D/$F.go\n+\n+// Copyright 2012 The Go Authors. All rights reserved.\n+// Use of this source code is governed by a BSD-style\n+// license that can be found in the LICENSE file.\n+\n+// Used to run 6g out of registers. Issue 2669.\n+\n+package p\n+\n+type y struct {\n+\tnum int\n+}\n+\n+func zzz () {\n+ k := make([]byte, 10)\n+\tarr := make ([]*y, 0)\n+ for s := range arr {\n+ x := make([]byte, 10)\n+ for i := 0; i < 100 ; i++ {\n+ x[i] ^= k[i-arr[s].num%0]\n+ }\n+ }\n+}\n```
## コアとなるコードの解説
### `src/cmd/6g/cgen.c` の変更
この変更は、コンパイラのコード生成戦略におけるレジスタの「ヒント」の利用を改善するものです。`regalloc` 関数は、新しいレジスタを割り当てる際に、`o` パラメータとして「望ましい固定レジスタ」を受け取ることができます。
変更前:
`regalloc(&n2, nr->type, N);`
`regalloc(&n1, nl->type, res);`
変更後:
`regalloc(&n2, nr->type, res);`
`regalloc(&n1, nl->type, N);`
`N` は「固定レジスタなし」を意味します。変更前は、右オペランド `nr` のレジスタ割り当て時に `N` を指定していたため、`nr` の計算結果が `res` に直接格納される可能性が低く、余分なレジスタ移動や割り当てが発生する可能性がありました。
変更後は、`nr` のレジスタ割り当て時に `res` をヒントとして渡すことで、`nr` の計算結果が直接 `res` に割り当てられるように促します。これにより、レジスタの再利用が促進され、レジスタの消費が抑えられます。左オペランド `nl` の割り当ては、結果レジスタとは独立しているため、`N` のままです。
### `src/cmd/6g/gsubr.c` の変更
1. **`resvd` から `D_R14` と `D_R15` の削除**:
`resvd` 配列は、コンパイラがレジスタ割り当てを行う際に、汎用レジスタとして使用してはならない予約済みレジスタを定義しています。`D_R14` と `D_R15` は、かつてGoランタイムのMとGのポインタを保持するために予約されていましたが、ランタイムの変更によりその必要がなくなりました。これらを `resvd` から削除することで、R14とR15が汎用レジスタとして利用可能になり、コンパイラが利用できるレジスタプールが拡大します。これは、レジスタ不足の直接的な原因の一つを解消します。
2. **`regpc` 配列の追加とデバッグ情報の強化**:
`static uintptr regpc[D_R15+1 - D_AX];`
この配列は、各レジスタが最後に割り当てられた際の呼び出し元のPC(プログラムカウンタ)を記録するために追加されました。レジスタが割り当てられる際に `regpc[i-D_AX] = (uintptr)getcallerpc(&n);` でPCが記録され、レジスタが解放される際に `regpc[i - D_AX] = 0;` でクリアされます。
レジスタ不足エラーが発生した場合、`flusherrors()` の後に `print("%d %p\\n", i, regpc[i]);` を使って、どのレジスタがどこで割り当てられたかを出力するようになりました。これにより、レジスタ不足の原因を特定するためのデバッグ情報が格段に向上します。
3. **エラーハンドリングの変更**:
以前は `yyerror` を使用していましたが、`fatal` 関数に変更されました。`yyerror` はエラーメッセージを出力するだけでコンパイルを続行しようとしますが、`fatal` は致命的なエラーとしてコンパイルを即座に停止させます。これにより、レジスタ不足のような深刻な問題が発生した場合に、不完全なコード生成を防ぎ、問題の早期発見を促します。
### `test/fixedbugs/bug410.go` の追加
このテストファイルは、レジスタ不足のバグを再現するために特別に作成されました。`zzz` 関数内のネストされたループと、`x[i] ^= k[i-arr[s].num%0]` のような複雑な式は、コンパイラが多くのレジスタを必要とする状況を作り出します。特に `arr[s].num%0` のようなゼロ除算は、コンパイラがこの式を評価する際に、レジスタ割り当ての複雑性を意図的に高めるためのものです。このテストケースが追加されたことで、このコミットによる修正が正しく機能していることを検証し、将来的な回帰を防ぐことができます。
## 関連リンク
* Go issue #2669: [https://github.com/golang/go/issues/2669](https://github.com/golang/go/issues/2669)
* Go CL 5655056: [https://golang.org/cl/5655056](https://golang.org/cl/5655056) (Goのコードレビューシステムにおける変更リスト)
## 参考にした情報源リンク
* Go issue #2669 の内容
* Go言語のコンパイラに関する一般的な知識
* CPUアーキテクチャ(AMD64)におけるレジスタの役割
* コンパイラのレジスタ割り当てに関する一般的な概念
* GoランタイムのMとGのスケジューリングモデルに関する情報
* `git diff` コマンドによるコードの変更点の分析
* Go言語のソースコード(`src/cmd/6g/cgen.c`, `src/cmd/6g/gsubr.c`)の構造と関数
* Go言語のテストフレームワークに関する知識
# [インデックス 11793] ファイルの概要
このコミットは、Go言語の6gコンパイラ(AMD64アーキテクチャ向けのGoコンパイラ)における「レジスタ不足」のバグを修正するものです。具体的には、レジスタの再利用戦略を改善し、かつて特定の用途(`m`と`g`、すなわちM(マシン)とG(ゴルーチン)のポインタ)に予約されていたR14およびR15レジスタを解放することで、レジスタの枯渇を防ぎ、コンパイルエラーを解消します。
## コミット
commit ca5da31f83517780893423e46665d48149e545ee Author: Russ Cox rsc@golang.org Date: Fri Feb 10 22:19:34 2012 -0500
6g: fix out of registers bug
Fix it twice: reuse registers more aggressively in cgen abop,
and also release R14 and R15, which are no longer m and g.
Fixes #2669.
R=ken2
CC=golang-dev
https://golang.org/cl/5655056
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/ca5da31f83517780893423e46665d48149e545ee](https://github.com/golang/go/commit/ca5da31f83517780893423e46665d48149e545ee)
## 元コミット内容
このコミットは、以下の2つの主要な変更によってレジスタ不足のバグを修正しています。
1. `cgen abop`(非対称二項演算のコード生成)において、レジスタの再利用をより積極的に行うように変更。
2. R14およびR15レジスタの予約を解除。これらのレジスタは、GoランタイムのM(マシン)とG(ゴルーチン)のポインタを保持するために使用されていましたが、このコミットの時点ではその役割を終えており、汎用レジスタとして利用可能になったため、解放されました。
この修正は、Go issue #2669 に対応するものです。
## 変更の背景
Goコンパイラ(特に6g、AMD64アーキテクチャ向け)において、特定のコードパターンがコンパイル時に「out of registers」(レジスタ不足)エラーを引き起こす問題が発生していました。これは、コンパイラがコードを機械語に変換する際に、利用可能なCPUレジスタを使い果たしてしまうために起こります。
この問題の根本原因は複数ありました。一つは、コンパイラのコード生成フェーズ、特に非対称二項演算(`abop`)の処理において、レジスタの割り当てと解放が最適化されていなかったことです。もう一つは、Goランタイムの進化に伴い、かつてMとGのポインタを保持するために予約されていたR14とR15レジスタが、もはやその目的で使用されなくなっていたにもかかわらず、コンパイラがそれらを予約済みとして扱っていたことです。これにより、利用可能な汎用レジスタの数が不必要に制限され、レジスタ不足を悪化させていました。
このバグは、特に複雑な式やループ内で多くの変数が使用される場合に顕著に現れ、開発者がGoプログラムをコンパイルできないという深刻な問題を引き起こしていました。
## 前提知識の解説
### 1. CPUレジスタ
CPUレジスタは、CPU内部にある高速な記憶領域で、CPUが現在処理しているデータや命令のアドレスを一時的に保持するために使われます。レジスタの数は限られており、効率的なレジスタの使用はプログラムのパフォーマンスに直結します。コンパイラは、ソースコードの変数をこれらのレジスタに割り当てることで、高速なデータアクセスを実現します。レジスタが不足すると、コンパイラは変数をメモリに退避させる必要があり(スピル)、これはパフォーマンスの低下を招くだけでなく、場合によってはコンパイルエラーの原因にもなります。
### 2. Goコンパイラ (6g)
Go言語のコンパイラは、Goのソースコードを機械語に変換するツールです。`6g`は、Go 1.x系の時代にAMD64(x86-64)アーキテクチャ向けのコンパイラを指す名称でした。Goのコンパイラは、フロントエンド(構文解析、型チェックなど)、ミドルエンド(中間表現の最適化)、バックエンド(コード生成、レジスタ割り当てなど)から構成されます。このコミットは、主にバックエンドのレジスタ割り当てとコード生成に関する問題に対処しています。
### 3. レジスタ割り当て (Register Allocation)
レジスタ割り当ては、コンパイラの重要な最適化フェーズの一つです。プログラム中の変数を、限られた数のCPUレジスタに効率的に割り当てることを目的とします。理想的には、すべての変数をレジスタに保持したいのですが、レジスタの数が足りない場合、一部の変数はメモリに退避されます。レジスタ割り当てのアルゴリズムは、レジスタの利用可能性、変数のライフタイム、使用頻度などを考慮して、最適な割り当てを決定します。
### 4. GoランタイムのMとG (Machine and Goroutine)
Goランタイムは、Goプログラムの実行を管理するシステムです。その中核には、M(Machine、OSスレッドを表す)とG(Goroutine、Goの軽量スレッド)という概念があります。かつて、Goランタイムは特定のCPUレジスタ(AMD64ではR14とR15)を、現在のMとGのポインタを高速に参照するために予約していました。これにより、コンテキストスイッチやスケジューリングの際にこれらの重要な情報に素早くアクセスできました。しかし、ランタイムの設計変更や最適化により、これらのレジスタを専用に予約する必要がなくなりました。
### 5. Go Issue #2669
GoのIssueトラッカーで報告されたバグは、Go開発における重要な情報源です。コミットメッセージに記載されている `Fixes #2669` は、Goの公式リポジトリのIssueトラッカーにおける特定のバグ報告を指しています。通常、Issueにはバグの再現手順、期待される動作、実際の動作、そして議論が含まれており、開発者が問題の背景と影響を理解するのに役立ちます。
## 技術的詳細
このコミットの技術的詳細は、Goコンパイラのバックエンドにおけるレジスタ管理とコード生成のメカニズムに深く関わっています。
### `cgen abop` におけるレジスタ再利用の改善
`cgen.c` ファイルの変更は、`abop`(asymmetric binary operation、非対称二項演算)のコード生成ロジックに焦点を当てています。非対称二項演算とは、例えば `a = b + c` のような演算で、オペランド(`b`と`c`)と結果(`a`)が異なるレジスタに割り当てられる可能性がある場合を指します。
変更前のコードでは、`regalloc` 関数(レジスタを割り当てる関数)の呼び出しにおいて、左右のオペランド(`nl`と`nr`)に対してレジスタを割り当てる際に、`res`(結果を格納するレジスタ)のヒントが適切に利用されていませんでした。
```diff
--- a/src/cmd/6g/cgen.c
+++ b/src/cmd/6g/cgen.c
@@ -387,9 +387,9 @@ abop: // asymmetric binary
regalloc(&n2, nr->type, N);\n
cgen(nr, &n2);\n
} else {\n
-\t\tregalloc(&n2, nr->type, N);\n
+\t\tregalloc(&n2, nr->type, res);\n
\t\tcgen(nr, &n2);\n
-\t\tregalloc(&n1, nl->type, res);\n
+\t\tregalloc(&n1, nl->type, N);\n
\t\tcgen(nl, &n1);\n
}\n
gins(a, &n2, &n1);\n
この変更により、右オペランド nr
のレジスタ割り当て時に、結果を格納するレジスタ res
をヒントとして渡すようになりました (regalloc(&n2, nr->type, res)
)。これにより、コンパイラは nr
の値を計算したレジスタを、そのまま結果 res
のレジスタとして再利用できる可能性が高まります。これは、レジスタのライフタイムを短くし、レジスタの枯渇を防ぐための一般的な最適化手法です。
R14およびR15レジスタの解放
gsubr.c
ファイルの変更は、Goランタイムの進化に伴うレジスタの役割変更を反映しています。
--- a/src/cmd/6g/gsubr.c
+++ b/src/cmd/6g/gsubr.c
@@ -287,8 +287,6 @@ static int resvd[] =\n D_CX,\t// for shift\n D_DX,\t// for divide\n D_SP,\t// for stack\n-\tD_R14,\t// reserved for m\n-\tD_R15,\t// reserved for u\n };\n```
この変更により、`resvd` 配列から `D_R14` と `D_R15` が削除されました。`resvd` は予約済みレジスタのリストであり、ここに記載されたレジスタは汎用レジスタ割り当ての対象外となります。これらを削除することで、R14とR15が汎用レジスタとして利用可能になり、コンパイラが使用できるレジスタの総数が増加します。
また、`regalloc` 関数内のエラーハンドリングも改善されています。以前は `yyerror` を呼び出してエラーメッセージを出力していましたが、この変更では `fatal` 関数を呼び出すことで、より明確にコンパイルを停止させるようになりました。さらに、レジスタ不足の場合に、どのレジスタがどこで割り当てられたかを示すデバッグ情報 (`regpc`) を出力するようになりました。これは、将来的なデバッグや問題特定に役立ちます。
```diff
--- a/src/cmd/6g/gsubr.c
+++ b/src/cmd/6g/gsubr.c
@@ -372,11 +372,15 @@ regalloc(Node *n, Type *t, Node *o)\n goto out;\n }\n for(i=D_AX; i<=D_R15; i++)\n-\t\t\tif(reg[i] == 0)\n+\t\t\tif(reg[i] == 0) {\n+\t\t\t\tregpc[i-D_AX] = (uintptr)getcallerpc(&n);\n \t\t\t\tgoto out;\n+\t\t\t}\n \n-\t\tyyerror(\"out of fixed registers\");\n-\t\tgoto err;\n+\t\tflusherrors();\n+\t\tfor(i=0; i+D_AX<=D_R15; i++)\n+\t\t\tprint(\"%d %p\\n\", i, regpc[i]);\n+\t\tfatal(\"out of fixed registers\");\n```
`regfree` 関数にも、解放されたレジスタの `regpc` をクリアするロジックが追加されています。
### テストケースの追加
`test/fixedbugs/bug410.go` は、このレジスタ不足のバグを再現するための新しいテストケースです。このテストケースは、複雑なループと配列操作を含むGoコードを含んでおり、以前のコンパイラではレジスタ不足エラーを引き起こしていました。このテストケースが追加されたことで、将来的に同様の回帰バグが発生しないことを保証できます。
特に注目すべきは、`x[i] ^= k[i-arr[s].num%0]` のような行です。`arr[s].num%0` はゼロ除算エラーを引き起こす可能性がありますが、これはコンパイラがこのコードを最適化しようとした際に、レジスタ割り当ての複雑さを増すための意図的な記述であると考えられます。このテストケースは、コンパイラが極端なケースでもレジスタを適切に管理できることを確認するために設計されています。
## コアとなるコードの変更箇所
### `src/cmd/6g/cgen.c`
```diff
--- a/src/cmd/6g/cgen.c
+++ b/src/cmd/6g/cgen.c
@@ -387,9 +387,9 @@ abop: // asymmetric binary
regalloc(&n2, nr->type, N);\n
cgen(nr, &n2);\n
} else {\n
-\t\tregalloc(&n2, nr->type, N);\n
+\t\tregalloc(&n2, nr->type, res);\n
\t\tcgen(nr, &n2);\n
-\t\tregalloc(&n1, nl->type, res);\n
+\t\tregalloc(&n1, nl->type, N);\n
\t\tcgen(nl, &n1);\n
}\n
gins(a, &n2, &n1);\n
src/cmd/6g/gsubr.c
--- a/src/cmd/6g/gsubr.c
+++ b/src/cmd/6g/gsubr.c
@@ -287,8 +287,6 @@ static int resvd[] =\n D_CX,\t// for shift\n D_DX,\t// for divide\n D_SP,\t// for stack\n-\tD_R14,\t// reserved for m\n-\tD_R15,\t// reserved for u\n };\n \n void\n@@ -340,6 +338,8 @@ anyregalloc(void)\n return 0;\n }\n \n+static uintptr regpc[D_R15+1 - D_AX];\n+\n /*\n * allocate register of type t, leave in n.\n * if o != N, o is desired fixed register.\n@@ -372,11 +372,15 @@ regalloc(Node *n, Type *t, Node *o)\n goto out;\n }\n for(i=D_AX; i<=D_R15; i++)\n-\t\t\tif(reg[i] == 0)\n+\t\t\t\tif(reg[i] == 0) {\n+\t\t\t\tregpc[i-D_AX] = (uintptr)getcallerpc(&n);\n \t\t\t\tgoto out;\n+\t\t\t}\n \n-\t\tyyerror(\"out of fixed registers\");\n-\t\tgoto err;\n+\t\tflusherrors();\n+\t\tfor(i=0; i+D_AX<=D_R15; i++)\n+\t\t\tprint(\"%d %p\\n\", i, regpc[i]);\n+\t\tfatal(\"out of fixed registers\");\n \n \tcase TFLOAT32:\n \tcase TFLOAT64:\n@@ -388,18 +392,14 @@ regalloc(Node *n, Type *t, Node *o)\n \t\tfor(i=D_X0; i<=D_X7; i++)\n \t\t\tif(reg[i] == 0)\n \t\t\t\tgoto out;\n-\t\tyyerror(\"out of floating registers\");\n-\t\tgoto err;\n+\t\tfatal(\"out of floating registers\");\n \n \tcase TCOMPLEX64:\n \tcase TCOMPLEX128:\n \t\ttempname(n, t);\n \t\treturn;\n \t}\n-\tyyerror(\"regalloc: unknown type %T\", t);\n-\n-err:\n-\tnodreg(n, t, 0);\n+\tfatal(\"regalloc: unknown type %T\", t);\n \treturn;\n \n out:\n@@ -424,6 +424,8 @@ regfree(Node *n)\n \tif(reg[i] <= 0)\n \t\tfatal(\"regfree: reg not allocated\");\n \treg[i]--;\n+\tif(reg[i] == 0 && D_AX <= i && i <= D_R15)\n+\t\tregpc[i - D_AX] = 0;\n }\n \n /*\n```
### `test/fixedbugs/bug410.go`
```diff
--- /dev/null
+++ b/test/fixedbugs/bug410.go
@@ -0,0 +1,24 @@\n+// $G $D/$F.go\n+\n+// Copyright 2012 The Go Authors. All rights reserved.\n+// Use of this source code is governed by a BSD-style\n+// license that can be found in the LICENSE file.\n+\n+// Used to run 6g out of registers. Issue 2669.\n+\n+package p\n+\n+type y struct {\n+\tnum int\n+}\n+\n+func zzz () {\n+ k := make([]byte, 10)\n+\tarr := make ([]*y, 0)\n+ for s := range arr {\n+ x := make([]byte, 10)\n+ for i := 0; i < 100 ; i++ {\n+ x[i] ^= k[i-arr[s].num%0]\n+ }\n+ }\n+}\n```
## コアとなるコードの解説
### `src/cmd/6g/cgen.c` の変更
この変更は、コンパイラのコード生成戦略におけるレジスタの「ヒント」の利用を改善するものです。`regalloc` 関数は、新しいレジスタを割り当てる際に、`o` パラメータとして「望ましい固定レジスタ」を受け取ることができます。
変更前:
`regalloc(&n2, nr->type, N);`
`regalloc(&n1, nl->type, res);`
変更後:
`regalloc(&n2, nr->type, res);`
`regalloc(&n1, nl->type, N);`
`N` は「固定レジスタなし」を意味します。変更前は、右オペランド `nr` のレジスタ割り当て時に `N` を指定していたため、`nr` の計算結果が `res` に直接格納される可能性が低く、余分なレジスタ移動や割り当てが発生する可能性がありました。
変更後は、`nr` のレジスタ割り当て時に `res` をヒントとして渡すことで、`nr` の計算結果が直接 `res` に割り当てられるように促します。これにより、レジスタの再利用が促進され、レジスタの消費が抑えられます。左オペランド `nl` の割り当ては、結果レジスタとは独立しているため、`N` のままです。
### `src/cmd/6g/gsubr.c` の変更
1. **`resvd` から `D_R14` と `D_R15` の削除**:
`resvd` 配列は、コンパイラがレジスタ割り当てを行う際に、汎用レジスタとして使用してはならない予約済みレジスタを定義しています。`D_R14` と `D_R15` は、かつてGoランタイムのMとGのポインタを保持するために予約されていましたが、ランタイムの変更によりその必要がなくなりました。これらを `resvd` から削除することで、R14とR15が汎用レジスタとして利用可能になり、コンパイラが利用できるレジスタプールが拡大します。これは、レジスタ不足の直接的な原因の一つを解消します。
2. **`regpc` 配列の追加とデバッグ情報の強化**:
`static uintptr regpc[D_R15+1 - D_AX];`
この配列は、各レジスタが最後に割り当てられた際の呼び出し元のPC(プログラムカウンタ)を記録するために追加されました。レジスタが割り当てられる際に `regpc[i-D_AX] = (uintptr)getcallerpc(&n);` でPCが記録され、レジスタが解放される際に `regpc[i - D_AX] = 0;` でクリアされます。
レジスタ不足エラーが発生した場合、`flusherrors()` の後に `print("%d %p\\n", i, regpc[i]);` を使って、どのレジスタがどこで割り当てられたかを出力するようになりました。これにより、レジスタ不足の原因を特定するためのデバッグ情報が格段に向上します。
3. **エラーハンドリングの変更**:
以前は `yyerror` を使用していましたが、`fatal` 関数に変更されました。`yyerror` はエラーメッセージを出力するだけでコンパイルを続行しようとしますが、`fatal` は致命的なエラーとしてコンパイルを即座に停止させます。これにより、レジスタ不足のような深刻な問題が発生した場合に、不完全なコード生成を防ぎ、問題の早期発見を促します。
## 関連リンク
* Go issue #2669: [https://github.com/golang/go/issues/2669](https://github.com/golang/go/issues/2669)
* Go CL 5655056: [https://golang.org/cl/5655056](https://golang.org/cl/5655056) (Goのコードレビューシステムにおける変更リスト)
## 参考にした情報源リンク
* Go issue #2669 の内容
* Go言語のコンパイラに関する一般的な知識
* CPUアーキテクチャ(AMD64)におけるレジスタの役割
* コンパイラのレジスタ割り当てに関する一般的な概念
* GoランタイムのMとGのスケジューリングモデルに関する情報
* `git diff` コマンドによるコードの変更点の分析
* Go言語のソースコード(`src/cmd/6g/cgen.c`, `src/cmd/6g/gsubr.c`)の構造と関数
* Go言語のテストフレームワークに関する知識