Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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コンパイラがエスケープ解析の後に、特定のサイズのしきい値を超えるローカル変数を強制的にヒープに移動させる新しいフェーズを追加したことです。

  1. MaxStackVarSize の導入: src/cmd/gc/go.hMaxStackVarSize という定数が導入されました。この定数は 10 * 1024 * 1024、つまり10MBに設定されています。これは、スタックに割り当てられる変数の最大サイズを定義します。このサイズを超える変数は、ヒープに移動される対象となります。

  2. movelarge 関数の追加: src/cmd/gc/pgen.cmovelarge 関数と movelargefn 関数が追加されました。

    • movelarge 関数は、トップレベルの関数リストをイテレートし、各関数に対して movelargefn を呼び出します。
    • movelargefn 関数は、特定の関数のローカル変数(PAUTO クラスの変数)を走査します。
    • 変数の型が有効であり(n->type != T)、かつその変数のサイズ(n->type->width)が MaxStackVarSize を超える場合、addrescapes(n) 関数が呼び出されます。
  3. lex.c での movelarge の呼び出し: src/cmd/gc/lex.cmain 関数内で、既存のエスケープ解析フェーズ(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*); // 新しく追加された関数宣言

MaxStackVarSizeenum の中に定義され、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" というコメントは、コンパイラがこの変数をヒープに移動したことを示すエラーメッセージ(実際には警告メッセージ)を出力することをテストしています。ymake で作成されるため、元々ヒープに割り当てられます。

関連リンク

このコミットは Go issue #6077 を修正すると記載されていますが、現在のGitHubリポジトリでは該当するIssueを見つけることができませんでした。これは、Issue番号が変更されたか、非常に古いIssueであるため公開されていない可能性があります。

参考にした情報源リンク