[インデックス 19469] ファイルの概要
このコミットは、Goコンパイラ(cmd/5g
, 6g
, 8g
)における最適化の挙動を修正し、特にガベージコレクション(GC)やスタックコピーの際に、スタック上の「fat variables」(構造体や配列などの複合型変数)が正しく認識されるようにするものです。具体的には、VARDEF
(変数定義)によって初期化されたこれらの変数が、関数呼び出し時に「ライブ」(生存している)と見なされるように強制することで、最適化によって不要なメモリ書き込みが削除されることを防ぎます。これにより、GCがスタックをスキャンする際に、初期化された正しい値がメモリ上に存在することを保証します。
コミット
commit 1afbceb5999f9c9743630ff8ea002d3ec58a08af
Author: Russ Cox <rsc@golang.org>
Date: Fri May 30 16:41:58 2014 -0400
cmd/6g: treat vardef-initialized fat variables as live at calls
This CL forces the optimizer to preserve some memory stores
that would be redundant except that a stack scan due to garbage
collection or stack copying might look at them during a function call.
As such, it forces additional memory writes and therefore slows
down the execution of some programs, especially garbage-heavy
programs that are already limited by memory bandwidth.
The slowdown can be as much as 7% for end-to-end benchmarks.
These numbers are from running go1.test -test.benchtime=5s three times,
taking the best (lowest) ns/op for each benchmark. I am excluding
benchmarks with time/op < 10us to focus on macro effects.
All benchmarks are on amd64.
Comparing tip (a27f34c771cb) against this CL on an Intel Core i5 MacBook Pro:
benchmark old ns/op new ns/op delta
BenchmarkBinaryTree17 3876500413 3856337341 -0.52%
BenchmarkFannkuch11 2965104777 2991182127 +0.88%
BenchmarkGobDecode 8563026 8788340 +2.63%
BenchmarkGobEncode 5050608 5267394 +4.29%
BenchmarkGzip 431191816 434168065 +0.69%
BenchmarkGunzip 107873523 110563792 +2.49%
BenchmarkHTTPClientServer 85036 86131 +1.29%
BenchmarkJSONEncode 22143764 22501647 +1.62%
BenchmarkJSONDecode 79646916 85658808 +7.55%
BenchmarkMandelbrot200 4720421 4700108 -0.43%
BenchmarkGoParse 4651575 4712247 +1.30%
BenchmarkRegexpMatchMedium_1K 71986 73490 +2.09%
BenchmarkRegexpMatchHard_1K 111018 117495 +5.83%
BenchmarkRevcomp 648798723 659352759 +1.63%
BenchmarkTemplate 112673009 112819078 +0.13%
Comparing tip (a27f34c771cb) against this CL on an Intel Xeon E5520:
BenchmarkBinaryTree17 5461110720 5393104469 -1.25%
BenchmarkFannkuch11 4314677151 4327177615 +0.29%
BenchmarkGobDecode 11065853 11235272 +1.53%
BenchmarkGobEncode 6500065 6959837 +7.07%
BenchmarkGzip 647478596 671769097 +3.75%
BenchmarkGunzip 139348579 141096376 +1.25%
BenchmarkHTTPClientServer 69376 73610 +6.10%
BenchmarkJSONEncode 30172320 31796106 +5.38%
BenchmarkJSONDecode 113704905 114239137 +0.47%
BenchmarkMandelbrot200 6032730 6003077 -0.49%
BenchmarkGoParse 6775251 6405995 -5.45%
BenchmarkRegexpMatchMedium_1K 111832 113895 +1.84%
BenchmarkRegexpMatchHard_1K 161112 168420 +4.54%
BenchmarkRevcomp 876363406 892319935 +1.82%
BenchmarkTemplate 146273096 148998339 +1.86%
Just to get a sense of where we are compared to the previous release,
here are the same benchmarks comparing Go 1.2 to this CL.
Comparing Go 1.2 against this CL on an Intel Core i5 MacBook Pro:
BenchmarkBinaryTree17 4370077662 3856337341 -11.76%
BenchmarkFannkuch11 3347052657 2991182127 -10.63%
BenchmarkGobDecode 8791384 8788340 -0.03%
BenchmarkGobEncode 4968759 5267394 +6.01%
BenchmarkGzip 437815669 434168065 -0.83%
BenchmarkGunzip 94604099 110563792 +16.87%
BenchmarkHTTPClientServer 87798 86131 -1.90%
BenchmarkJSONEncode 22818243 22501647 -1.39%
BenchmarkJSONDecode 97182444 85658808 -11.86%
BenchmarkMandelbrot200 4733516 4700108 -0.71%
BenchmarkGoParse 5054384 4712247 -6.77%
BenchmarkRegexpMatchMedium_1K 67612 73490 +8.69%
BenchmarkRegexpMatchHard_1K 107321 117495 +9.48%
BenchmarkRevcomp 733270055 659352759 -10.08%
BenchmarkTemplate 109304977 112819078 +3.21%
Comparing Go 1.2 against this CL on an Intel Xeon E5520:
BenchmarkBinaryTree17 5986953594 5393104469 -9.92%
BenchmarkFannkuch11 4861139174 4327177615 -10.98%
BenchmarkGobDecode 11830997 11235272 -5.04%
BenchmarkGobEncode 6608722 6959837 +5.31%
BenchmarkGzip 661875826 671769097 +1.49%
BenchmarkGunzip 138630019 141096376 +1.78%
BenchmarkHTTPClientServer 71534 73610 +2.90%
BenchmarkJSONEncode 30393609 31796106 +4.61%
BenchmarkJSONDecode 139645860 114239137 -18.19%
BenchmarkMandelbrot200 5988660 6003077 +0.24%
BenchmarkGoParse 6974092 6405995 -8.15%
BenchmarkRegexpMatchMedium_1K 111331 113895 +2.30%
BenchmarkRegexpMatchHard_1K 165961 168420 +1.48%
BenchmarkRevcomp 995049292 892319935 -10.32%
BenchmarkTemplate 145623363 148998339 +2.32%
Fixes #8036.
LGTM=khr
R=golang-codereviews, josharian, khr
CC=golang-codereviews, iant, r
https://golang.org/cl/99660044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1afbceb5999f9c9743630ff8ea002d3ec58a08af
元コミット内容
cmd/6g: treat vardef-initialized fat variables as live at calls
This CL forces the optimizer to preserve some memory stores
that would be redundant except that a stack scan due to garbage
collection or stack copying might look at them during a function call.
As such, it forces additional memory writes and therefore slows
down the execution of some programs, especially garbage-heavy
programs that are already limited by memory bandwidth.
The slowdown can be as much as 7% for end-to-end benchmarks.
These numbers are from running go1.test -test.benchtime=5s three times,
taking the best (lowest) ns/op for each benchmark. I am excluding
benchmarks with time/op < 10us to focus on macro effects.
All benchmarks are on amd64.
Comparing tip (a27f34c771cb) against this CL on an Intel Core i5 MacBook Pro:
benchmark old ns/op new ns/op delta
BenchmarkBinaryTree17 3876500413 3856337341 -0.52%
BenchmarkFannkuch11 2965104777 2991182127 +0.88%
BenchmarkGobDecode 8563026 8788340 +2.63%
BenchmarkGobEncode 5050608 5267394 +4.29%
BenchmarkGzip 431191816 434168065 +0.69%
BenchmarkGunzip 107873523 110563792 +2.49%
BenchmarkHTTPClientServer 85036 86131 +1.29%
BenchmarkJSONEncode 22143764 22501647 +1.62%
BenchmarkJSONDecode 79646916 85658808 +7.55%
BenchmarkMandelbrot200 4720421 4700108 -0.43%
BenchmarkGoParse 4651575 4712247 +1.30%
BenchmarkRegexpMatchMedium_1K 71986 73490 +2.09%
BenchmarkRegexpMatchHard_1K 111018 117495 +5.83%
BenchmarkRevcomp 648798723 659352759 +1.63%
BenchmarkTemplate 112673009 112819078 +0.13%
Comparing tip (a27f34c771cb) against this CL on an Intel Xeon E5520:
BenchmarkBinaryTree17 5461110720 5393104469 -1.25%
BenchmarkFannkuch11 4314677151 4327177615 +0.29%
BenchmarkGobDecode 11065853 11235272 +1.53%
BenchmarkGobEncode 6500065 6959837 +7.07%
BenchmarkGzip 647478596 671769097 +3.75%
BenchmarkGunzip 139348579 141096376 +1.25%
BenchmarkHTTPClientServer 69376 73610 +6.10%
BenchmarkJSONEncode 30172320 31796106 +5.38%
BenchmarkJSONDecode 113704905 114239137 +0.47%
BenchmarkMandelbrot200 6032730 6003077 -0.49%
BenchmarkGoParse 6775251 6405995 -5.45%
BenchmarkRegexpMatchMedium_1K 111832 113895 +1.84%
BenchmarkRegexpMatchHard_1K 161112 168420 +4.54%
BenchmarkRevcomp 876363406 892319935 +1.82%
BenchmarkTemplate 146273096 148998339 +1.86%
Just to get a sense of where we are compared to the previous release,
here are the same benchmarks comparing Go 1.2 to this CL.
Comparing Go 1.2 against this CL on an Intel Core i5 MacBook Pro:
BenchmarkBinaryTree17 4370077662 3856337341 -11.76%
BenchmarkFannkuch11 3347052657 2991182127 -10.63%
BenchmarkGobDecode 8791384 8788340 -0.03%
BenchmarkGobEncode 4968759 5267394 +6.01%
BenchmarkGzip 437815669 434168065 -0.83%
BenchmarkGunzip 94604099 110563792 +16.87%
BenchmarkHTTPClientServer 87798 86131 -1.90%
BenchmarkJSONEncode 22818243 22501647 -1.39%
BenchmarkJSONDecode 97182444 85658808 -11.86%
BenchmarkMandelbrot200 4733516 4700108 -0.71%
BenchmarkGoParse 5054384 4712247 -6.77%
BenchmarkRegexpMatchMedium_1K 67612 73490 +8.69%
BenchmarkRegexpMatchHard_1K 107321 117495 +9.48%
BenchmarkRevcomp 733270055 659352759 -10.08%
BenchmarkTemplate 109304977 112819078 +3.21%
Comparing Go 1.2 against this CL on an Intel Xeon E5520:
BenchmarkBinaryTree17 5986953594 5393104469 -9.92%
BenchmarkFannkuch11 4861139174 4327177615 -10.98%
BenchmarkGobDecode 11830997 11235272 -5.04%
BenchmarkGobEncode 6608722 6959837 +5.31%
BenchmarkGzip 661875826 671769097 +1.49%
BenchmarkGunzip 138630019 141096376 +1.78%
BenchmarkHTTPClientServer 71534 73610 +2.90%
BenchmarkJSONEncode 30393609 31796106 +4.61%
BenchmarkJSONDecode 139645860 114239137 -18.19%
BenchmarkMandelbrot200 5988660 6003077 +0.24%
BenchmarkGoParse 6974092 6405995 -8.15%
BenchmarkRegexpMatchMedium_1K 111331 113895 +2.30%
BenchmarkRegexpMatchHard_1K 165961 168420 +1.48%
BenchmarkRevcomp 995049292 892319935 -10.32%
BenchmarkTemplate 145623363 148998339 +2.32%
Fixes #8036.
LGTM=khr
R=golang-codereviews, josharian, khr
CC=golang-codereviews, iant, r
https://golang.org/cl/99660044
変更の背景
このコミットは、Goコンパイラの最適化が、特定の状況下でガベージコレクタ(GC)の正確な動作を妨げる可能性があったバグ(Issue 8036)を修正するために導入されました。
Goのガベージコレクタは、プログラムの実行中に不要になったメモリを自動的に解放する役割を担っています。GCが正しく機能するためには、どの変数がまだ「ライブ」(プログラムによって参照されており、将来使用される可能性がある)であるかを正確に把握する必要があります。特に、関数呼び出し中にGCがスタックをスキャンする際、スタック上の変数がヒープ上のオブジェクトへのポインタを含んでいる場合、そのポインタが正しく初期化され、メモリ上に存在している必要があります。
しかし、Goコンパイラの最適化フェーズでは、冗長なメモリ書き込みを削除する「冗長ストア削除(redundant store elimination)」のような最適化が行われます。この最適化は通常、プログラムのパフォーマンスを向上させますが、Issue 8036では、VARDEF
(変数定義)によって初期化された「fat variables」(構造体や配列などの複合型変数で、複数のメモリワードを占めるもの)に対するメモリ書き込みが、GCの観点からは冗長ではないにもかかわらず、最適化によって削除されてしまう問題が発生していました。
その結果、関数呼び出し中にGCがスタックをスキャンした際に、これらの変数が初期化されていない、あるいは古い値を指していると誤認し、本来ライブであるべきヒープオブジェクトを誤って回収してしまう可能性がありました。これは、プログラムのクラッシュや不正な動作につながる重大なバグです。
このコミットは、このようなGCの正確性を損なう最適化を抑制し、GCが常に正しいスタック情報を参照できるようにするために行われました。
前提知識の解説
Goコンパイラ (cmd/5g
, 6g
, 8g
)
Go言語のコンパイラは、ソースコードを機械語に変換するツールチェーンの一部です。cmd/5g
, cmd/6g
, cmd/8g
は、それぞれ異なるアーキテクチャ向けのGoコンパイラを指します。
5g
: ARMアーキテクチャ (例: ARMv5, ARMv6, ARMv7)6g
: AMD64 (x86-64) アーキテクチャ8g
: x86 (32-bit) アーキテクチャ
これらのコンパイラは、Goのソースコードを中間表現に変換し、様々な最適化パスを経て最終的な実行可能バイナリを生成します。本コミットで変更されている reg.c
ファイルは、コンパイラのレジスタ割り当て(register allocation)および最適化フェーズに関連する部分です。
レジスタ割り当てと最適化
コンパイラの重要な役割の一つに、プログラムの実行速度を向上させるための最適化があります。
- レジスタ割り当て (Register Allocation): CPUのレジスタは非常に高速なメモリ領域ですが、その数は限られています。レジスタ割り当ては、プログラムの実行中にどの変数をどのレジスタに格納するかを決定し、メモリへのアクセスを最小限に抑えることでパフォーマンスを向上させます。
- 最適化 (Optimization): コンパイラは、コードの構造を分析し、より効率的な機械語に変換するための様々な最適化手法を適用します。これには、デッドコード削除(dead code elimination)、定数畳み込み(constant folding)、ループ最適化、そして本コミットで問題となった冗長ストア削除などが含まれます。
Goのガベージコレクション (GC)
Goのガベージコレクタは、並行(concurrent)かつ三色マーク&スイープ(tri-color mark-and-sweep)方式を採用しています。
- 並行性: GCは、アプリケーションの実行と並行して動作し、プログラムの停止時間(stop-the-world時間)を最小限に抑えます。
- 三色マーク&スイープ:
- 白 (White): まだGCによって訪問されていないオブジェクト。最終的に回収される可能性がある。
- 灰 (Gray): GCによって訪問されたが、その参照先がまだ訪問されていないオブジェクト。
- 黒 (Black): GCによって訪問され、その参照先もすべて訪問されたオブジェクト。回収されない。 GCは、ルート(グローバル変数、スタック上の変数、レジスタなど)から到達可能なオブジェクトをマークし、マークされなかった(白のままの)オブジェクトをスイープ(回収)します。
スタックスキャンとライブネス情報
GCがヒープ上のオブジェクトを正確にマークするためには、スタック上のどの変数がヒープオブジェクトへのポインタを含んでいるかを知る必要があります。これを「スタックスキャン」と呼びます。
- ライブネス分析 (Liveness Analysis): コンパイラは、プログラムの各ポイントでどの変数が「ライブ」(将来使用される可能性がある)であるかを決定する「ライブネス分析」を行います。この情報は、レジスタ割り当てやデッドコード削除などの最適化に利用されるだけでなく、GCがスタックをスキャンする際にも利用されます。GCは、ライブな変数に含まれるポインタを追跡して、ヒープ上のオブジェクトをマークします。
- ポインタの正確性: GCがスタックをスキャンする際、スタック上のメモリ位置にポインタが格納されている場合、そのポインタが有効なヒープオブジェクトを指している必要があります。もし、コンパイラの最適化によってポインタの初期化がスキップされたり、古い値が残されたりすると、GCは誤ったポインタを追跡したり、本来ライブであるべきオブジェクトを見逃したりする可能性があります。
「Fat Variables」
Goにおける「fat variables」とは、単一のCPUワード(例: 32ビットシステムでの4バイト、64ビットシステムでの8バイト)よりも大きなメモリ領域を占める変数を指します。これには、構造体、配列、スライス、マップ、チャネル、インターフェースなどが含まれます。これらの変数は、その内容全体がメモリ上に配置されるため、特にスタック上に割り当てられる場合、GCがその内容を正確にスキャンできることが重要です。
VARDEF
と VARKILL
これらはGoコンパイラの内部で使われる命令(または概念)です。
VARDEF
(Variable Definition): 変数が定義され、初期化されるポイントを示します。VARKILL
(Variable Kill): 変数がもはや使用されなくなり、その値が不要になるポイントを示します。コンパイラは、VARKILL
以降の変数のメモリ領域を再利用したり、その変数に関連するストアを削除したりする可能性があります。
冗長ストア削除 (Redundant Store Elimination)
これは一般的なコンパイラ最適化の一つです。あるメモリ位置への書き込みが、その値が読み取られる前に別の値で上書きされる場合、またはその値がすでにメモリに存在する場合、最初の書き込みは「冗長」と見なされ、削除されることがあります。この最適化は通常、パフォーマンスを向上させますが、GCのような外部要因がメモリの内容を検査する場合には、問題を引き起こす可能性があります。
技術的詳細
このコミットの核心は、Goコンパイラのレジスタ割り当ておよび最適化フェーズにおいて、VARDEF
によって初期化された「fat variables」が、関数呼び出し(CALL
命令)を跨いで「ライブ」であると見なされるように、ライブネス情報を伝播させる点にあります。
従来の最適化では、コンパイラは特定のメモリ書き込みを冗長と判断し、削除していました。これは、その書き込まれた値が後続のコードで読み取られることなく、すぐに別の値で上書きされる場合などに適用されます。しかし、GoのGCは、プログラムの実行中に非同期的にスタックをスキャンし、ポインタを識別してヒープ上のオブジェクトをマークします。もし、最適化によってスタック上の「fat variables」の初期化(メモリへの書き込み)が削除されてしまうと、GCがスタックをスキャンした際に、そのメモリ位置に古い値や未初期化の値が残っている可能性があり、GCが誤ったポインタを追跡したり、本来ライブであるべきオブジェクトを誤って回収したりする危険性がありました。
このコミットは、この問題を解決するために、コンパイラのregopt
関数(レジスタ最適化のメイン関数)に新しい「パス2.5」を追加しています。
-
新しいパス2.5の導入:
regopt
関数内に、既存のパス2とパス3の間に新しいパス2.5が追加されました。- このパスでは、プログラムの命令(
Prog
)を順に走査します。 AVARDEF
(変数定義)命令に遭遇し、かつその変数がisfat
(複合型、つまり「fat variable」)であり、かつp->to.node->opt
がnil
でない(最適化情報がある)場合、walkvardef
関数が呼び出されます。active
というカウンタがインクリメントされ、walkvardef
に渡されます。これは、現在のVARDEF
に関連する伝播の「アクティブなセッション」を識別するために使用されます。r->act
というビットセットが導入され、VARDEF
以降のCALL
命令までの間に定義された変数を記録するために使用されます。
-
walkvardef
関数の追加:- この新しい関数は、特定の
VARDEF
命令から制御フローグラフを前方(順方向)に走査します。 - 走査中に訪れる各プログラムポイント(
Reg
構造体で表現される)に対して、r1->f.active = active
を設定し、現在のVARDEF
に関連するアクティブな状態であることをマークします。 AVARKILL
(変数破棄)命令に遭遇するか、ABL
(関数呼び出し)またはACALL
(関数呼び出し)命令に遭遇すると、そのパスでの伝播を停止します。これは、変数の寿命が尽きるか、関数呼び出しによってスタックの状態が変化する可能性があるためです。- 走査中に、
VARDEF
された変数のビットをr1->act.b
ビットセットに設定します。これにより、その変数がこのVARDEF
によって初期化され、かつ現在のパスでライブであるという情報が記録されます。
- この新しい関数は、特定の
-
prop
関数の変更:prop
関数は、コンパイラのライブネス分析の一部であり、変数のライブネス情報を伝播させる役割を担っています。- この関数内で、
ACALL
(関数呼び出し)命令を処理する部分が変更されました。 - 変更前は、関数呼び出し時に「ライブ」と見なされる変数(
cal.b[z]
)は、参照されている変数(ref.b[z]
)、外部変数(externs.b[z]
)、および入力変数(ivar.b[z]
)のビットセットのOR演算によって決定されていました。 - 変更後、
r1->act.b[z]
(walkvardef
によって設定された、最近VARDEF
された変数のビットセット)もOR演算に追加されました。 - これにより、
VARDEF
によって初期化された「fat variables」が、関数呼び出しを跨いで強制的に「ライブ」と見なされるようになります。コンパイラは、これらの変数のメモリ書き込みを削除する最適化を抑制し、GCがスタックをスキャンする際に、常に初期化された正しい値がメモリ上に存在することを保証します。
この変更は、GCの正確性を保証するために、コンパイラの最適化の一部を意図的に抑制するものです。その結果、コミットメッセージに示されているように、一部のベンチマークではパフォーマンスの低下(最大7%)が見られます。これは、GCの正確性というトレードオフを受け入れた結果と言えます。
コアとなるコードの変更箇所
変更は主にGoコンパイラのレジスタ割り当てと最適化を担当するファイル群にあります。具体的には、以下のファイルが変更されています。
src/cmd/5g/reg.c
(ARMアーキテクチャ向けコンパイラ)src/cmd/6g/reg.c
(AMD64アーキテクチャ向けコンパイラ)src/cmd/8g/reg.c
(x86アーキテクチャ向けコンパイラ)test/fixedbugs/issue8036.go
(バグ修正を検証するためのテストファイル)
各reg.c
ファイルにおける主要な変更点は以下の通りです。
-
regopt
関数内の新しいパス2.5の追加:// regopt関数の内部 + /* + * pass 2.5 + * iterate propagating fat vardef covering forward + * r->act records vars with a VARDEF since the last CALL. + * (r->act will be reused in pass 5 for something else, + * but we'll be done with it by then.) + */ + active = 0; + for(r = firstr; r != R; r = (Reg*)r->f.link) { + r->f.active = 0; + r->act = zbits; + } + for(r = firstr; r != R; r = (Reg*)r->f.link) { + p = r->f.prog; + if(p->as == AVARDEF && isfat(p->to.node->type) && p->to.node->opt != nil) { + active++; + walkvardef(p->to.node, r, active); + } + }
-
walkvardef
関数の新規追加:+static void +walkvardef(Node *n, Reg *r, int active) +{ + Reg *r1, *r2; + int bn; + Var *v; + + for(r1=r; r1!=R; r1=(Reg*)r1->f.s1) { + if(r1->f.active == active) + break; + r1->f.active = active; + if(r1->f.prog->as == AVARKILL && r1->f.prog->to.node == n) + break; + for(v=n->opt; v!=nil; v=v->nextinnode) { + bn = v - var; + r1->act.b[bn/32] |= 1L << (bn%32); + } + if(r1->f.prog->as == ABL) // 5g/reg.c + // if(r1->f.prog->as == ACALL) // 6g/reg.c, 8g/reg.c + break; + } + + for(r2=r; r2!=r1; r2=(Reg*)r2->f.s1) + if(r2->f.s2 != nil) + walkvardef(n, (Reg*)r2->f.s2, active); +}
注:
ABL
とACALL
は、アーキテクチャによって関数呼び出し命令が異なるため、reg.c
のバージョンによって条件が異なります。 -
prop
関数内のACALL
処理の変更:// prop関数の内部、ACALL命令を処理する箇所 - cal.b[z] |= ref.b[z] | externs.b[z] | ivar.b[z]; + cal.b[z] |= ref.b[z] | externs.b[z] | ivar.b[z] | r1->act.b[z];
test/fixedbugs/issue8036.go
は、このバグを再現し、修正が正しく機能することを確認するためのテストケースです。runtime.GC()
を呼び出すことで、GCがスタックをスキャンするタイミングを強制し、最適化によってストアが削除された場合に問題が発生することを示します。
コアとなるコードの解説
regopt
関数における新しいパス2.5
regopt
関数は、Goコンパイラのバックエンドにおけるレジスタ割り当てと最適化の主要なエントリポイントです。この関数は、プログラムの命令列(Prog
構造体のリンクリスト)を処理し、レジスタの使用状況を分析し、最適化を適用します。
新しいパス2.5は、このregopt
の既存の処理フローに挿入されます。
active = 0;
:active
変数は、walkvardef
関数が呼び出されるたびにインクリメントされ、現在のVARDEF
の伝播セッションを一意に識別するために使用されます。- 最初のループ:
for(r = firstr; r != R; r = (Reg*)r->f.link)
- これは、プログラムのすべての
Reg
(レジスタ割り当ての単位)を走査し、r->f.active
を0に、r->act
をzbits
(ゼロビットセット)に初期化します。これは、新しいパスの開始時に以前の状態をクリアするためです。
- これは、プログラムのすべての
- 二番目のループ:
for(r = firstr; r != R; r = (Reg*)r->f.link)
- 再びプログラムのすべての
Reg
を走査します。 p = r->f.prog;
: 現在のReg
に関連付けられたプログラム命令を取得します。if(p->as == AVARDEF && isfat(p->to.node->type) && p->to.node->opt != nil)
:p->as == AVARDEF
: 現在の命令が変数定義(VARDEF
)であるかを確認します。isfat(p->to.node->type)
: 定義されている変数が「fat variable」(構造体や配列などの複合型)であるかを確認します。p->to.node->opt != nil
: 変数に関連する最適化情報が存在するかを確認します。- これらの条件がすべて満たされた場合、その
VARDEF
がGCの正確性に影響を与える可能性があると判断されます。
active++;
: 新しいVARDEF
の伝播セッションを開始するため、active
カウンタをインクリメントします。walkvardef(p->to.node, r, active);
:walkvardef
関数を呼び出し、このVARDEF
から前方へのライブネス情報の伝播を開始します。
- 再びプログラムのすべての
walkvardef
関数
walkvardef
関数は、特定のVARDEF
命令から開始して、制御フローグラフを前方(順方向)に走査し、そのVARDEF
によって初期化された「fat variable」が、関数呼び出しを跨いでライブであるべきであることをマークします。
-
for(r1=r; r1!=R; r1=(Reg*)r1->f.s1)
:- このループは、現在の
Reg
(r
) から開始し、制御フローの「成功パス」(r1->f.s1
は次の命令へのポインタ)をたどります。 if(r1->f.active == active) break;
: 既に現在のactive
セッションで訪問済みのReg
であれば、無限ループを防ぐために走査を停止します。r1->f.active = active;
: 現在のReg
を、現在のactive
セッションで訪問済みとしてマークします。if(r1->f.prog->as == AVARKILL && r1->f.prog->to.node == n) break;
:- もし現在の命令が
AVARKILL
(変数破棄)であり、かつそれが対象の変数n
に対するものであれば、その変数の寿命が尽きたため、伝播を停止します。
- もし現在の命令が
for(v=n->opt; v!=nil; v=v->nextinnode)
:- 対象の変数
n
に関連するすべてのサブ変数(構造体のフィールドなど)を走査します。 bn = v - var;
: 変数v
のインデックスを計算します。r1->act.b[bn/32] |= 1L << (bn%32);
:r1->act
はビットセットであり、b
はそのビットセットの配列です。- この行は、現在の
Reg
(r1
) において、対象の変数v
が「アクティブ」であることをビットセットに記録します。具体的には、変数のインデックスbn
に対応するビットをセットします。
- 対象の変数
if(r1->f.prog->as == ABL) break;
(またはACALL
):- もし現在の命令が関数呼び出し(
ABL
またはACALL
)であれば、伝播を停止します。これは、関数呼び出しを跨いでライブネス情報を伝播させるための特別な処理がprop
関数で行われるためです。
- もし現在の命令が関数呼び出し(
- このループは、現在の
-
for(r2=r; r2!=r1; r2=(Reg*)r2->f.s1) if(r2->f.s2 != nil) walkvardef(n, (Reg*)r2->f.s2, active);
:- このループは、現在の
Reg
(r
) から、最初のループで停止したr1
までの間のすべてのReg
を走査します。 if(r2->f.s2 != nil)
: もし現在のReg
(r2
) に分岐パス(r2->f.s2
)が存在すれば、その分岐パスに対しても再帰的にwalkvardef
を呼び出します。これにより、制御フローグラフのすべての可能なパスでライブネス情報が伝播されることが保証されます。
- このループは、現在の
prop
関数における ACALL
処理の変更
prop
関数は、コンパイラのライブネス分析において、変数のライブネス情報を逆方向(後方)に伝播させる役割を担っています。特に、関数呼び出し(ACALL
)の命令を処理する際に、その呼び出しを跨いでライブであるべき変数を決定します。
変更された行は以下の通りです。
cal.b[z] |= ref.b[z] | externs.b[z] | ivar.b[z] | r1->act.b[z];
cal.b[z]
: 関数呼び出し後にライブであると見なされる変数のビットセットです。ref.b[z]
: 関数呼び出し内で参照される変数のビットセット。externs.b[z]
: 外部変数(グローバル変数など)のビットセット。ivar.b[z]
: 入力変数(関数の引数など)のビットセット。r1->act.b[z]
: このコミットで追加された部分。walkvardef
関数によって設定された、最近VARDEF
された「fat variables」のビットセットです。
この変更により、VARDEF
によって初期化された「fat variables」は、関数呼び出しを跨いで強制的にライブであると見なされるようになります。これは、コンパイラがこれらの変数のメモリ書き込みを削除する最適化を抑制し、GCがスタックをスキャンする際に、常に初期化された正しい値がメモリ上に存在することを保証するための重要なステップです。コミットメッセージのコメントにあるように、「GCがこのCALL中に発生した場合、コレクタが初期化されたメモリを見るように、最適化が初期化をメモリにフラッシュするように」強制します。
関連リンク
- Go Issue 8036: https://github.com/golang/go/issues/8036
- Go Change List 99660044: https://golang.org/cl/99660044
参考にした情報源リンク
- Go言語の公式ドキュメント
- Goコンパイラのソースコード
- コンパイラ最適化に関する一般的な情報源 (例: Dragon Book)
- ガベージコレクションに関する一般的な情報源 (例: The Garbage Collection Handbook)
- Goのガベージコレクションに関するブログ記事や解説記事 (例: "Go's new GC: Less Latency, More Throughput" by Rick Hudson)
- GoのIssueトラッカーとChange Listシステム
- Goのベンチマークに関する情報I have generated the detailed technical explanation in Markdown format, following all the specified instructions and chapter structure. I have included extensive details on the background, prerequisite knowledge, technical specifics, and core code changes, along with relevant links.
I will now output the generated Markdown to standard output.
# [インデックス 19469] ファイルの概要
このコミットは、Goコンパイラ(`cmd/5g`, `6g`, `8g`)における最適化の挙動を修正し、特にガベージコレクション(GC)やスタックコピーの際に、スタック上の「fat variables」(構造体や配列などの複合型変数)が正しく認識されるようにするものです。具体的には、`VARDEF`(変数定義)によって初期化されたこれらの変数が、関数呼び出し時に「ライブ」(生存している)と見なされるように強制することで、最適化によって不要なメモリ書き込みが削除されることを防ぎます。これにより、GCがスタックをスキャンする際に、初期化された正しい値がメモリ上に存在することを保証します。
## コミット
commit 1afbceb5999f9c9743630ff8ea002d3ec58a08af Author: Russ Cox rsc@golang.org Date: Fri May 30 16:41:58 2014 -0400
cmd/6g: treat vardef-initialized fat variables as live at calls
This CL forces the optimizer to preserve some memory stores
that would be redundant except that a stack scan due to garbage
collection or stack copying might look at them during a function call.
As such, it forces additional memory writes and therefore slows
down the execution of some programs, especially garbage-heavy
programs that are already limited by memory bandwidth.
The slowdown can be as much as 7% for end-to-end benchmarks.
These numbers are from running go1.test -test.benchtime=5s three times,
taking the best (lowest) ns/op for each benchmark. I am excluding
benchmarks with time/op < 10us to focus on macro effects.
All benchmarks are on amd64.
Comparing tip (a27f34c771cb) against this CL on an Intel Core i5 MacBook Pro:
benchmark old ns/op new ns/op delta
BenchmarkBinaryTree17 3876500413 3856337341 -0.52%
BenchmarkFannkuch11 2965104777 2991182127 +0.88%
BenchmarkGobDecode 8563026 8788340 +2.63%
BenchmarkGobEncode 5050608 5267394 +4.29%
BenchmarkGzip 431191816 434168065 +0.69%
BenchmarkGunzip 107873523 110563792 +2.49%
BenchmarkHTTPClientServer 85036 86131 +1.29%
BenchmarkJSONEncode 22143764 22501647 +1.62%
BenchmarkJSONDecode 79646916 85658808 +7.55%
BenchmarkMandelbrot200 4720421 4700108 -0.43%
BenchmarkGoParse 4651575 4712247 +1.30%
BenchmarkRegexpMatchMedium_1K 71986 73490 +2.09%
BenchmarkRegexpMatchHard_1K 111018 117495 +5.83%
BenchmarkRevcomp 648798723 659352759 +1.63%
BenchmarkTemplate 112673009 112819078 +0.13%
Comparing tip (a27f34c771cb) against this CL on an Intel Xeon E5520:
BenchmarkBinaryTree17 5461110720 5393104469 -1.25%
BenchmarkFannkuch11 4314677151 4327177615 +0.29%
BenchmarkGobDecode 11065853 11235272 +1.53%
BenchmarkGobEncode 6500065 6959837 +7.07%
BenchmarkGzip 647478596 671769097 +3.75%
BenchmarkGunzip 139348579 141096376 +1.25%
BenchmarkHTTPClientServer 69376 73610 +6.10%
BenchmarkJSONEncode 30172320 31796106 +5.38%
BenchmarkJSONDecode 113704905 114239137 +0.47%
BenchmarkMandelbrot200 6032730 6003077 -0.49%
BenchmarkGoParse 6775251 6405995 -5.45%
BenchmarkRegexpMatchMedium_1K 111832 113895 +1.84%
BenchmarkRegexpMatchHard_1K 161112 168420 +4.54%
BenchmarkRevcomp 876363406 892319935 +1.82%
BenchmarkTemplate 146273096 148998339 +1.86%
Just to get a sense of where we are compared to the previous release,
here are the same benchmarks comparing Go 1.2 to this CL.
Comparing Go 1.2 against this CL on an Intel Core i5 MacBook Pro:
BenchmarkBinaryTree17 4370077662 3856337341 -11.76%
BenchmarkFannkuch11 3347052657 2991182127 -10.63%
BenchmarkGobDecode 8791384 8788340 -0.03%
BenchmarkGobEncode 4968759 5267394 +6.01%
BenchmarkGzip 437815669 434168065 -0.83%
BenchmarkGunzip 94604099 110563792 +16.87%
BenchmarkHTTPClientServer 87798 86131 -1.90%
BenchmarkJSONEncode 22818243 22501647 -1.39%
BenchmarkJSONDecode 97182444 85658808 -11.86%
BenchmarkMandelbrot200 4733516 4700108 -0.71%
BenchmarkGoParse 5054384 4712247 -6.77%
BenchmarkRegexpMatchMedium_1K 67612 73490 +8.69%
BenchmarkRegexpMatchHard_1K 107321 117495 +9.48%
BenchmarkRevcomp 733270055 659352759 -10.08%
BenchmarkTemplate 109304977 112819078 +3.21%
Comparing Go 1.2 against this CL on an Intel Xeon E5520:
BenchmarkBinaryTree17 5986953594 5393104469 -9.92%
BenchmarkFannkuch11 4861139174 4327177615 -10.98%
BenchmarkGobDecode 11830997 11235272 -5.04%
BenchmarkGobEncode 6608722 6959837 +5.31%
BenchmarkGzip 661875826 671769097 +1.49%
BenchmarkGunzip 138630019 141096376 +1.78%
BenchmarkHTTPClientServer 71534 73610 +2.90%
BenchmarkJSONEncode 30393609 31796106 +4.61%
BenchmarkJSONDecode 139645860 114239137 -18.19%
BenchmarkMandelbrot200 5988660 6003077 +0.24%
BenchmarkGoParse 6974092 6405995 -8.15%
BenchmarkRegexpMatchMedium_1K 111331 113895 +2.30%
BenchmarkRegexpMatchHard_1K 165961 168420 +1.48%
BenchmarkRevcomp 995049292 892319935 -10.32%
BenchmarkTemplate 145623363 148998339 +2.32%
Fixes #8036.
LGTM=khr
R=golang-codereviews, josharian, khr
CC=golang-codereviews, iant, r
https://golang.org/cl/99660044
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/1afbceb5999f9c9743630ff8ea002d3ec58a08af](https://github.com/golang/go/commit/1afbceb5999f9c9743630ff8ea002d3ec58a08af)
## 元コミット内容
cmd/6g: treat vardef-initialized fat variables as live at calls
This CL forces the optimizer to preserve some memory stores that would be redundant except that a stack scan due to garbage collection or stack copying might look at them during a function call. As such, it forces additional memory writes and therefore slows down the execution of some programs, especially garbage-heavy programs that are already limited by memory bandwidth.
The slowdown can be as much as 7% for end-to-end benchmarks.
These numbers are from running go1.test -test.benchtime=5s three times, taking the best (lowest) ns/op for each benchmark. I am excluding benchmarks with time/op < 10us to focus on macro effects. All benchmarks are on amd64.
Comparing tip (a27f34c771cb) against this CL on an Intel Core i5 MacBook Pro:
benchmark old ns/op new ns/op delta BenchmarkBinaryTree17 3876500413 3856337341 -0.52% BenchmarkFannkuch11 2965104777 2991182127 +0.88% BenchmarkGobDecode 8563026 8788340 +2.63% BenchmarkGobEncode 5050608 5267394 +4.29% BenchmarkGzip 431191816 434168065 +0.69% BenchmarkGunzip 107873523 110563792 +2.49% BenchmarkHTTPClientServer 85036 86131 +1.29% BenchmarkJSONEncode 22143764 22501647 +1.62% BenchmarkJSONDecode 79646916 85658808 +7.55% BenchmarkMandelbrot200 4720421 4700108 -0.43% BenchmarkGoParse 4651575 4712247 +1.30% BenchmarkRegexpMatchMedium_1K 71986 73490 +2.09% BenchmarkRegexpMatchHard_1K 111018 117495 +5.83% BenchmarkRevcomp 648798723 659352759 +1.63% BenchmarkTemplate 112673009 112819078 +0.13%
Comparing tip (a27f34c771cb) against this CL on an Intel Xeon E5520:
benchmark old ns/op new ns/op delta BenchmarkBinaryTree17 5461110720 5393104469 -1.25% BenchmarkFannkuch11 4314677151 4327177615 +0.29% BenchmarkGobDecode 11065853 11235272 +1.53% BenchmarkGobEncode 6500065 6959837 +7.07% BenchmarkGzip 647478596 671769097 +3.75% BenchmarkGunzip 139348579 141096376 +1.25% BenchmarkHTTPClientServer 69376 73610 +6.10% BenchmarkJSONEncode 30172320 31796106 +5.38% BenchmarkJSONDecode 113704905 114239137 +0.47% BenchmarkMandelbrot200 6032730 6003077 -0.49% BenchmarkGoParse 6775251 6405995 -5.45% BenchmarkRegexpMatchMedium_1K 111832 113895 +1.84% BenchmarkRegexpMatchHard_1K 161112 168420 +4.54% BenchmarkRevcomp 876363406 892319935 +1.82% BenchmarkTemplate 146273096 148998339 +1.86%
Just to get a sense of where we are compared to the previous release, here are the same benchmarks comparing Go 1.2 to this CL.
Comparing Go 1.2 against this CL on an Intel Core i5 MacBook Pro:
benchmark 4370077662 3856337341 -11.76% BenchmarkFannkuch11 3347052657 2991182127 -10.63% BenchmarkGobDecode 8791384 8788340 -0.03% BenchmarkGobEncode 4968759 5267394 +6.01% BenchmarkGzip 437815669 434168065 -0.83% BenchmarkGunzip 94604099 110563792 +16.87% BenchmarkHTTPClientServer 87798 86131 -1.90% BenchmarkJSONEncode 22818243 22501647 -1.39% BenchmarkJSONDecode 97182444 85658808 -11.86% BenchmarkMandelbrot200 4733516 4700108 -0.71% BenchmarkGoParse 5054384 4712247 -6.77% BenchmarkRegexpMatchMedium_1K 67612 73490 +8.69% BenchmarkRegexpMatchHard_1K 107321 117495 +9.48% BenchmarkRevcomp 733270055 659352759 -10.08% BenchmarkTemplate 109304977 112819078 +3.21%
Comparing Go 1.2 against this CL on an Intel Xeon E5520:
benchmark 5986953594 5393104469 -9.92% BenchmarkFannkuch11 4861139174 4327177615 -10.98% BenchmarkGobDecode 11830997 11235272 -5.04% BenchmarkGobEncode 6608722 6959837 +5.31% BenchmarkGzip 661875826 671769097 +1.49% BenchmarkGunzip 138630019 141096376 +1.78% BenchmarkHTTPClientServer 71534 73610 +2.90% BenchmarkJSONEncode 30393609 31796106 +4.61% BenchmarkJSONDecode 139645860 114239137 -18.19% BenchmarkMandelbrot200 5988660 6003077 +0.24% BenchmarkGoParse 6974092 6405995 -8.15% BenchmarkRegexpMatchMedium_1K 111331 113895 +2.30% BenchmarkRegexpMatchHard_1K 165961 168420 +1.48% BenchmarkRevcomp 995049292 892319935 -10.32% BenchmarkTemplate 145623363 148998339 +2.32%
Fixes #8036.
LGTM=khr R=golang-codereviews, josharian, khr CC=golang-codereviews, iant, r https://golang.org/cl/99660044
## 変更の背景
このコミットは、Goコンパイラの最適化が、特定の状況下でガベージコレクタ(GC)の正確な動作を妨げる可能性があったバグ(Issue 8036)を修正するために導入されました。
Goのガベージコレクタは、プログラムの実行中に不要になったメモリを自動的に解放する役割を担っています。GCが正しく機能するためには、どの変数がまだ「ライブ」(プログラムによって参照されており、将来使用される可能性がある)であるかを正確に把握する必要があります。特に、関数呼び出し中にGCがスタックをスキャンする際、スタック上の変数がヒープ上のオブジェクトへのポインタを含んでいる場合、そのポインタが正しく初期化され、メモリ上に存在している必要があります。
しかし、Goコンパイラの最適化フェーズでは、冗長なメモリ書き込みを削除する「冗長ストア削除(redundant store elimination)」のような最適化が行われます。この最適化は通常、プログラムのパフォーマンスを向上させますが、Issue 8036では、`VARDEF`(変数定義)によって初期化された「fat variables」(構造体や配列などの複合型変数で、複数のメモリワードを占めるもの)に対するメモリ書き込みが、GCの観点からは冗長ではないにもかかわらず、最適化によって削除されてしまう問題が発生していました。
その結果、関数呼び出し中にGCがスタックをスキャンした際に、これらの変数が初期化されていない、あるいは古い値を指していると誤認し、本来ライブであるべきヒープオブジェクトを誤って回収してしまう可能性がありました。これは、プログラムのクラッシュや不正な動作につながる重大なバグです。
このコミットは、このようなGCの正確性を損なう最適化を抑制し、GCが常に正しいスタック情報を参照できるようにするために行われました。
## 前提知識の解説
### Goコンパイラ (`cmd/5g`, `6g`, `8g`)
Go言語のコンパイラは、ソースコードを機械語に変換するツールチェーンの一部です。`cmd/5g`, `cmd/6g`, `cmd/8g` は、それぞれ異なるアーキテクチャ向けのGoコンパイラを指します。
* `5g`: ARMアーキテクチャ (例: ARMv5, ARMv6, ARMv7)
* `6g`: AMD64 (x86-64) アーキテクチャ
* `8g`: x86 (32-bit) アーキテクチャ
これらのコンパイラは、Goのソースコードを中間表現に変換し、様々な最適化パスを経て最終的な実行可能バイナリを生成します。本コミットで変更されている `reg.c` ファイルは、コンパイラのレジスタ割り当て(register allocation)および最適化フェーズに関連する部分です。
### レジスタ割り当てと最適化
コンパイラの重要な役割の一つに、プログラムの実行速度を向上させるための最適化があります。
* **レジスタ割り当て (Register Allocation)**: CPUのレジスタは非常に高速なメモリ領域ですが、その数は限られています。レジスタ割り当ては、プログラムの実行中にどの変数をどのレジスタに格納するかを決定し、メモリへのアクセスを最小限に抑えることでパフォーマンスを向上させます。
* **最適化 (Optimization)**: コンパイラは、コードの構造を分析し、より効率的な機械語に変換するための様々な最適化手法を適用します。これには、デッドコード削除(dead code elimination)、定数畳み込み(constant folding)、ループ最適化、そして本コミットで問題となった冗長ストア削除などが含まれます。
### Goのガベージコレクション (GC)
Goのガベージコレクタは、並行(concurrent)かつ三色マーク&スイープ(tri-color mark-and-sweep)方式を採用しています。
* **並行性**: GCは、アプリケーションの実行と並行して動作し、プログラムの停止時間(stop-the-world時間)を最小限に抑えます。
* **三色マーク&スイープ**:
* **白 (White)**: まだGCによって訪問されていないオブジェクト。最終的に回収される可能性がある。
* **灰 (Gray)**: GCによって訪問されたが、その参照先がまだ訪問されていないオブジェクト。
* **黒 (Black)**: GCによって訪問され、その参照先もすべて訪問されたオブジェクト。回収されない。
GCは、ルート(グローバル変数、スタック上の変数、レジスタなど)から到達可能なオブジェクトをマークし、マークされなかった(白のままの)オブジェクトをスイープ(回収)します。
### スタックスキャンとライブネス情報
GCがヒープ上のオブジェクトを正確にマークするためには、スタック上のどの変数がヒープオブジェクトへのポインタを含んでいるかを知る必要があります。これを「スタックスキャン」と呼びます。
* **ライブネス分析 (Liveness Analysis)**: コンパイラは、プログラムの各ポイントでどの変数が「ライブ」(将来使用される可能性がある)であるかを決定する「ライブネス分析」を行います。この情報は、レジスタ割り当てやデッドコード削除などの最適化に利用されるだけでなく、GCがスタックをスキャンする際にも利用されます。GCは、ライブな変数に含まれるポインタを追跡して、ヒープ上のオブジェクトをマークします。
* **ポインタの正確性**: GCがスタックをスキャンする際、スタック上のメモリ位置にポインタが格納されている場合、そのポインタが有効なヒープオブジェクトを指している必要があります。もし、コンパイラの最適化によってポインタの初期化がスキップされたり、古い値が残されたりすると、GCは誤ったポインタを追跡したり、本来ライブであるべきオブジェクトを見逃したりする可能性があります。
### 「Fat Variables」
Goにおける「fat variables」とは、単一のCPUワード(例: 32ビットシステムでの4バイト、64ビットシステムでの8バイト)よりも大きなメモリ領域を占める変数を指します。これには、構造体、配列、スライス、マップ、チャネル、インターフェースなどが含まれます。これらの変数は、その内容全体がメモリ上に配置されるため、特にスタック上に割り当てられる場合、GCがその内容を正確にスキャンできることが重要です。
### `VARDEF` と `VARKILL`
これらはGoコンパイラの内部で使われる命令(または概念)です。
* `VARDEF` (Variable Definition): 変数が定義され、初期化されるポイントを示します。
* `VARKILL` (Variable Kill): 変数がもはや使用されなくなり、その値が不要になるポイントを示します。コンパイラは、`VARKILL`以降の変数のメモリ領域を再利用したり、その変数に関連するストアを削除したりする可能性があります。
### 冗長ストア削除 (Redundant Store Elimination)
これは一般的なコンパイラ最適化の一つです。あるメモリ位置への書き込みが、その値が読み取られる前に別の値で上書きされる場合、またはその値がすでにメモリに存在する場合、最初の書き込みは「冗長」と見なされ、削除されることがあります。この最適化は通常、パフォーマンスを向上させますが、GCのような外部要因がメモリの内容を検査する場合には、問題を引き起こす可能性があります。
## 技術的詳細
このコミットの核心は、Goコンパイラのレジスタ割り当ておよび最適化フェーズにおいて、`VARDEF`によって初期化された「fat variables」が、関数呼び出し(`CALL`命令)を跨いで「ライブ」であると見なされるように、ライブネス情報を伝播させる点にあります。
従来の最適化では、コンパイラは特定のメモリ書き込みを冗長と判断し、削除していました。これは、その書き込まれた値が後続のコードで読み取られることなく、すぐに別の値で上書きされる場合などに適用されます。しかし、GoのGCは、プログラムの実行中に非同期的にスタックをスキャンし、ポインタを識別してヒープ上のオブジェクトをマークします。もし、最適化によってスタック上の「fat variables」の初期化(メモリへの書き込み)が削除されてしまうと、GCがスタックをスキャンした際に、そのメモリ位置に古い値や未初期化の値が残っている可能性があり、GCが誤ったポインタを追跡したり、本来ライブであるべきオブジェクトを誤って回収したりする危険性がありました。
このコミットは、この問題を解決するために、コンパイラの`regopt`関数(レジスタ最適化のメイン関数)に新しい「パス2.5」を追加しています。
1. **新しいパス2.5の導入**:
* `regopt`関数内に、既存のパス2とパス3の間に新しいパス2.5が追加されました。
* このパスでは、プログラムの命令(`Prog`)を順に走査します。
* `AVARDEF`(変数定義)命令に遭遇し、かつその変数が`isfat`(複合型、つまり「fat variable」)であり、かつ`p->to.node->opt`が`nil`でない(最適化情報がある)場合、`walkvardef`関数が呼び出されます。
* `active`というカウンタがインクリメントされ、`walkvardef`に渡されます。これは、現在の`VARDEF`に関連する伝播の「アクティブなセッション」を識別するために使用されます。
* `r->act`というビットセットが導入され、`VARDEF`以降の`CALL`命令までの間に定義された変数を記録するために使用されます。
2. **`walkvardef`関数の追加**:
* この新しい関数は、特定の`VARDEF`命令から制御フローグラフを前方(順方向)に走査します。
* 走査中に訪れる各プログラムポイント(`Reg`構造体で表現される)に対して、`r1->f.active = active`を設定し、現在の`VARDEF`に関連するアクティブな状態であることをマークします。
* `AVARKILL`(変数破棄)命令に遭遇するか、`ABL`(関数呼び出し)または`ACALL`(関数呼び出し)命令に遭遇すると、そのパスでの伝播を停止します。これは、変数の寿命が尽きるか、関数呼び出しによってスタックの状態が変化する可能性があるためです。
* 走査中に、`VARDEF`された変数のビットを`r1->act.b`ビットセットに設定します。これにより、その変数がこの`VARDEF`によって初期化され、かつ現在のパスでライブであるという情報が記録されます。
3. **`prop`関数の変更**:
* `prop`関数は、コンパイラのライブネス分析の一部であり、変数のライブネス情報を伝播させる役割を担っています。
* この関数内で、`ACALL`(関数呼び出し)命令を処理する部分が変更されました。
* 変更前は、関数呼び出し時に「ライブ」と見なされる変数(`cal.b[z]`)は、参照されている変数(`ref.b[z]`)、外部変数(`externs.b[z]`)、および入力変数(`ivar.b[z]`)のビットセットのOR演算によって決定されていました。
* 変更後、`r1->act.b[z]`(`walkvardef`によって設定された、最近`VARDEF`された変数のビットセット)もOR演算に追加されました。
* これにより、`VARDEF`によって初期化された「fat variables」が、関数呼び出しを跨いで強制的に「ライブ」と見なされるようになります。コンパイラは、これらの変数のメモリ書き込みを削除する最適化を抑制し、GCがスタックをスキャンする際に、常に初期化された正しい値がメモリ上に存在することを保証します。
この変更は、GCの正確性を保証するために、コンパイラの最適化の一部を意図的に抑制するものです。その結果、コミットメッセージに示されているように、一部のベンチマークではパフォーマンスの低下(最大7%)が見られます。これは、GCの正確性というトレードオフを受け入れた結果と言えます。
## コアとなるコードの変更箇所
変更は主にGoコンパイラのレジスタ割り当てと最適化を担当するファイル群にあります。具体的には、以下のファイルが変更されています。
* `src/cmd/5g/reg.c` (ARMアーキテクチャ向けコンパイラ)
* `src/cmd/6g/reg.c` (AMD64アーキテクチャ向けコンパイラ)
* `src/cmd/8g/reg.c` (x86アーキテクチャ向けコンパイラ)
* `test/fixedbugs/issue8036.go` (バグ修正を検証するためのテストファイル)
各`reg.c`ファイルにおける主要な変更点は以下の通りです。
1. **`regopt` 関数内の新しいパス2.5の追加**:
```c
// regopt関数の内部
+ /*
+ * pass 2.5
+ * iterate propagating fat vardef covering forward
+ * r->act records vars with a VARDEF since the last CALL.
+ * (r->act will be reused in pass 5 for something else,
+ * but we'll be done with it by then.)
+ */
+ active = 0;
+ for(r = firstr; r != R; r = (Reg*)r->f.link) {
+ r->f.active = 0;
+ r->act = zbits;
+ }
+ for(r = firstr; r != R; r = (Reg*)r->f.link) {
+ p = r->f.prog;
+ if(p->as == AVARDEF && isfat(p->to.node->type) && p->to.node->opt != nil) {
+ active++;
+ walkvardef(p->to.node, r, active);
+ }
+ }
```
2. **`walkvardef` 関数の新規追加**:
```c
+static void
+walkvardef(Node *n, Reg *r, int active)
+{
+ Reg *r1, *r2;
+ int bn;
+ Var *v;
+
+ for(r1=r; r1!=R; r1=(Reg*)r1->f.s1) {
+ if(r1->f.active == active)
+ break;
+ r1->f.active = active;
+ if(r1->f.prog->as == AVARKILL && r1->f.prog->to.node == n)
+ break;
+ for(v=n->opt; v!=nil; v=v->nextinnode) {
+ bn = v - var;
+ r1->act.b[bn/32] |= 1L << (bn%32);
+ }
+ if(r1->f.prog->as == ABL) // 5g/reg.c
+ // if(r1->f.prog->as == ACALL) // 6g/reg.c, 8g/reg.c
+ break;
+ }
+
+ for(r2=r; r2!=r1; r2=(Reg*)r2->f.s1)
+ if(r2->f.s2 != nil)
+ walkvardef(n, (Reg*)r2->f.s2, active);
+}
```
*注: `ABL`と`ACALL`は、アーキテクチャによって関数呼び出し命令が異なるため、`reg.c`のバージョンによって条件が異なります。*
3. **`prop` 関数内の `ACALL` 処理の変更**:
```c
// prop関数の内部、ACALL命令を処理する箇所
- cal.b[z] |= ref.b[z] | externs.b[z] | ivar.b[z];
+ cal.b[z] |= ref.b[z] | externs.b[z] | ivar.b[z] | r1->act.b[z];
```
`test/fixedbugs/issue8036.go` は、このバグを再現し、修正が正しく機能することを確認するためのテストケースです。`runtime.GC()`を呼び出すことで、GCがスタックをスキャンするタイミングを強制し、最適化によってストアが削除された場合に問題が発生することを示します。
## コアとなるコードの解説
### `regopt` 関数における新しいパス2.5
`regopt`関数は、Goコンパイラのバックエンドにおけるレジスタ割り当てと最適化の主要なエントリポイントです。この関数は、プログラムの命令列(`Prog`構造体のリンクリスト)を処理し、レジスタの使用状況を分析し、最適化を適用します。
新しいパス2.5は、この`regopt`の既存の処理フローに挿入されます。
* `active = 0;`: `active`変数は、`walkvardef`関数が呼び出されるたびにインクリメントされ、現在の`VARDEF`の伝播セッションを一意に識別するために使用されます。
* 最初のループ: `for(r = firstr; r != R; r = (Reg*)r->f.link)`
* これは、プログラムのすべての`Reg`(レジスタ割り当ての単位)を走査し、`r->f.active`を0に、`r->act`を`zbits`(ゼロビットセット)に初期化します。これは、新しいパスの開始時に以前の状態をクリアするためです。
* 二番目のループ: `for(r = firstr; r != R; r = (Reg*)r->f.link)`
* 再びプログラムのすべての`Reg`を走査します。
* `p = r->f.prog;`: 現在の`Reg`に関連付けられたプログラム命令を取得します。
* `if(p->as == AVARDEF && isfat(p->to.node->type) && p->to.node->opt != nil)`:
* `p->as == AVARDEF`: 現在の命令が変数定義(`VARDEF`)であるかを確認します。
* `isfat(p->to.node->type)`: 定義されている変数が「fat variable」(構造体や配列などの複合型)であるかを確認します。
* `p->to.node->opt != nil`: 変数に関連する最適化情報が存在するかを確認します。
* これらの条件がすべて満たされた場合、その`VARDEF`がGCの正確性に影響を与える可能性があると判断されます。
* `active++;`: 新しい`VARDEF`の伝播セッションを開始するため、`active`カウンタをインクリメントします。
* `walkvardef(p->to.node, r, active);`: `walkvardef`関数を呼び出し、この`VARDEF`から前方へのライブネス情報の伝播を開始します。
### `walkvardef` 関数
`walkvardef`関数は、特定の`VARDEF`命令から開始して、制御フローグラフを前方(順方向)に走査し、その`VARDEF`によって初期化された「fat variable」が、関数呼び出しを跨いでライブであるべきであることをマークします。
* `for(r1=r; r1!=R; r1=(Reg*)r1->f.s1)`:
* このループは、現在の`Reg` (`r`) から開始し、制御フローの「成功パス」(`r1->f.s1`は次の命令へのポインタ)をたどります。
* `if(r1->f.active == active) break;`: 既に現在の`active`セッションで訪問済みの`Reg`であれば、無限ループを防ぐために走査を停止します。
* `r1->f.active = active;`: 現在の`Reg`を、現在の`active`セッションで訪問済みとしてマークします。
* `if(r1->f.prog->as == AVARKILL && r1->f.prog->to.node == n) break;`:
* もし現在の命令が`AVARKILL`(変数破棄)であり、かつそれが対象の変数`n`に対するものであれば、その変数の寿命が尽きたため、伝播を停止します。
* `for(v=n->opt; v!=nil; v=v->nextinnode)`:
* 対象の変数`n`に関連するすべてのサブ変数(構造体のフィールドなど)を走査します。
* `bn = v - var;`: 変数`v`のインデックスを計算します。
* `r1->act.b[bn/32] |= 1L << (bn%32);`:
* `r1->act`はビットセットであり、`b`はそのビットセットの配列です。
* この行は、現在の`Reg` (`r1`) において、対象の変数`v`が「アクティブ」であることをビットセットに記録します。具体的には、変数のインデックス`bn`に対応するビットをセットします。
* `if(r1->f.prog->as == ABL) break;` (または `ACALL`):
* もし現在の命令が関数呼び出し(`ABL`または`ACALL`)であれば、伝播を停止します。これは、関数呼び出しを跨いでライブネス情報を伝播させるための特別な処理が`prop`関数で行われるためです。
* `for(r2=r; r2!=r1; r2=(Reg*)r2->f.s1) if(r2->f.s2 != nil) walkvardef(n, (Reg*)r2->f.s2, active);`:
* このループは、現在の`Reg` (`r`) から、最初のループで停止した`r1`までの間のすべての`Reg`を走査します。
* `if(r2->f.s2 != nil)`: もし現在の`Reg` (`r2`) に分岐パス(`r2->f.s2`)が存在すれば、その分岐パスに対しても再帰的に`walkvardef`を呼び出します。これにより、制御フローグラフのすべての可能なパスでライブネス情報が伝播されることが保証されます。
### `prop` 関数における `ACALL` 処理の変更
`prop`関数は、コンパイラのライブネス分析において、変数のライブネス情報を逆方向(後方)に伝播させる役割を担っています。特に、関数呼び出し(`ACALL`)の命令を処理する際に、その呼び出しを跨いでライブであるべき変数を決定します。
変更された行は以下の通りです。
```c
cal.b[z] |= ref.b[z] | externs.b[z] | ivar.b[z] | r1->act.b[z];
cal.b[z]
: 関数呼び出し後にライブであると見なされる変数のビットセットです。ref.b[z]
: 関数呼び出し内で参照される変数のビットセット。externs.b[z]
: 外部変数(グローバル変数など)のビットセット。ivar.b[z]
: 入力変数(関数の引数など)のビットセット。r1->act.b[z]
: このコミットで追加された部分。walkvardef
関数によって設定された、最近VARDEF
された「fat variables」のビットセットです。
この変更により、VARDEF
によって初期化された「fat variables」は、関数呼び出しを跨いで強制的にライブであると見なされるようになります。これは、コンパイラがこれらの変数のメモリ書き込みを削除する最適化を抑制し、GCがスタックをスキャンする際に、常に初期化された正しい値がメモリ上に存在することを保証するための重要なステップです。コミットメッセージのコメントにあるように、「GCがこのCALL中に発生した場合、コレクタが初期化されたメモリを見るように、最適化が初期化をメモリにフラッシュするように」強制します。
関連リンク
- Go Issue 8036: https://github.com/golang/go/issues/8036
- Go Change List 99660044: https://golang.org/cl/99660044
参考にした情報源リンク
- Go言語の公式ドキュメント
- Goコンパイラのソースコード
- コンパイラ最適化に関する一般的な情報源 (例: Dragon Book)
- ガベージコレクションに関する一般的な情報源 (例: The Garbage Collection Handbook)
- Goのガベージコレクションに関するブログ記事や解説記事 (例: "Go's new GC: Less Latency, More Throughput" by Rick Hudson)
- GoのIssueトラッカーとChange Listシステム
- Goのベンチマークに関する情報