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

[インデックス 16454] ファイルの概要

このコミットは、Goランタイムのガベージコレクタ(GC)の実行方法に関する重要な変更を導入しています。具体的には、GCを通常のゴルーチンスタックではなく、g0スタック(システムスタック)上で実行するように変更しています。これにより、GC実行中のスタックの変更を防ぎ、ルートセットのスキャン範囲を削減し、将来的なコピー可能なスタックの実装を可能にしています。

コミット

commit 71f061043df89b1c1150b4ed2bf2d70a78b2af0d
Author: Keith Randall <khr@golang.org>
Date:   Fri May 31 20:43:33 2013 -0700

    runtime/gc: Run garbage collector on g0 stack
    instead of regular g stack. We do this so that the g stack
    we're currently running on is no longer changing.  Cuts
    the root set down a bit (g0 stacks are not scanned, and
    we don't need to scan gc's internal state).  Also an
    enabler for copyable stacks.
    
    R=golang-dev, cshapiro, khr, 0xe2.0x9a.0x9b, dvyukov, rsc, iant
    CC=golang-dev
    https://golang.org/cl/9754044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/71f061043df89b1c1150b4ed2bf2d70a78b2af0d

元コミット内容

runtime/gc: Run garbage collector on g0 stack
instead of regular g stack. We do this so that the g stack
we're currently running on is no longer changing.  Cuts
the root set down a bit (g0 stacks are not scanned, and
we don't need to scan gc's internal state).  Also an
enabler for copyable stacks.

変更の背景

このコミットの主な背景には、Goランタイムのガベージコレクション(GC)の効率性と将来的なスタック管理の改善という二つの側面があります。

  1. GCの効率化と正確性の向上:

    • 従来のGC実行では、GC自身が通常のゴルーチンスタック上で動作していました。この場合、GCが自身のスタックをスキャンする必要があり、そのスタックはGCの実行中に変化する可能性がありました。スタックが変化すると、GCが正確にルートセット(GCの起点となる参照)を特定するのが難しくなり、GCの複雑性が増していました。
    • g0スタックは、Goランタイムの内部処理(スケジューラ、システムコールなど)のために使用される特別なスタックであり、通常のゴルーチンスタックとは異なり、GCの対象外です。GCをg0スタックで実行することで、GCが自身のスタックをスキャンする必要がなくなり、ルートセットのサイズを削減できます。これにより、GCの処理が簡素化され、効率と正確性が向上します。
  2. コピー可能なスタックへの移行の準備:

    • Go 1.4以降、Goランタイムはセグメントスタックからコピー可能なスタックへと移行しました。セグメントスタックは、スタックが不足すると新しいセグメントを割り当てて連結する方式でしたが、スタックの成長・縮小に伴うパフォーマンスオーバーヘッドや複雑性がありました。
    • コピー可能なスタックは、スタックが不足するとより大きな連続したメモリブロックを割り当て、古いスタックの内容を新しいブロックにコピーする方式です。この方式は、スタックの動的なサイズ変更をより効率的に行い、パフォーマンスを向上させます。
    • GCをg0スタックで実行することは、このコピー可能なスタックの実装を可能にするための重要なステップでした。GCが自身のスタックをスキャンする必要がなくなることで、スタックのコピー操作がGCの正確性に影響を与えるリスクが低減されます。

