[インデックス 15160] ファイルの概要
このコミットは、Goコンパイラのcmd/8g
(386アーキテクチャ向けコンパイラ)におけるレジスタリークのバグを修正するものです。具体的には、シフト操作のコード生成時にsplitclean
の呼び出しが不足していた問題を解決し、レジスタの適切な解放を保証します。
コミット
commit 7594440ef134ddebc2864d207815eb325adda13f
Author: Russ Cox <rsc@golang.org>
Date: Thu Feb 7 17:55:25 2013 -0500
cmd/8g: add a few missing splitclean
Fixes #887.
R=ken2
CC=golang-dev
https://golang.org/cl/7303061
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7594440ef134ddebc2864d207815eb325adda13f
元コミット内容
cmd/8g: add a few missing splitclean
このコミットは、cmd/8g
コンパイラにおいて、いくつかのsplitclean
呼び出しが不足していた箇所を追加するものです。これにより、Go issue #887で報告された問題が修正されます。
変更の背景
この変更は、Go言語のIssue #887「8g
leaks registers on shift operations」を修正するために行われました。この問題は、386アーキテクチャ向けのGoコンパイラ(8g
)が、特定のシフト操作(<<
や>>
)のコードを生成する際に、一時的に使用したレジスタを適切に解放しないために発生するレジスタリークでした。
レジスタリークが発生すると、コンパイラが利用可能なレジスタを使い果たし、結果としてコード生成の効率が低下したり、最悪の場合、コンパイルエラーや不正なコードが生成される可能性がありました。特に、uint64
のような64ビット値を扱うシフト操作では、32ビットアーキテクチャ上で複数のレジスタを組み合わせて64ビット値を表現するため、レジスタ管理がより複雑になります。この複雑さの中で、splitclean
の呼び出し忘れがレジスタリークを引き起こしていました。
前提知識の解説
- Goコンパイラ (
cmd/8g
): Go言語の初期のコンパイラ群は、ターゲットアーキテクチャごとに命名されていました。8g
はIntel 386(32ビット)アーキテクチャ向けのGoコンパイラを指します。Goのコンパイラは、ソースコードを中間表現に変換し、最終的にターゲットアーキテクチャの機械語に変換する役割を担います。 - レジスタ (Registers): CPU内部にある高速な記憶領域で、プログラムの実行中にデータやアドレスを一時的に保持するために使用されます。レジスタの数は限られており、効率的なプログラム実行のためには、レジスタを適切に割り当て、使用後に解放する「レジスタ割り当て (Register Allocation)」が非常に重要です。
- レジスタリーク (Register Leak): プログラム(この場合はコンパイラ)が、一時的に使用したレジスタを解放し忘れることで、そのレジスタが不必要に占有され続ける状態を指します。これにより、利用可能なレジスタが減少し、コンパイラがより低速なメモリ操作に頼らざるを得なくなったり、レジスタ不足に陥る可能性があります。
splitclean
: Goコンパイラのバックエンドにおける内部関数の一つで、主に64ビット値を32ビットアーキテクチャ上で扱う際に、その値を構成する上位32ビットと下位32ビット(hi
とlo
)を一時的に保持するために割り当てられたレジスタを解放・クリーンアップする役割を担います。Goコンパイラでは、split64
関数が64ビット値をlo
とhi
の2つの32ビット値に分割し、これらをレジスタに割り当てます。splitclean
は、これらのレジスタが不要になったときに、それらをレジスタプールに戻すためのメカニズムです。
技術的詳細
Goコンパイラ(特に8g
)は、64ビット整数型(uint64
など)を32ビットアーキテクチャ上で処理する際に、その値を2つの32ビット部分(上位と下位)に分割して扱います。この分割処理はsplit64
関数によって行われ、分割された各部分は一時的にレジスタに割り当てられます。
問題となっていたのは、シフト操作(cgen_shift
関数内で処理される)のコード生成パスにおいて、このsplit64
によって割り当てられた一時レジスタが、操作完了後に適切にsplitclean
によって解放されていなかった点です。特に、シフト量が64ビット値で表現される場合や、シフト量が定数でない場合に、このレジスタリークが発生していました。
splitclean
は、コンパイラのレジスタ割り当てシステムの一部として機能し、一時レジスタのライフサイクルを管理します。splitclean
が呼び出されないと、これらのレジスタは「使用中」とマークされたままになり、他の目的で再利用できなくなります。これが繰り返されると、コンパイラが利用できるレジスタが枯渇し、レジスタ割り当ての失敗や、よりコストの高いメモリへのスピル(レジスタの内容をメモリに退避させること)が発生し、コンパイルされたコードの性能低下につながります。
このコミットでは、cgen_shift
関数内の特定のコードパス、特にシフト量を表すノード(nr
)が64ビット幅を持つ場合や、シフト量が定数でない場合に、splitclean()
の呼び出しを追加しています。これにより、シフト操作のために一時的に使用されたレジスタが、操作が完了した直後にシステムに返却され、レジスタリークが防止されます。
また、gsubr.c
におけるsplit64
関数の変更は、sclean
配列(splitclean
が管理するレジスタクリーンアップリスト)への追加ロジックの順序を修正するものです。これは、sclean
配列がオーバーフローする可能性のあるエッジケースを修正し、splitclean
メカニズム自体の堅牢性を高めるためのものです。
コアとなるコードの変更箇所
-
src/cmd/8g/ggen.c
:cgen_shift
関数内で、split64(&nt, &lo, &hi);
の直後にsplitclean();
が追加されました。cgen_shift
関数内で、p1 = gbranch(optoas(OLT, types[TUINT32]), T, +1);
の直後にsplitclean();
が追加されました。
-
src/cmd/8g/gsubr.c
:split64
関数内で、if(nsclean >= nelem(sclean))
のチェックの前にあったsclean[nsclean].op = OEMPTY;
の行が、チェックの後に移動されました。
-
test/fixedbugs/issue887.go
:- レジスタリークを再現する新しいテストケースが追加されました。このテストは、
byte
型の変数とuint64
型のシフト量を用いたシフト操作をswitch
文で複数回実行することで、問題が顕在化するように設計されています。
- レジスタリークを再現する新しいテストケースが追加されました。このテストは、
コアとなるコードの解説
-
src/cmd/8g/ggen.c
における変更:cgen_shift
関数は、Goのシフト演算子(<<
,>>
)のコードを生成する役割を担っています。この関数内で、シフト量(nr
)が64ビット型である場合や、シフト量が動的に決定される場合に、split64
関数が呼び出され、64ビットのシフト量を2つの32ビットレジスタに分割して扱います。 追加されたsplitclean();
の呼び出しは、これらの分割されたシフト量が一時的に使用された直後に、それらを保持していたレジスタを解放することを保証します。これにより、レジスタが不必要に占有され続けることがなくなり、レジスタリークが解消されます。 -
src/cmd/8g/gsubr.c
における変更:split64
関数は、64ビットのノードを2つの32ビットのノード(lo
とhi
)に分割し、それらをレジスタに割り当てます。sclean
配列は、splitclean
が後でクリーンアップする必要があるノードを追跡するために使用されます。 元のコードでは、sclean[nsclean].op = OEMPTY;
がnsclean
の境界チェックの前に実行されていました。これは、nsclean
が配列の最大値に達している場合に、配列の範囲外アクセスを引き起こす可能性がありました。変更後、この代入は境界チェックの後に移動され、sclean
配列へのアクセスが常に安全であることを保証します。これは、splitclean
メカニズム全体の堅牢性を高めるための重要な修正です。 -
test/fixedbugs/issue887.go
の追加: このテストファイルは、8g
コンパイラがレジスタリークを起こしていた具体的なシナリオを再現します。f(x byte, y uint64)
関数内で、y
(uint64
型)をシフト量としてx
をシフトする操作をswitch
文で複数回繰り返しています。このようなパターンは、コンパイラがレジスタを繰り返し割り当て、解放し忘れることで、レジスタリークが顕在化する典型的なケースです。このテストの追加により、将来的に同様の回帰バグが発生しないことを保証します。
これらの変更により、8g
コンパイラはシフト操作においてレジスタを適切に管理できるようになり、コンパイルの安定性と効率が向上しました。
関連リンク
- Go Issue #887: https://github.com/golang/go/issues/887
- Go CL 7303061: https://golang.org/cl/7303061
参考にした情報源リンク
- Go言語のソースコード(
src/cmd/8g/ggen.c
,src/cmd/8g/gsubr.c
) - Go Issue Tracker: https://github.com/golang/go/issues
- Go Code Review: https://go-review.googlesource.com/
- Go compiler internals (general knowledge)# [インデックス 15160] ファイルの概要
このコミットは、Goコンパイラのcmd/8g
(386アーキテクチャ向けコンパイラ)におけるレジスタリークのバグを修正するものです。具体的には、シフト操作のコード生成時にsplitclean
の呼び出しが不足していた問題を解決し、レジスタの適切な解放を保証します。
コミット
commit 7594440ef134ddebc2864d207815eb325adda13f
Author: Russ Cox <rsc@golang.org>
Date: Thu Feb 7 17:55:25 2013 -0500
cmd/8g: add a few missing splitclean
Fixes #887.
R=ken2
CC=golang-dev
https://golang.org/cl/7303061
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7594440ef134ddebc2864d207815eb325adda13f
元コミット内容
cmd/8g: add a few missing splitclean
このコミットは、cmd/8g
コンパイラにおいて、いくつかのsplitclean
呼び出しが不足していた箇所を追加するものです。これにより、Go issue #887で報告された問題が修正されます。
変更の背景
この変更は、Go言語のIssue #887「8g
leaks registers on shift operations」を修正するために行われました。この問題は、386アーキテクチャ向けのGoコンパイラ(8g
)が、特定のシフト操作(<<
や>>
)のコードを生成する際に、一時的に使用したレジスタを適切に解放しないために発生するレジスタリークでした。
レジスタリークが発生すると、コンパイラが利用可能なレジスタを使い果たし、結果としてコード生成の効率が低下したり、最悪の場合、コンパイルエラーや不正なコードが生成される可能性がありました。特に、uint64
のような64ビット値を扱うシフト操作では、32ビットアーキテクチャ上で複数のレジスタを組み合わせて64ビット値を表現するため、レジスタ管理がより複雑になります。この複雑さの中で、splitclean
の呼び出し忘れがレジスタリークを引き起こしていました。
前提知識の解説
- Goコンパイラ (
cmd/8g
): Go言語の初期のコンパイラ群は、ターゲットアーキテクチャごとに命名されていました。8g
はIntel 386(32ビット)アーキテクチャ向けのGoコンパイラを指します。Goのコンパイラは、ソースコードを中間表現に変換し、最終的にターゲットアーキテクチャの機械語に変換する役割を担います。 - レジスタ (Registers): CPU内部にある高速な記憶領域で、プログラムの実行中にデータやアドレスを一時的に保持するために使用されます。レジスタの数は限られており、効率的なプログラム実行のためには、レジスタを適切に割り当て、使用後に解放する「レジスタ割り当て (Register Allocation)」が非常に重要です。
- レジスタリーク (Register Leak): プログラム(この場合はコンパイラ)が、一時的に使用したレジスタを解放し忘れることで、そのレジスタが不必要に占有され続ける状態を指します。これにより、利用可能なレジスタが減少し、コンパイラがより低速なメモリ操作に頼らざるを得なくなったり、レジスタ不足に陥る可能性があります。
splitclean
: Goコンパイラのバックエンドにおける内部関数の一つで、主に64ビット値を32ビットアーキテクチャ上で扱う際に、その値を構成する上位32ビットと下位32ビット(hi
とlo
)を一時的に保持するために割り当てられたレジスタを解放・クリーンアップする役割を担います。Goコンパイラでは、split64
関数が64ビット値をlo
とhi
の2つの32ビット値に分割し、これらをレジスタに割り当てます。splitclean
は、これらのレジスタが不要になったときに、それらをレジスタプールに戻すためのメカニズムです。
技術的詳細
Goコンパイラ(特に8g
)は、64ビット整数型(uint64
など)を32ビットアーキテクチャ上で処理する際に、その値を2つの32ビット部分(上位と下位)に分割して扱います。この分割処理はsplit64
関数によって行われ、分割された各部分は一時的にレジスタに割り当てられます。
問題となっていたのは、シフト操作(cgen_shift
関数内で処理される)のコード生成パスにおいて、このsplit64
によって割り当てられた一時レジスタが、操作完了後に適切にsplitclean
によって解放されていなかった点です。特に、シフト量が64ビット値で表現される場合や、シフト量が定数でない場合に、このレジスタリークが発生していました。
splitclean
は、コンパイラのレジスタ割り当てシステムの一部として機能し、一時レジスタのライフサイクルを管理します。splitclean
が呼び出されないと、これらのレジスタは「使用中」とマークされたままになり、他の目的で再利用できなくなります。これが繰り返されると、コンパイラが利用できるレジスタが枯渇し、レジスタ割り当ての失敗や、よりコストの高いメモリへのスピル(レジスタの内容をメモリに退避させること)が発生し、コンパイルされたコードの性能低下につながります。
このコミットでは、cgen_shift
関数内の特定のコードパス、特にシフト量を表すノード(nr
)が64ビット幅を持つ場合や、シフト量が定数でない場合に、splitclean()
の呼び出しを追加しています。これにより、シフト操作のために一時的に使用されたレジスタが、操作が完了した直後にシステムに返却され、レジスタリークが防止されます。
また、gsubr.c
におけるsplit64
関数の変更は、sclean
配列(splitclean
が管理するレジスタクリーンアップリスト)への追加ロジックの順序を修正するものです。これは、sclean
配列がオーバーフローする可能性のあるエッジケースを修正し、splitclean
メカニズム自体の堅牢性を高めるためのものです。
コアとなるコードの変更箇所
-
src/cmd/8g/ggen.c
:cgen_shift
関数内で、split64(&nt, &lo, &hi);
の直後にsplitclean();
が追加されました。cgen_shift
関数内で、p1 = gbranch(optoas(OLT, types[TUINT32]), T, +1);
の直後にsplitclean();
が追加されました。
-
src/cmd/8g/gsubr.c
:split64
関数内で、if(!is64(n->type))
のチェックの後にあったsclean[nsclean].op = OEMPTY;
の行が、if(nsclean >= nelem(sclean))
のチェックの後に移動されました。
-
test/fixedbugs/issue887.go
:- レジスタリークを再現する新しいテストケースが追加されました。このテストは、
byte
型の変数とuint64
型のシフト量を用いたシフト操作をswitch
文で複数回実行することで、問題が顕在化するように設計されています。
- レジスタリークを再現する新しいテストケースが追加されました。このテストは、
コアとなるコードの解説
-
src/cmd/8g/ggen.c
における変更:cgen_shift
関数は、Goのシフト演算子(<<
,>>
)のコードを生成する役割を担っています。この関数内で、シフト量(nr
)が64ビット型である場合や、シフト量が動的に決定される場合に、split64
関数が呼び出され、64ビットのシフト量を2つの32ビットレジスタに分割して扱います。 追加されたsplitclean();
の呼び出しは、これらの分割されたシフト量が一時的に使用された直後に、それらを保持していたレジスタを解放することを保証します。これにより、レジスタが不必要に占有され続けることがなくなり、レジスタリークが解消されます。 -
src/cmd/8g/gsubr.c
における変更:split64
関数は、64ビットのノードを2つの32ビットのノード(lo
とhi
)に分割し、それらをレジスタに割り当てます。sclean
配列は、splitclean
が後でクリーンアップする必要があるノードを追跡するために使用されます。 元のコードでは、sclean[nsclean].op = OEMPTY;
がnsclean
の境界チェックの前に実行されていました。これは、nsclean
が配列の最大値に達している場合に、配列の範囲外アクセスを引き起こす可能性がありました。変更後、この代入は境界チェックの後に移動され、sclean
配列へのアクセスが常に安全であることを保証します。これは、splitclean
メカニズム全体の堅牢性を高めるための重要な修正です。 -
test/fixedbugs/issue887.go
の追加: このテストファイルは、8g
コンパイラがレジスタリークを起こしていた具体的なシナリオを再現します。f(x byte, y uint64)
関数内で、y
(uint64
型)をシフト量としてx
をシフトする操作をswitch
文で複数回繰り返しています。このようなパターンは、コンパイラがレジスタを繰り返し割り当て、解放し忘れることで、レジスタリークが顕在化する典型的なケースです。このテストの追加により、将来的に同様の回帰バグが発生しないことを保証します。
これらの変更により、8g
コンパイラはシフト操作においてレジスタを適切に管理できるようになり、コンパイルの安定性と効率が向上しました。
関連リンク
- Go Issue #887: https://github.com/golang/go/issues/887
- Go CL 7303061: https://golang.org/cl/7303061
参考にした情報源リンク
- Go言語のソースコード(
src/cmd/8g/ggen.c
,src/cmd/8g/gsubr.c
) - Go Issue Tracker: https://github.com/golang/go/issues
- Go Code Review: https://go-review.googlesource.com/
- Go compiler internals (general knowledge)