[インデックス 18519] ファイルの概要
コミット
commit 91b1f7cb15700f39ca63c4e056b41d9b04100e97
Author: Russ Cox <rsc@golang.org>
Date: Thu Feb 13 22:45:16 2014 -0500
cmd/gc: handle variable initialization by block move in liveness
Any initialization of a variable by a block copy or block zeroing
or by multiple assignments (componentwise copying or zeroing
of a multiword variable) needs to emit a VARDEF. These cases were not.
Fixes #7205.
TBR=iant
CC=golang-codereviews
https://golang.org/cl/63650044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/91b1f7cb15700f39ca63c4e056b41d9b04100e97
元コミット内容
cmd/gc: handle variable initialization by block move in liveness
Any initialization of a variable by a block copy or block zeroing
or by multiple assignments (componentwise copying or zeroing
of a multiword variable) needs to emit a VARDEF. These cases were not.
Fixes #7205.
TBR=iant
CC=golang-codereviews
https://golang.org/cl/63650044
変更の背景
このコミットは、Goコンパイラ(cmd/gc
)における変数のライブネス解析の正確性を向上させることを目的としています。具体的には、ブロックコピー(block move
)やブロックゼロ化(block zeroing
)、あるいは複数回の代入(多ワード変数のコンポーネントごとのコピーやゼロ化)によって変数が初期化される場合に、コンパイラがVARDEF
(Variable Definition)イベントを正しく発行していなかった問題に対処しています。
ライブネス解析は、プログラムの特定のポイントでどの変数が「ライブ」(将来使用される可能性がある)であるかを判断するコンパイラの最適化フェーズです。この解析は、レジスタ割り当て、デッドコード削除、スタックフレームの最適化など、多くの重要な最適化の基盤となります。ライブネス解析が正しく機能するためには、変数がいつ定義(初期化)され、いつ使用され、いつデッドになるかを正確に追跡する必要があります。
VARDEF
は、コンパイラが変数の定義サイトをマークするために使用する内部的な命令またはイベントです。変数が初期化されたことをコンパイラに通知し、ライブネス解析がその変数のライフタイムを正確に開始できるようにします。以前の実装では、単純な単一の代入による初期化はVARDEF
を生成していましたが、より複雑な初期化パターン(例えば、構造体全体のコピーや、複数のフィールドへの個別の代入による構造体の初期化)ではVARDEF
が欠落していました。
この欠落により、ライブネス解析が変数を早期にデッドと判断したり、逆に不要な変数をライブと判断し続けたりする可能性がありました。これは、生成されるコードの効率性やデバッグ情報の正確性に悪影響を及ぼす可能性があります。コミットメッセージにあるFixes #7205
は、この問題が特定のバグとして報告されていたことを示唆しています。
前提知識の解説
このコミットを理解するためには、以下のGoコンパイラと関連する概念についての知識が必要です。
-
Goコンパイラ (
cmd/gc
,5g
,6g
,8g
):cmd/gc
はGo言語の公式コンパイラです。Go 1.5以降は単一のgo tool compile
コマンドに統合されましたが、このコミットが作成された2014年当時は、各アーキテクチャ(例:5g
はARM、6g
はx86-64、8g
はx86-32)ごとに異なるコンパイラバイナリが存在していました。これらのコンパイラは共通のフロントエンドと最適化パスを共有し、バックエンドでアーキテクチャ固有のコード生成を行っていました。cgen.c
ファイルは、これらのアーキテクチャ固有のコンパイラにおけるコード生成(code generation
)を担当する部分です。pgen.c
とplive.c
は、コンパイラの共通部分(プラットフォーム非依存)で、それぞれプログラミング言語の構造から中間表現への変換(pgen
)とライブネス解析(plive
)を担当します。
-
ライブネス解析 (Liveness Analysis):
- コンパイラのデータフロー解析の一種で、プログラムの各ポイントにおいて、どの変数の値が将来使用される可能性があるか(「ライブ」であるか)を決定します。
- 変数がライブであるとは、その変数の現在の値がプログラムの実行パスのどこかで読み取られる可能性があることを意味します。
- 変数がデッドであるとは、その変数の現在の値が将来使用されることがないことを意味します。
- ライブネス情報は、レジスタ割り当て(ライブな変数をレジスタに保持し、デッドな変数のレジスタを再利用する)、デッドストア削除(デッドな変数への書き込みを削除する)、スタックフレームのサイズ決定などに利用されます。
-
VARDEF
:- Goコンパイラの内部的な概念で、変数が「定義された」(つまり、有効な値が割り当てられた)ことを示すマーカーです。
- ライブネス解析は、
VARDEF
イベントを検出することで、変数のライブネスの開始点を認識します。これにより、変数のライフタイムを正確に追跡し、不要なレジスタ保持やメモリ割り当てを防ぎます。
-
ブロックコピー (Block Copy) とブロックゼロ化 (Block Zeroing):
- ブロックコピー: 構造体や配列のような複合型全体を、メモリ上で一括してコピーする操作です。例えば、
dst = src
のような代入で、dst
とsrc
が大きな構造体である場合、コンパイラはこれをバイト単位のブロックコピーとして実装することがあります。 - ブロックゼロ化: 構造体や配列のような複合型のメモリ領域全体を、一括してゼロで埋める操作です。変数が初期化時にゼロ値で埋められる場合などに使用されます。
- これらの操作は、複数のフィールドや要素にわたる初期化を効率的に行うためのものです。
- ブロックコピー: 構造体や配列のような複合型全体を、メモリ上で一括してコピーする操作です。例えば、
-
Goコンパイラにおけるノードの種類 (
ONAME
,PEXTERN
,PPARAMOUT
,PAUTO
,PPARAM
):- Goコンパイラは、プログラムのソースコードを抽象構文木(AST)として表現します。ASTの各要素は「ノード」と呼ばれます。
ONAME
: 変数や関数などの名前を表すノードです。PEXTERN
: 外部リンケージを持つ変数(グローバル変数など)を表すクラスです。PPARAMOUT
: 関数の戻り値パラメータを表すクラスです。PAUTO
: ローカル変数(自動変数)を表すクラスです。PPARAM
: 関数の入力パラメータを表すクラスです。- これらのクラスは、変数のスコープやストレージクラスを区別するために使用されます。
-
isfat
とisfunny
関数:isfat(type)
: 型が「fat」(つまり、複数のワードを占める大きな型、例えば構造体や配列)であるかどうかをチェックするGoコンパイラ内部のヘルパー関数です。isfunny(node)
: 特定の特殊なノード(例えば、.fp
(フレームポインタ)、.args
(関数引数領域)、_
(ブランク識別子))を識別するためのヘルパー関数です。これらのノードは通常の変数とは異なるライブネスの振る舞いをするため、特別に扱われることがあります。
技術的詳細
このコミットの核心は、Goコンパイラのコード生成フェーズ(sgen
関数)とライブネス解析フェーズ(plive.c
)におけるVARDEF
イベントの生成と処理の改善にあります。
以前のGoコンパイラでは、変数の初期化が単純な単一の代入(例: x = 10
)である場合、VARDEF
が適切に発行されていました。しかし、以下のようなケースではVARDEF
が欠落していました。
-
ブロックコピーによる初期化:
type S struct { a, b, c int } var s1 S var s2 S s2 = s1 // s2の初期化がブロックコピーで行われる
この場合、
s2
全体がs1
からコピーされることで初期化されますが、コンパイラはs2
が「定義された」ことを示すVARDEF
を適切に発行していませんでした。 -
ブロックゼロ化による初期化:
type S struct { a, b, c int } var s S // sはゼロ値で初期化される
複合型がゼロ値で初期化される場合、そのメモリ領域はゼロで埋められます。この操作も
VARDEF
を必要としますが、発行されていませんでした。 -
複数回の代入による多ワード変数の初期化:
type S struct { a, b int } var s S s.a = 1 s.b = 2 // sの各コンポーネントが個別に初期化される
多ワード変数(例: 構造体)が、そのコンポーネント(フィールド)ごとに個別の代入によって初期化される場合、全体としての変数の定義が正しく追跡されていませんでした。
これらのケースでは、sgen
関数(Goコンパイラのコード生成器)が、変数の初期化のためにメモリ操作(ブロックコピーやゼロ化)を生成しますが、その際にVARDEF
を伴っていませんでした。結果として、ライブネス解析はこれらの変数がいつ「ライブ」になったかを正確に判断できず、誤った最適化やデバッグ情報の不正確さにつながっていました。
このコミットでは、sgen
関数内で、res
(結果変数)がONAME
ノードであり、かつ外部変数(PEXTERN
)でない場合に、gvardef(res)
を呼び出すように変更されました。これにより、ブロックコピーやゼロ化によって初期化される変数に対してもVARDEF
が発行されるようになります。
また、関数の戻り値(.args
シンボルで表される)の初期化についても同様の修正が行われました。関数が複数の戻り値を返す場合、それらは.args
という特殊なシンボルを通じて扱われます。これらの戻り値パラメータ(PPARAMOUT
)も、gvardef
によって定義サイトがマークされるようになりました。
plive.c
における変更は、ライブネス解析のロジックを調整し、VARDEF
の新しい生成パターンに対応するためのものです。特に、isfunny
関数からブランク識別子_
が除外され、progeffects
関数におけるVARDEF
の処理がより汎用的に変更されました。これにより、VARDEF
が発行された変数は、その定義サイトで正しくライブとしてマークされるようになります。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが変更されています。
-
src/cmd/5g/cgen.c
,src/cmd/6g/cgen.c
,src/cmd/8g/cgen.c
:- 各アーキテクチャ(ARM, x86-64, x86-32)のコード生成器ファイル。
sgen
関数内に、変数の定義サイトをライブネス解析のために記録するgvardef(res)
の呼び出しが追加されました。- 特に、
res->op == ONAME && res->class != PEXTERN
の場合にgvardef(res)
が呼ばれます。 - また、
.args
シンボルがコピーされる場合(関数の戻り値の初期化に相当)、その関数の戻り値パラメータ(PPARAMOUT
)に対してもgvardef
が呼ばれるようになりました。
-
src/cmd/gc/pgen.c
:gvardef
関数のエラーチェックが変更されました。以前はisfat(n->type)
(ノードの型がfatであるか)をチェックしていましたが、これは削除され、単にn == N
(ノードがnilでないか)をチェックするようになりました。これは、gvardef
がfatでない変数に対しても呼ばれるようになったためです。
-
src/cmd/gc/plive.c
:isfunny
関数から、ブランク識別子_
が除外されました。これにより、_
が通常の変数と同様にライブネス解析の対象となる可能性が示唆されます(ただし、_
は通常、値を破棄するために使用されるため、ライブネス解析の対象から外れることが多いですが、この変更はより一般的なVARDEF
の処理を可能にします)。progeffects
関数内のライブネス解析ロジックが変更されました。特に、from->node
やto->node
がisfunny
でないことのチェックが削除され、より一般的な条件でライブネスの更新が行われるようになりました。LeftWrite
フラグがセットされている場合、isfat(from->node->type)
のチェックが削除され、prog->as == AVARDEF
の場合にのみvarkill
がセットされるようになりました。to->node->addrtaken
の場合のavarinit
とvarkill
の処理ロジックが変更され、AVARDEF
の場合にvarkill
がセットされるようになりました。
-
test/live.go
:- 新しいテストケース
f8
とf9
が追加されました。 f8
はブロックリターン(return g8()
)がライブネス解析に与える影響をテストします。f9
はブロック代入(x := i9
)がライブネス解析に与える影響をテストし、issue 7205
に関連する問題の再現と修正の検証を目的としています。
- 新しいテストケース
コアとなるコードの解説
このコミットの主要な変更は、Goコンパイラのコード生成フェーズ(sgen
)とライブネス解析フェーズ(plive.c
)の連携を強化し、変数の初期化がより複雑な形式で行われる場合でも、ライブネス解析が正確に行われるようにすることです。
src/cmd/{5,6,8}g/cgen.c
の変更
// Record site of definition of ns for liveness analysis.
if(res->op == ONAME && res->class != PEXTERN)
gvardef(res);
// If copying .args, that's all the results, so record definition sites
// for them for the liveness analysis.
if(res->op == ONAME && strcmp(res->sym->name, ".args") == 0)
for(l = curfn->dcl; l != nil; l = l->next)
if(l->n->class == PPARAMOUT)
gvardef(l->n);
gvardef(res)
の追加:sgen
関数は、代入や初期化のコードを生成する役割を担っています。ここで、res
(結果として値が代入されるノード)がONAME
(名前付き変数)であり、かつPEXTERN
(外部変数、つまりグローバル変数など)でない場合に、gvardef(res)
が呼び出されます。これは、ローカル変数やパラメータがブロックコピーやゼロ化によって初期化される際に、その変数が「定義された」ことをライブネス解析に明示的に通知するためのものです。以前は、このような複雑な初期化ではVARDEF
が発行されず、ライブネス解析が変数のライフタイムを誤って判断する可能性がありました。.args
の特殊処理: Go言語では、複数の戻り値を持つ関数は、内部的に.args
という特殊なシンボルを通じて戻り値の領域を扱います。このコードブロックは、.args
シンボルがコピーされる場合(これは関数の戻り値が初期化されることを意味します)、その関数のすべての戻り値パラメータ(PPARAMOUT
)に対してgvardef
を呼び出します。これにより、関数の戻り値もライブネス解析によって正しく追跡されるようになります。
src/cmd/gc/pgen.c
の変更
void
gvardef(Node *n)
{
- if(n == N || !isfat(n->type))
- fatal("gvardef: node is not fat");
+ if(n == N)
+ fatal("gvardef nil");
switch(n->class) {
case PAUTO:
case PPARAM:
gvardef
のエラーチェックの緩和: 以前のgvardef
関数は、引数n
がN
(nil)でないことと、isfat(n->type)
(ノードの型が「fat」、つまり複数ワードを占める大きな型であること)を前提としていました。このコミットでは、!isfat(n->type)
のチェックが削除されました。これは、sgen
関数からのgvardef
呼び出しが、fatでない(単一ワードの)変数に対しても行われるようになったためです。これにより、gvardef
はより汎用的な変数の定義マーカーとして機能するようになります。
src/cmd/gc/plive.c
の変更
static int
isfunny(Node *node)
{
- char *names[] = { ".fp", ".args", "_", nil };
+ char *names[] = { ".fp", ".args", nil };
int i;
if(node->sym != nil && node->sym->name != nil)
isfunny
から_
の削除:isfunny
関数は、ライブネス解析において特殊な振る舞いをするノード(例えば、フレームポインタや関数引数領域)を識別するために使用されていました。以前はブランク識別子_
もこのリストに含まれていましたが、この変更により_
がリストから削除されました。これは、_
が通常の変数と同様にVARDEF
によって定義サイトがマークされるようになったため、isfunny
による特別な除外が不要になったことを示唆しています。
if(info.flags & (LeftRead | LeftWrite | LeftAddr)) {
from = &prog->from;
- if (from->node != nil && !isfunny(from->node) && from->sym != nil) {
- switch(prog->from.node->class & ~PHEAP) {
+ if (from->node != nil && from->sym != nil) {
+ switch(from->node->class & ~PHEAP) {
case PAUTO:
case PPARAM:
case PPARAMOUT:
if(info.flags & (RightRead | RightWrite | RightAddr)) {
to = &prog->to;
- if (to->node != nil && to->sym != nil && !isfunny(to->node)) {
- switch(prog->to.node->class & ~PHEAP) {
+ if (to->node != nil && to->sym != nil) {
+ switch(to->node->class & ~PHEAP) {
case PAUTO:
case PPARAM:
case PPARAMOUT:
isfunny
チェックの削除:progeffects
関数は、個々の命令(Prog
)がライブネスに与える影響を計算します。以前は、命令のfrom
オペランドやto
オペランドがisfunny
である場合、ライブネス解析の対象から除外されていました。この変更により、!isfunny(from->node)
や!isfunny(to->node)
のチェックが削除されました。これは、VARDEF
の生成がより正確になったため、これらの特殊なノードに対してもライブネス解析がより適切に適用されるようになったことを意味します。
if(info.flags & LeftWrite)
- if(from->node != nil && (!isfat(from->node->type) || prog->as == AVARDEF))
+ if(from->node != nil && !isfat(from->node->type))
bvset(varkill, pos);
LeftWrite
処理の変更:LeftWrite
フラグは、命令が左オペランド(from
)に書き込むことを示します。以前は、from->node
がfatでない型であるか、または命令がAVARDEF
(VARDEF
命令)である場合にvarkill
(変数がデッドになるビットベクトル)がセットされていました。この変更により、prog->as == AVARDEF
の条件が削除されました。これは、VARDEF
が変数を定義する命令であり、その命令自体が変数を「デッド」にするわけではないため、この条件が不要になったことを示唆しています。
if(to->node->addrtaken) {
- //if(prog->as == AKILL)
- // bvset(varkill, pos);
- //else
- bvset(avarinit, pos);
+ bvset(avarinit, pos);
+ if(prog->as == AVARDEF)
+ bvset(varkill, pos);
} else {
if(info.flags & (RightRead | RightAddr))
bvset(uevar, pos);
addrtaken
変数の処理の改善:addrtaken
フラグは、変数のアドレスが取得されたことを示します。アドレスが取得された変数は、ポインタを通じてアクセスされる可能性があるため、ライブネス解析で特別な注意が必要です。この変更では、addrtaken
変数がto
オペランドである場合、avarinit
(変数が初期化されたビットベクトル)が常にセットされるようになりました。さらに、命令がAVARDEF
である場合にのみvarkill
がセットされます。これは、VARDEF
が変数を定義する際に、その変数の以前の値を「デッド」にする(つまり、新しい値で上書きする)ことを正確に反映するためのものです。
これらの変更により、Goコンパイラは、ブロックコピー、ブロックゼロ化、および複数回の代入による変数の初期化を、ライブネス解析の観点からより正確に処理できるようになりました。これにより、コンパイラが生成するコードの品質とデバッグ情報の正確性が向上します。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/91b1f7cb15700f39ca63c4e056b41d9b04100e97
- Go CL (Code Review): https://golang.org/cl/63650044
参考にした情報源リンク
- Goコンパイラのソースコード (上記コミットのファイル群)
- コンパイラ理論に関する一般的な知識 (ライブネス解析、ASTなど)
- Go言語の内部構造に関する一般的な知識
- Go issue 7205 (検索したが直接的な情報は見つからず、コミットメッセージとコードから推測)