これらの背景から、このコミットはGoランタイムのGCとスタック管理の基盤を強化し、将来的なパフォーマンスと安定性の向上に貢献するものでした。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念について理解しておく必要があります。

  1. ゴルーチン (Goroutine):

    • Go言語における軽量な実行単位です。OSのスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。
    • 各ゴルーチンは独自のスタックを持ち、このスタック上でユーザーコードが実行されます。
  2. スタック (Stack):

    • 関数呼び出しの際に、ローカル変数、引数、戻りアドレスなどを格納するために使用されるメモリ領域です。
    • 通常のゴルーチンスタック: 各ゴルーチンに割り当てられるスタックで、ユーザーコードの実行に使用されます。Goのスタックは動的にサイズが変更されます(成長・縮小)。
    • g0スタック (システムスタック): Goランタイムの内部処理(スケジューラ、ガベージコレクション、システムコール、CGO呼び出しなど)のために使用される特別なスタックです。各OSスレッド(M)に一つ関連付けられます。g0スタックは固定サイズであり、GCの対象外です。g0スタック上で実行されるコードは、Goスケジューラによってプリエンプト(横取り)されません。
  3. ガベージコレクション (Garbage Collection - GC):

    • GoのGCは、不要になったメモリを自動的に解放する仕組みです。GoのGCは、主に「マーク&スイープ」アルゴリズムをベースにしており、並行性(concurrent)と低遅延(low-latency)を重視しています。
    • ルートセット (Root Set): GCがメモリをスキャンする際の起点となる参照の集合です。これには、グローバル変数や、実行中のゴルーチンのスタック上のローカル変数やポインタなどが含まれます。GCはルートセットから到達可能なオブジェクトを「生きている」と判断し、到達不可能なオブジェクトを「死んでいる」と判断して解放します。
    • スタックのスキャン: GCのマークフェーズにおいて、すべてのゴルーチンのスタックがスキャンされ、ヒープ上のオブジェクトへのポインタが特定されます。これにより、GCはヒープ上のオブジェクトグラフを辿り、到達可能なオブジェクトをマークします。
    • Stop-The-World (STW): GCの特定のフェーズ(マーク開始時やマーク終了時など)で、すべてのゴルーチンの実行を一時的に停止させる期間です。GoのGCはSTW時間を最小限に抑えるように設計されていますが、完全にゼロにすることはできません。STW中は、GCがメモリの状態を安定させて、正確な処理を行うことができます。
  4. runtime.mcallreflect.Call:

    • runtime.mcall: Goランタイムの内部関数であり、主にアセンブリで実装されています。現在のゴルーチンスタックからg0スタックへ実行コンテキストを切り替えるために使用されます。ユーザーコードから直接呼び出すことはできません。スケジューラやGCのような低レベルのランタイム操作で利用されます。
    • reflect.Call: reflectパッケージが提供する高レベルなメソッドで、Goプログラムが実行時に動的に関数やメソッドを呼び出すことを可能にします。これはリフレクション機能の一部であり、runtime.mcallとは異なり、ユーザーが利用できるAPIです。
  5. コピー可能なスタック (Copyable Stacks):

    • Go 1.4で導入されたスタック管理の仕組みです。ゴルーチンのスタックが不足した場合、より大きな連続したメモリ領域を割り当て、既存のスタックの内容をそこにコピーします。これにより、スタックの動的なサイズ変更が効率的に行われ、パフォーマンスが向上します。このコミットは、このコピー可能なスタックへの移行を容易にするための前提条件の一つでした。

技術的詳細

