[インデックス 19365] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)におけるグローバル変数、関数引数、および関数戻り値のレジスタ割り当て(registerization)の取り扱いを修正するものです。特に、パニックとリカバリ、ガベージコレクション、そしてポインタによる更新といった特殊な状況下での変数の正確な状態管理と、それによるパフォーマンスへの影響を改善することを目的としています。
コミット
commit f5184d34376f92bfa99ec5ca343fe425fd98be85
Author: Russ Cox <rsc@golang.org>
Date: Thu May 15 15:34:53 2014 -0400
cmd/gc: correct handling of globals, func args, results
Globals, function arguments, and results are special cases in
registerization.
Globals must be flushed aggressively, because nearly any
operation can cause a panic, and the recovery code must see
the latest values. Globals also must be loaded aggressively,
because nearly any store through a pointer might be updating a
global: the compiler cannot see all the "address of"
operations on globals, especially exported globals. To
accomplish this, mark all globals as having their address
taken, which effectively disables registerization.
If a function contains a defer statement, the function results
must be flushed aggressively, because nearly any operation can
cause a panic, and the deferred code may call recover, causing
the original function to return the current values of its
function results. To accomplish this, mark all function
results as having their address taken if the function contains
any defer statements. This causes not just aggressive flushing
but also aggressive loading. The aggressive loading is
overkill but the best we can do in the current code.
Function arguments must be considered live at all safe points
in a function, because garbage collection always preserves
them: they must be up-to-date in order to be preserved
correctly. Accomplish this by marking them live at all call
sites. An earlier attempt at this marked function arguments as
having their address taken, which disabled registerization
completely, making programs slower. This CL's solution allows
registerization while preserving safety. The benchmark speedup
is caused by being able to registerize again (the earlier CL
lost the same amount).
benchmark old ns/op new ns/op delta
BenchmarkEqualPort32 61.4 56.0 -8.79%
benchmark old MB/s new MB/s speedup
BenchmarkEqualPort32 521.56 570.97 1.09x
Fixes #1304. (again)
Fixes #7944. (again)
Fixes #7984.
Fixes #7995.
LGTM=khr
R=golang-codereviews, khr
CC=golang-codereviews, iant, r
https://golang.org/cl/97500044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f5184d34376f92bfa99ec5ca343fe425fd98be85
元コミット内容
上記の「コミット」セクションに記載されている内容が、このコミットの元の内容です。
変更の背景
このコミットは、Goコンパイラがグローバル変数、関数引数、および関数戻り値をレジスタに割り当てる際の既存の問題に対処するために導入されました。これらの変数は、Goランタイムの特定の動作(パニックとリカバリ、ガベージコレクション、ポインタによる間接的なアクセス)において特殊な要件を持つため、通常のレジスタ割り当て戦略では問題が発生する可能性がありました。
具体的には、以下の問題が挙げられます。
-
グローバル変数の不正確な状態:
- パニック時のリカバリ: プログラムがパニックを起こし、
recover
が呼び出された場合、グローバル変数の最新の値がメモリにフラッシュされていないと、リカバリコードが古い値を見てしまう可能性がありました。 - ポインタによる更新の不整合: グローバル変数へのポインタを介した更新がコンパイラに完全に認識されない場合、レジスタにキャッシュされた値とメモリ上の値との間に不整合が生じる可能性がありました。特にエクスポートされたグローバル変数ではこの問題が顕著でした。これはIssue #1304とIssue #7995に関連しています。
- パニック時のリカバリ: プログラムがパニックを起こし、
-
defer
文を持つ関数の戻り値の不正確な状態:defer
文を含む関数内でパニックが発生し、recover
が呼び出された場合、その関数は現在の戻り値の値を返します。しかし、戻り値がレジスタにキャッシュされたままだと、最新の値がメモリにフラッシュされず、recover
後の戻り値が期待と異なる可能性がありました。これはIssue #4066(コミットメッセージには直接記載されていませんが、関連する問題として言及されることがあります)やIssue #1304に関連する振る舞いです。
-
関数引数のガベージコレクションとの整合性:
- ガベージコレクション(GC)は、関数引数を常に「ライブ」(使用中)と見なし、それらを保存します。そのため、関数引数は常に最新の状態である必要がありました。以前の試みでは、関数引数を「アドレスが取られた」ものとしてマークすることでこの問題を解決しようとしましたが、これはレジスタ割り当てを完全に無効にし、プログラムの速度を低下させていました。
これらの問題は、コンパイラの最適化(レジスタ割り当て)とランタイムの正確性(パニック/リカバリ、GC)との間のトレードオフに起因していました。このコミットは、これらの特殊なケースを安全かつ効率的に処理するためのより良いアプローチを導入し、特にパフォーマンスの低下を招かずに安全性を確保することを目指しています。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびコンパイラの概念に関する知識が必要です。
-
レジスタ割り当て (Registerization): コンパイラの最適化手法の一つで、頻繁にアクセスされる変数をCPUの高速なレジスタに割り当てることで、メモリへのアクセス回数を減らし、プログラムの実行速度を向上させます。レジスタはメモリよりもアクセスがはるかに高速です。しかし、レジスタに割り当てられた変数は、メモリ上の対応する値と同期が取れていない場合があり、特定の状況下で問題を引き起こす可能性があります。
-
グローバル変数 (Globals): プログラム全体からアクセス可能な変数です。Goでは、パッケージレベルで宣言された変数がグローバル変数に相当します。グローバル変数は、複数のゴルーチンから同時にアクセスされる可能性があり、また、プログラムのどの時点でもその値が変更される可能性があるため、コンパイラは特に慎重に扱う必要があります。
-
関数引数 (Function Arguments) と 関数戻り値 (Function Results): 関数に渡される値(引数)と、関数が返す値(戻り値)です。これらも通常はレジスタに割り当てられることで効率的な関数の呼び出しと戻りが実現されます。
-
defer
文: Go言語のキーワードで、defer
に続く関数呼び出しを、その関数がリターンする直前(パニックが発生した場合でも)に実行することを保証します。defer
はリソースの解放やログの記録など、クリーンアップ処理によく使用されます。 -
panic
とrecover
: Go言語のエラーハンドリングメカニズムの一部です。panic
: プログラムの異常終了を示すために使用されます。panic
が呼び出されると、現在のゴルーチンの通常の実行フローは停止し、defer
関数が順に実行されながらスタックがアンワインドされます。recover
:defer
関数内で呼び出されると、panic
からの回復を試み、パニックの引数を返します。recover
が呼び出されない場合、panic
はプログラム全体を終了させます。recover
が成功すると、パニックが発生した時点からプログラムの実行が再開されます。
-
ガベージコレクション (Garbage Collection - GC): Goランタイムの自動メモリ管理機能です。不要になったメモリを自動的に解放します。GCは、プログラムの実行中にいつでも発生する可能性があり、その際、GCは「ライブ」(使用中)な変数を正確に識別し、それらのメモリを保持する必要があります。関数引数は、GCが実行される際に常にライブであると見なされるため、GCが正確に動作するためには、それらの値が常に最新である必要があります。
-
アドレスが取られた (Address Taken): 変数のアドレスがポインタとして使用されることを指します。コンパイラは、変数のアドレスが取られている場合、その変数がメモリ上に存在し、かつそのメモリ位置が他のコードから間接的にアクセスされる可能性があると判断します。これにより、コンパイラはその変数をレジスタに完全にキャッシュする最適化を控え、メモリとの同期をより頻繁に行うようになります。これは、レジスタ割り当てを無効にする効果があります。
技術的詳細
このコミットの技術的詳細は、Goコンパイラのレジスタ割り当てフェーズにおけるグローバル変数、関数引数、および関数戻り値の特殊な取り扱いに関するものです。
1. グローバル変数の取り扱い
- 問題点: グローバル変数は、プログラムのどこからでもアクセス可能であり、また、ポインタを介して間接的に更新される可能性があります。さらに、Goの
panic
/recover
メカニズムは、予期せぬ時点で発生し、リカバリコードがグローバル変数の最新の状態を参照する必要があります。レジスタにキャッシュされたグローバル変数がメモリにフラッシュされていない場合、これらのシナリオで不整合が生じます。 - 解決策: コンパイラは、すべてのグローバル変数(
node->class == PEXTERN
)を「アドレスが取られた」(v->addr = 1
)ものとしてマークします。- これにより、コンパイラはグローバル変数をレジスタに割り当てることを実質的に無効にします。
- 結果として、グローバル変数へのアクセスは常にメモリを介して行われるようになり、パニック時のリカバリコードが最新の値を参照できるようになります(Issue #1304)。
- また、ポインタを介したグローバル変数への更新も、常にメモリ上の値に反映されるようになり、不整合が解消されます(Issue #7995)。
- このアプローチは、グローバル変数に対するレジスタ割り当ての最適化を犠牲にしますが、正確性と安全性を確保するために必要と判断されました。
2. defer
文を持つ関数の戻り値の取り扱い
- 問題点:
defer
文を含む関数内でパニックが発生し、recover
が呼び出された場合、その関数は現在の戻り値の値を返します。戻り値がレジスタにキャッシュされたままだと、recover
後に古い値が返される可能性があります。 - 解決策: 関数が
defer
文を含む場合(hasdefer
フラグが真の場合)、その関数の戻り値(node->class == PPARAMOUT
)も「アドレスが取られた」(v->addr = 1
)ものとしてマークされます。- これにより、戻り値もレジスタ割り当てが無効になり、常にメモリを介してアクセスされるようになります。
- コミットメッセージでは、「積極的なフラッシュだけでなく、積極的なロードも引き起こす。積極的なロードはやりすぎだが、現在のコードではこれが最善」と述べられています。これは、戻り値が常にメモリに同期されることで、
recover
が呼び出された際に最新の値が利用可能になることを保証します。
3. 関数引数の取り扱い
- 問題点: ガベージコレクションは、関数引数を常にライブであると見なし、それらを保存します。そのため、関数引数は常に最新の状態である必要があります。以前の試みでは、関数引数を「アドレスが取られた」ものとしてマークすることでこの問題を解決しようとしましたが、これはレジスタ割り当てを完全に無効にし、パフォーマンスを低下させました。
- 解決策: 関数引数(
node->class == PPARAM
)は、すべての呼び出しサイト(call sites
)で「ライブ」(ivar.b[z] |= bit.b[z]
)であるとマークされます。- これにより、コンパイラは関数引数をレジスタに割り当てつつも、GCがそれらを正確に追跡できるように、必要な時点でメモリとの同期を保証します。
- このアプローチは、レジスタ割り当てを可能にしながらも安全性を維持するため、以前の解決策で失われたパフォーマンスを取り戻すことができます。ベンチマーク結果(
BenchmarkEqualPort32
の速度向上)は、この改善が成功したことを示しています。
mkvar
関数の変更
mkvar
関数は、変数をレジスタ割り当ての対象としてマークし、その特性(グローバル、引数、戻り値など)に基づいて特別な処理を適用する役割を担っています。このコミットでは、mkvar
内で以下の重要な変更が行われました。
node->class == PPARAM
の場合、ivar
(入力変数ビットセット)にビットを追加し、関数引数をライブとしてマークします。node->class == PPARAMOUT
の場合、ovar
(出力変数ビットセット)にビットを追加し、関数戻り値をライブとしてマークします。node->class == PEXTERN
(グローバル変数)またはhasdefer && node->class == PPARAMOUT
(defer
を持つ関数の戻り値)の場合、v->addr = 1
を設定し、レジスタ割り当てを無効にします。
prop
関数の変更
prop
関数は、レジスタ割り当ての伝播(propagation)ロジックの一部であり、命令間のレジスタの状態を管理します。このコミットでは、以前のIssue #1304のワークアラウンドとして存在していた、各命令の前に変更されたグローバル変数や戻り値を強制的にフラッシュするロジックが削除されました。これは、mkvar
での新しいアプローチ(グローバル変数とdefer
を持つ戻り値のレジスタ割り当て無効化)により、この強制フラッシュが不要になったためです。新しいアプローチはより粒度が高く、効率的です。
これらの変更により、Goコンパイラは、グローバル変数、関数引数、および関数戻り値の正確性とパフォーマンスのバランスを改善し、Goプログラムの堅牢性と実行効率を向上させています。
コアとなるコードの変更箇所
このコミットのコアとなるコードの変更は、主にGoコンパイラのレジスタ割り当てに関連するファイル、具体的にはsrc/cmd/5g/reg.c
、src/cmd/6g/reg.c
、src/cmd/8g/reg.c
に集中しています。これらはそれぞれ、異なるアーキテクチャ(5g: ARM、6g: x86-64、8g: x86)向けのコンパイラバックエンドにおけるレジスタ割り当てロジックを扱っています。
主要な変更点は以下の通りです。
-
setvar
関数の削除:setvar
関数は、以前は関数引数や戻り値をビットセットに構築するために使用されていましたが、このコミットで削除されました。その機能はmkvar
関数内に統合されました。 -
regopt
関数内の初期化ロジックの変更:regopt
関数内で、以前はsetvar
を呼び出してパラメータと結果のリストを構築していましたが、この呼び出しが削除されました。 -
mkvar
関数の変更(最も重要):mkvar
関数は、変数をレジスタ割り当ての対象としてマークする際に、その変数のクラス(node->class
)に基づいて特別な処理を行うようになりました。node->class == PPARAM
(関数引数)の場合、ivar
(入力変数ビットセット)にその変数のビットを追加し、引数をライブとしてマークします。node->class == PPARAMOUT
(関数戻り値)の場合、ovar
(出力変数ビットセット)にその変数のビットを追加し、戻り値をライブとしてマークします。node->addrtaken
(アドレスが取られた変数)の場合、v->addr = 1
を設定し、レジスタ割り当てを無効にします。- 新しいロジックの追加:
node->class == PEXTERN
(グローバル変数)の場合、v->addr = 1
を設定し、レジスタ割り当てを無効にします。これはIssue #1304とIssue #7995への対応です。hasdefer && node->class == PPARAMOUT
(defer
を持つ関数の戻り値)の場合も、v->addr = 1
を設定し、レジスタ割り当てを無効にします。
-
prop
関数の変更:prop
関数内のdefault
ケースで、以前はIssue #1304のワークアラウンドとして、各命令の前に変更されたグローバル変数や戻り値を強制的にフラッシュするロジックが存在しましたが、これが削除されました。これは、mkvar
での新しい、より正確なレジスタ割り当て無効化のロジックにより、この強制フラッシュが不要になったためです。
-
テストファイルの追加/修正:
test/fixedbugs/issue1304.go
: グローバル変数のパニック/リカバリ時の動作をテストする新しいファイル。test/fixedbugs/issue7995.go
およびtest/fixedbugs/issue7995b.dir/x1.go
,x2.go
,issue7995b.go
: グローバル変数がポインタを介して更新された際の動作をテストする新しいファイル群。test/nilptr3.go
: 既存のnilポインタチェックテストファイルが修正され、グローバル変数のnilチェックの振る舞いが更新されました。特に、グローバル変数はレジスタ割り当てされないため、繰り返しnilチェックが削除されないことが明示されています。
これらの変更は、コンパイラのレジスタ割り当てロジックの核心部分に触れており、Goプログラムの実行時の安全性とパフォーマンスに直接影響を与えます。
コアとなるコードの解説
このコミットのコアとなるコードの変更は、Goコンパイラのレジスタ割り当て器が、特定の種類の変数(グローバル変数、関数引数、defer
を持つ関数の戻り値)をどのように扱うかを再定義しています。
mkvar
関数における変更の意図
mkvar
関数は、コンパイラがプログラム内の各変数を分析し、レジスタ割り当ての候補として登録する際に呼び出されます。この関数内で、変数の種類や特性に基づいて、レジスタ割り当ての振る舞いを制御するためのフラグが設定されます。
-
node->class == PPARAM
(関数引数) の処理:if(node->class == PPARAM) for(z=0; z<BITS; z++) ivar.b[z] |= bit.b[z];
このコードは、関数引数を表すノード(
PPARAM
)が検出された場合、その変数をivar
(入力変数ビットセット)に追加します。ivar
は、関数呼び出し時にライブであるべき変数を追跡するために使用されます。これにより、コンパイラは関数引数をレジスタに割り当てつつも、ガベージコレクションがそれらを正確に認識し、保存できるようにします。以前の「アドレスが取られた」としてマークする方法とは異なり、レジスタ割り当てを無効にすることなく安全性を確保しています。 -
node->class == PPARAMOUT
(関数戻り値) の処理:if(node->class == PPARAMOUT) for(z=0; z<BITS; z++) ovar.b[z] |= bit.b[z];
同様に、関数戻り値を表すノード(
PPARAMOUT
)が検出された場合、その変数をovar
(出力変数ビットセット)に追加します。ovar
は、関数が戻る際にライブであるべき変数を追跡するために使用されます。 -
グローバル変数と
defer
を持つ戻り値のレジスタ割り当て無効化:if(node->class == PEXTERN || (hasdefer && node->class == PPARAMOUT)) v->addr = 1;
これがこのコミットの最も重要な変更点の一つです。
node->class == PEXTERN
: これはグローバル変数を表します。グローバル変数は、パニック時のリカバリや、ポインタを介した間接的な更新の際に、常に最新のメモリ上の値が参照される必要があります。そのため、レジスタ割り当てを無効にし、常にメモリからロード/ストアされるようにします。hasdefer && node->class == PPARAMOUT
: これは、defer
文を持つ関数内の戻り値を表します。defer
を持つ関数でパニックが発生し、recover
が呼び出された場合、その関数は現在の戻り値を返します。このとき、戻り値がレジスタにキャッシュされたままだと問題が生じるため、これもレジスタ割り当てを無効にし、メモリとの同期を強制します。v->addr = 1
を設定することで、コンパイラはこれらの変数を「アドレスが取られた」ものとして扱い、レジスタにキャッシュする最適化を抑制します。これにより、常にメモリ上の最新の値が利用可能になります。
prop
関数における変更の意図
prop
関数は、レジスタ割り当てのデータフロー解析の一部として、命令間のレジスタの状態を伝播させます。以前のバージョンでは、Issue #1304のワークアラウンドとして、default
ケース(特定の命令タイプに該当しない場合)で、変更されたグローバル変数や戻り値を強制的にメモリにフラッシュするロジックが含まれていました。
// 削除されたコードの抜粋
// default:
// // Work around for issue 1304:
// // flush modified globals before each instruction.
// for(z=0; z<BITS; z++) {
// cal.b[z] |= externs.b[z];
// // issue 4066: flush modified return variables in case of panic
// if(hasdefer)
// cal.b[z] |= ovar.b[z];
// }
// break;
このコミットでは、上記のコードが削除されました。その理由は、mkvar
関数における新しい、より正確なレジスタ割り当て無効化のロジックが導入されたためです。グローバル変数とdefer
を持つ戻り値は、mkvar
の段階で既にレジスタ割り当ての対象外とされているため、prop
関数で各命令の前に強制的にフラッシュする必要がなくなりました。これにより、コンパイラのコードがよりクリーンになり、冗長な処理が削減されます。
これらの変更は、Goコンパイラが変数のライフタイムとメモリの一貫性をより正確に管理できるようにし、特にパニックやガベージコレクションといったランタイムの重要なイベントにおいて、プログラムの予測可能性と堅牢性を向上させています。同時に、関数引数に対するレジスタ割り当てを再有効化することで、パフォーマンスの向上も実現しています。
関連リンク
- Go CL: https://golang.org/cl/97500044
- Issue 1304: https://code.google.com/p/go/issues/detail?id=1304 (現在はGitHubに移行: https://github.com/golang/go/issues/1304)
- Issue 7944: https://code.google.com/p/go/issues/detail?id=7944 (現在はGitHubに移行: https://github.com/golang/go/issues/7944)
- Issue 7984: https://code.google.com/p/go/issues/detail?id=7984 (現在はGitHubに移行: https://github.com/golang/go/issues/7984)
- Issue 7995: https://code.google.com/p/go/issues/detail?id=7995 (現在はGitHubに移行: https://github.com/golang/go/issues/7995)
参考にした情報源リンク
- Goコミットメッセージ (./commit_data/19365.txt)
- Go言語の公式ドキュメント (Goの
defer
,panic
,recover
, ガベージコレクションに関する一般的な情報) - コンパイラの最適化に関する一般的な知識 (レジスタ割り当ての概念)
- GoのIssueトラッカー (上記Issueの詳細情報)