[インデックス 17095] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)において、非常に大きなスタック変数を自動的にヒープに移動させる機能を追加するものです。具体的には、10MBを超えるサイズの変数がスタックに割り当てられようとした場合、コンパイラがその変数をヒープに「エスケープ」させるように変更されます。これにより、巨大な配列などをスタックに確保しようとした際に発生する、スタックオーバーフローや非効率なメモリ使用といった問題が回避されます。
コミット
- コミットハッシュ:
1f4d58ad5d6cdb03cd4f9d8062a711db9fe137bd
- Author: Russ Cox rsc@golang.org
- Date: Thu Aug 8 13:46:30 2013 -0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1f4d58ad5d6cdb03cd4f9d8062a711db9fe137bd
元コミット内容
cmd/gc: move large stack variables to heap
Individual variables bigger than 10 MB are now
moved to the heap, as if they had escaped on
their own.
This avoids ridiculous stacks for programs that
do things like
x := [1<<30]byte{}
... use x ...
If 10 MB is too small, we can raise the limit.
Fixes #6077.
R=ken2
CC=golang-dev
https://golang.org/cl/12650045
変更の背景
Go言語では、変数は通常、その生存期間や使用方法に基づいてスタックまたはヒープに割り当てられます。スタックは高速なメモリ領域であり、関数の呼び出しやローカル変数の管理に用いられます。しかし、スタックのサイズには限りがあり、非常に大きなデータ構造(例えば、[1<<30]byte{}
のような1GBのバイト配列)をローカル変数として宣言すると、スタックがすぐに枯渇し、スタックオーバーフローを引き起こす可能性があります。
この問題は、特に大きな固定サイズの配列を宣言する際に顕著でした。Goコンパイラは、通常のエスケープ解析(変数が関数のスコープ外で参照される可能性があるかを判断するプロセス)によって、ヒープへの割り当てが必要な変数を特定します。しかし、このコミット以前は、エスケープしない巨大なローカル変数はスタックに割り当てられようとし、結果として「ridiculous stacks」(途方もないスタックサイズ)という問題を引き起こしていました。
このコミットは、このような状況を改善し、Goプログラムがより堅牢に、そして効率的にメモリを使用できるようにするために導入されました。
前提知識の解説
スタックとヒープ
- スタック (Stack): プログラムの実行中に一時的にデータを格納するメモリ領域です。関数の呼び出し、ローカル変数の割り当て、関数の戻りアドレスなどがスタックに積まれます(プッシュ)そして取り出されます(ポップ)。スタックはLIFO(Last-In, First-Out)構造で、非常に高速なアクセスが可能です。しかし、サイズが限られており、通常は数MB程度です。
- ヒープ (Heap): プログラムの実行中に動的にメモリを割り当てる領域です。スタックとは異なり、ヒープに割り当てられたデータは、そのデータが不要になるまで(ガベージコレクションによって回収されるまで)メモリ上に存在し続けます。ヒープはスタックよりもアクセスが遅いですが、より大きなメモリ領域を確保できます。
エスケープ解析 (Escape Analysis)
Goコンパイラの重要な最適化の一つにエスケープ解析があります。これは、コンパイラが変数がスタックに割り当てられるべきか、それともヒープに割り当てられるべきかを決定するプロセスです。
- スタック割り当て: 変数がその関数スコープ内でのみ使用され、関数の終了後には不要になる場合、コンパイラは通常その変数をスタックに割り当てます。
- ヒープ割り当て: 変数が関数のスコープ外で参照される可能性がある場合(例: ポインタが関数から返される、グローバル変数に代入されるなど)、コンパイラはその変数をヒープに割り当てます。これは「エスケープする」と呼ばれます。
エスケープ解析は、メモリの効率的な利用とガベージコレクションの負荷軽減に貢献します。しかし、このコミット以前は、エスケープしないが非常に大きなローカル変数は、エスケープ解析の対象外であったため、スタックに割り当てられようとしていました。
技術的詳細
このコミットの核心は、Goコンパイラがエスケープ解析の後に、特定のサイズのしきい値を超えるローカル変数を強制的にヒープに移動させる新しいフェーズを追加したことです。
-
MaxStackVarSize
の導入:src/cmd/gc/go.h
にMaxStackVarSize
という定数が導入されました。この定数は10 * 1024 * 1024
、つまり10MBに設定されています。これは、スタックに割り当てられる変数の最大サイズを定義します。このサイズを超える変数は、ヒープに移動される対象となります。 -
movelarge
関数の追加:src/cmd/gc/pgen.c
にmovelarge
関数とmovelargefn
関数が追加されました。movelarge
関数は、トップレベルの関数リストをイテレートし、各関数に対してmovelargefn
を呼び出します。movelargefn
関数は、特定の関数のローカル変数(PAUTO
クラスの変数)を走査します。- 変数の型が有効であり(
n->type != T
)、かつその変数のサイズ(n->type->width
)がMaxStackVarSize
を超える場合、addrescapes(n)
関数が呼び出されます。
-
lex.c
でのmovelarge
の呼び出し:src/cmd/gc/lex.c
のmain
関数内で、既存のエスケープ解析フェーズ(escapes(xtop)
)の直後にmovelarge(xtop)
が呼び出されるようになりました。これにより、エスケープ解析によってヒープに移動されなかった大きなローカル変数も、この新しいフェーズでヒープに移動されるようになります。
この変更により、コンパイラは、エスケープ解析の結果に関わらず、サイズが大きすぎるローカル変数を自動的にヒープに割り当てることで、スタックオーバーフローのリスクを軽減し、より安定したプログラム実行を可能にします。
コアとなるコードの変更箇所
このコミットによって以下のファイルが変更されました。
src/cmd/gc/go.h
: Goコンパイラのグローバルな定義や定数が含まれるヘッダーファイル。MaxStackVarSize
が追加されました。src/cmd/gc/lex.c
: Goコンパイラの字句解析(lexical analysis)およびコンパイルの主要なフェーズを制御するファイル。movelarge
関数の呼び出しが追加されました。src/cmd/gc/pgen.c
: Goコンパイラのコード生成(program generation)に関連するファイル。movelarge
およびmovelargefn
関数が追加され、大きなスタック変数をヒープに移動させるロジックが実装されました。test/escape5.go
: エスケープ解析のテストケース。大きな配列がヒープに移動されることを確認する新しいテストケースf10
が追加されました。test/fixedbugs/bug385_64.go
: 以前のスタックフレームが大きすぎるエラーをテストするファイル。このコミットによって、スタックオーバーフローを引き起こす可能性のあるコードが大幅に拡張され、新しい挙動がテストされています。
コアとなるコードの解説
src/cmd/gc/go.h
enum
{
// ... 既存の定義 ...
BADWIDTH = -1000000000,
MaxStackVarSize = 10*1024*1024, // 10MB
};
// ... 既存の関数宣言 ...
void movelarge(NodeList*); // 新しく追加された関数宣言
MaxStackVarSize
が enum
の中に定義され、10MBというしきい値が設定されました。また、movelarge
関数のプロトタイプ宣言が追加されています。
src/cmd/gc/lex.c
main(int argc, char *argv[])
{
// ... 既存のコンパイルフェーズ ...
// Phase 5: Escape analysis.
if(!debug['N'])
escapes(xtop);
// Escape analysis moved escaped values off stack.
// Move large values off stack too.
movelarge(xtop); // ここでmovelargeが呼び出される
// Phase 6: Compile top level functions.
// ...
}
main
関数内で、エスケープ解析(escapes(xtop)
)の直後に movelarge(xtop)
が呼び出されています。これにより、エスケープ解析でヒープに移動されなかった変数の中で、MaxStackVarSize
を超えるものがヒープに移動されるようになります。
src/cmd/gc/pgen.c
static void movelargefn(Node*); // movelargefnのプロトタイプ宣言
void
movelarge(NodeList *l)
{
for(; l; l=l->next)
if(l->n->op == ODCLFUNC) // 関数宣言ノードの場合
movelargefn(l->n); // movelargefnを呼び出す
}
static void
movelargefn(Node *fn)
{
NodeList *l;
Node *n;
for(l=fn->dcl; l != nil; l=l->next) { // 関数の宣言リストをイテレート
n = l->n;
// ローカル変数 (PAUTO) で、型が有効かつサイズがMaxStackVarSizeを超える場合
if(n->class == PAUTO && n->type != T && n->type->width > MaxStackVarSize)
addrescapes(n); // ヒープにエスケープさせる
}
}
movelarge
関数は、コンパイル対象のトップレベルのノードリストを受け取り、その中の関数宣言ノード(ODCLFUNC
)に対して movelargefn
を呼び出します。
movelargefn
関数は、引数として受け取った関数の宣言リスト(ローカル変数や引数など)を走査します。もし変数が自動変数(PAUTO
、つまりローカル変数)であり、その型が定義されており、かつそのサイズ(n->type->width
)が MaxStackVarSize
を超える場合、addrescapes(n)
が呼び出されます。addrescapes
は、その変数をヒープに割り当てるようにマークするGoコンパイラの内部関数です。
test/escape5.go
func f10() {
// These don't escape but are too big for the stack
var x [1<<30]byte // ERROR "moved to heap: x"
var y = make([]byte, 1<<30) // ERROR "does not escape"
_ = x[0] + y[0]
}
f10
関数は、1GBのバイト配列 x
を宣言しています。この配列はエスケープ解析ではヒープに移動されないはずですが、MaxStackVarSize
を超えるため、コンパイラによってヒープに移動されることが期待されます。ERROR "moved to heap: x"
というコメントは、コンパイラがこの変数をヒープに移動したことを示すエラーメッセージ(実際には警告メッセージ)を出力することをテストしています。y
は make
で作成されるため、元々ヒープに割り当てられます。
関連リンク
このコミットは Go issue #6077 を修正すると記載されていますが、現在のGitHubリポジトリでは該当するIssueを見つけることができませんでした。これは、Issue番号が変更されたか、非常に古いIssueであるため公開されていない可能性があります。
参考にした情報源リンク
- Go言語のコミット履歴: https://github.com/golang/go/commit/1f4d58ad5d6cdb03cd4f9d8062a711db9fe137bd
- Goコンパイラのソースコード (当時のバージョンに基づく)
- Go言語におけるスタックとヒープ、エスケープ解析に関する一般的な知識