このコミットの技術的な核心は、ガベージコレクタ(GC)の実行コンテキストを通常のゴルーチンスタックからg0スタックへ移行することにあります。

  1. GC実行コンテキストの変更:

    • 変更前は、runtime.gc関数は通常のゴルーチンスタック上で実行されていました。これは、reflect.call(コミットメッセージではreflect·callと表記されているが、これは内部的な表記であり、実際のreflect.Callとは異なる低レベルな呼び出しメカニズムを指す)を通じて呼び出されていました。
    • 変更後は、runtime.gcは直接runtime.mcallを通じてmgc関数から呼び出されるようになりました。runtime.mcallは、現在のゴルーチンスタックからg0スタックへ実行コンテキストを切り替えるための低レベルなプリミティブです。これにより、gc関数はg0スタック上で実行されることになります。
  2. スタックスキャンの最適化:

    • ルートセットの削減: GCがg0スタック上で実行されることで、GC自身のスタック(g0スタック)はGCの対象外となります。これにより、GCがスキャンする必要のあるルートセットのサイズが削減されます。従来の方式では、GCが自身のゴルーチンスタック上で動作していたため、そのスタックもスキャン対象となり、GCの内部状態もスキャンする必要がありました。g0スタックはGCによってスキャンされないため、このオーバーヘッドがなくなります。
    • スタックの安定性: GCがg0スタック上で実行されている間、GCをトリガーした元のゴルーチンスタックは変化しません。これは、GCがスタックをスキャンする際に、その内容が途中で変更されることによる複雑性や潜在的なバグのリスクを排除します。GCは静的なスタックの状態を前提に処理を進めることができるため、より正確で堅牢なGCが可能になります。
  3. コピー可能なスタックへの対応:

    • Go 1.4で導入されたコピー可能なスタックは、スタックの成長・縮小時にスタックの内容を新しいメモリ領域にコピーします。このコピー操作中にGCがスタックをスキャンすると、ポインタの整合性の問題が発生する可能性があります。
    • GCをg0スタックで実行することで、GCが自身のスタックをコピーする必要がなくなります。また、GCが実行されている間、他のゴルーチンのスタックがコピーされる可能性はありますが、GC自身が安定したg0スタック上で動作しているため、この複雑なシナリオをより安全に処理できるようになります。これは、コピー可能なスタックをGoランタイムに統合するための重要な前提条件でした。
  4. runtime.throwの追加:

    • addstackroots関数において、gp == g(自身のスタックをスキャンしようとしている場合)やmp->helpgc(GCヘルパーのスタックをスキャンしようとしている場合)にruntime.throwが追加されています。これは、これらのシナリオがGCのg0スタック上での実行と矛盾するため、不正な状態を検出するための防御的なチェックです。特に、GCヘルパーのスタックはアクティブに使用されており、GCの対象外であるため、スキャンすべきではありません。

この変更は、GoランタイムのGCの内部動作を根本的に改善し、将来のGoの進化(特にスタック管理)のための強固な基盤を築きました。

コアとなるコードの変更箇所

このコミットで変更されたファイルは src/pkg/runtime/mgc0.c のみです。主要な変更点は以下の通りです。

  1. addstackroots 関数の変更:

    • 変更前:
      if(gp == g) {
          // Scanning our own stack: start at &gp.
          sp = runtime·getcallersp(&gp);
          pc = runtime·getcallerpc(&gp);
      } else if((mp = gp->m) != nil && mp->helpgc) {
          // gchelper's stack is in active use and has no interesting pointers.
          return;
      } else if(gp->gcstack != (uintptr)nil) {
      
    • 変更後:
      if(gp == g)
          runtime·throw("can't scan our own stack");
      if((mp = gp->m) != nil && mp->helpgc)
          runtime·throw("can't scan gchelper stack");
      if(gp->gcstack != (uintptr)nil) {
      
      自身のスタックやGCヘルパーのスタックをスキャンしようとした場合に、runtime·throw を呼び出すようになりました。これは、GCがg0スタックで実行されるようになったため、これらのスタックをスキャンする必要がなくなった(またはスキャンすべきではない)ことを示しています。
  2. gc 関数の引数構造体の変更:

    • struct gc_args のコメントが reflect·call から runtime·mcall に変更されました。
    • int32 force;int64 start_time; に変更されました。これは、GCの開始時間を引数として渡すように変更されたことを示しています。
  3. runtime·gc 関数の大幅な変更:

    • GCの実行ロジックが大きく変更されました。
    • 変更前は、reflect·call を使用して gc 関数を呼び出していました。
    • 変更後、runtime·semacquireruntime·semrelease によるワールドセマフォの取得と解放、runtime·stoptheworld()runtime·starttheworld() によるSTWの制御が明示的に追加されました。
    • GCの実行が g0 スタック上で行われるように、runtime·mcall(mgc) が導入されました。
    • gctrace の設定に応じてGCを1回または2回実行するループが追加されました。
    • ファイナライザの処理がGCの実行後に行われるように移動されました。
  4. mgc 関数の新規追加:

    • static void mgc(G *gp) 関数が新しく追加されました。この関数は runtime·mcall から呼び出され、g0 スタック上で gc 関数を実行するためのラッパーとして機能します。
    • gp->status の変更、gc(gp->param) の呼び出し、runtime·gogo による元のゴルーチンへの復帰処理が含まれています。
  5. gc 関数の変更:

    • runtime·gc 関数から移動されたSTW関連のコードが削除されました。
    • t0 = args->start_time; が追加され、GCの開始時間が引数から取得されるようになりました。
  6. gchelperstart 関数の変更:

    • if(g != m->g0) のチェックが追加され、GCヘルパーがg0スタック上で実行されていない場合にruntime·throwを呼び出すようになりました。

これらの変更は、GCの実行コンテキストをg0スタックに移行し、GCのライフサイクルとスタック管理をより厳密に制御するためのものです。

コアとなるコードの解説

このコミットのコアとなる変更は、runtime.gc 関数と新しく追加された mgc 関数、そしてそれらが g0 スタック上でガベージコレクションを実行するためにどのように連携するかです。

  1. runtime.gc 関数の役割:

    • runtime.gc は、Goプログラムからガベージコレクションをトリガーするエントリポイントです。
    • 変更前は、reflect·call を使って gc 関数を呼び出していましたが、これはGCが通常のゴルーチンスタック上で実行されることを意味していました。
    • 変更後、runtime.gc はGCを実行する前に、まず runtime·semacquire(&runtime·worldsema) でワールドセマフォを取得し、runtime·stoptheworld() を呼び出してすべてのゴルーチンを停止させます。これは、GCが安全にメモリをスキャンできるようにするためです。
    • 最も重要な変更は、GCの実際の処理を g0 スタックに切り替えて実行する部分です。
      if(g == m->g0) {
          // already on g0
          gc(&a);
      } else {
          // switch to g0, call gc(&a), then switch back
          g->param = &a;
          runtime·mcall(mgc);
      }
      
      • もし現在すでに g0 スタック上にいる場合(これは稀なケースですが、ランタイムの内部処理中にGCがトリガーされた場合など)、直接 gc(&a) を呼び出します。
      • 通常のゴルーチンスタック上にいる場合、g->param = &a; でGCの引数(start_time)を現在のゴルーチン(g)の param フィールドに保存します。
      • そして runtime·mcall(mgc); を呼び出します。runtime·mcall は、現在のゴルーチンスタックから、現在のOSスレッド(m)に関連付けられた g0 スタックへ実行コンテキストを切り替える低レベルな関数です。切り替え後、mgc 関数が g0 スタック上で実行されます。
  2. mgc 関数の役割:

    • mgc 関数は、runtime·mcall によって g0 スタック上で実行されるように設計されたラッパー関数です。
    • static void
      mgc(G *gp)
      {
          gp->status = Grunnable;
          gc(gp->param);
          gp->status = Grunning;
          gp->param = nil;
          runtime·gogo(&gp->sched, 0);
      }
      
    • gp->status = Grunnable;mgc が呼び出された時点では、gpruntime.gc を呼び出した元のゴルーチンを指します。このゴルーチンのステータスを Grunnable に設定することで、スケジューラがこのゴルーチンを再度実行可能と認識できるようにします。
    • gc(gp->param);:ここで、実際のガベージコレクション処理を行う gc 関数が呼び出されます。引数は、runtime.gcg->param に保存しておいたものです。この gc 関数は g0 スタック上で実行されます。
    • gp->status = Grunning;gc 関数が完了した後、元のゴルーチンのステータスを Grunning に戻します。
    • gp->param = nil;:使用済みの引数をクリアします。
    • runtime·gogo(&gp->sched, 0);:これは、g0 スタックから元のゴルーチンスタックへ実行コンテキストを切り替えるための低レベルな関数です。これにより、GCが完了した後、元のゴルーチンが中断された場所から実行を再開できるようになります。
  3. gc 関数の変更:

    • gc 関数自体は、GCの実際のマーク、スイープなどのロジックを含んでいます。
    • このコミットでは、runtime.gc からSTW関連のコードが移動されたため、gc 関数からは runtime·semacquireruntime·stoptheworld などの呼び出しが削除されました。
    • t0 = args->start_time; が追加され、GCの開始時間が引数から渡されるようになりました。

この一連の変更により、Goのガベージコレクタは、より安定したg0スタック上で実行されるようになり、GCの正確性と効率が向上しました。また、これは将来のスタック管理の改善(コピー可能なスタック)のための重要な基盤となりました。

関連リンク

参考にした情報源リンク