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

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

このコミットは、Goランタイムにおけるスタック縮小のタイミングとスタックコピーの再有効化に関する変更を含んでいます。具体的には、src/pkg/runtime/mgc0.csrc/pkg/runtime/proc.c の2つのファイルが変更されています。

コミット

commit e9445547b6d04edc358ae60e2eb29db88fd67654
Author: Keith Randall <khr@golang.org>
Date:   Thu Feb 27 14:20:15 2014 -0800

    runtime: move stack shrinking until after sweepgen is incremented.
    
    Before GC, we flush all the per-P allocation caches.  Doing
    stack shrinking mid-GC causes these caches to fill up.  At the
    end of gc, the sweepgen is incremented which causes all of the
    data in these caches to be in a bad state (cached but not yet
    swept).
    
    Move the stack shrinking until after sweepgen is incremented,
    so any caching that happens as part of shrinking is done with
    already-swept data.
    
    Reenable stack copying.
    
    LGTM=bradfitz
    R=golang-codereviews, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/69620043

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

https://github.com/golang/go/commit/e9445547b6d04edc358ae60e2eb29db88fd67654

元コミット内容

Goランタイムにおいて、スタック縮小の処理をsweepgenがインクリメントされた後に行うように変更しました。

GC(ガベージコレクション)の前に、全てのP(プロセッサ)ごとのアロケーションキャッシュがフラッシュされます。GCの途中でスタック縮小を行うと、これらのキャッシュが再び埋まってしまいます。GCの終了時にsweepgenがインクリメントされると、これらのキャッシュ内のデータは不正な状態(キャッシュされているがまだスイープされていない)になります。

この問題を解決するため、スタック縮小をsweepgenがインクリメントされた後に行うように移動しました。これにより、縮小処理中に発生する可能性のあるキャッシュ操作が、既にスイープされたデータに対して行われるようになります。

また、スタックコピー機能を再有効化しました。

変更の背景

このコミットの主な背景は、Goランタイムのガベージコレクション(GC)とスタック管理の間の相互作用における潜在的なデータ不整合の問題を解決することにあります。

GoのGCは、メモリの再利用を効率的に行うために、複数のフェーズで動作します。このコミットが対象としている問題は、GCの「スイープ」フェーズと、Goルーチンのスタックを動的に調整する「スタック縮小」機能のタイミングに関するものです。

具体的には、以下の問題がありました。

  1. GC中のキャッシュの再充填: GCが開始される前には、各P(プロセッサ、GoランタイムのスケジューラがGoルーチンを実行するために使用する論理的なCPU)に紐付けられたアロケーションキャッシュ(per-P allocation caches)がフラッシュされます。これは、GCが正確にメモリをスキャンし、到達可能なオブジェクトを特定するために重要です。しかし、以前の実装では、GCの途中でスタック縮小が行われる可能性がありました。スタック縮小は、Goルーチンが使用するスタック領域を最適化するプロセスであり、この過程で新たなメモリ割り当てが発生し、per-P allocation cachesが再び埋まってしまうことがありました。

  2. sweepgenとキャッシュデータの不整合: GCのスイープフェーズが完了すると、sweepgenという世代カウンタがインクリメントされます。このsweepgenは、メモリブロックがどのGCサイクルでスイープされたかを示すために使用されます。問題は、GC中にスタック縮小によってキャッシュにデータが再充填された場合、そのデータがsweepgenのインクリメント前にキャッシュされてしまうことでした。これにより、キャッシュ内のデータは「まだスイープされていない」と見なされるにもかかわらず、実際にはGCサイクルが終了し、sweepgenが更新された後の状態と整合性が取れない「不正な状態」になっていました。これは、後続のメモリ割り当てやGCサイクルで問題を引き起こす可能性がありました。

このコミットは、スタック縮小のタイミングをsweepgenがインクリメントされた後に移動することで、この不整合を解消しようとしています。これにより、スタック縮小中に発生する可能性のあるキャッシュ操作が、既にスイープされ、sweepgenによって適切にマークされたデータに対して行われるようになり、データの一貫性が保たれます。

また、コメントアウトされていたスタックコピー機能の再有効化も行われています。これは、スタック縮小のタイミング変更と合わせて、Goルーチンのスタック管理全体を改善する一環と考えられます。

前提知識の解説

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

1. Goのガベージコレクション (GC)

GoのGCは、並行・非同期に動作するトレース型GCです。主な目的は、プログラムが参照しなくなったメモリを自動的に解放し、メモリリークを防ぐことです。GoのGCは、以下のフェーズを繰り返します。

  • マークフェーズ (Mark Phase): GCルート(グローバル変数、スタック上の変数など)から到達可能な全てのオブジェクトをマークします。このフェーズは、アプリケーションの実行と並行して行われます(並行マーク)。
  • マークターミネーションフェーズ (Mark Termination Phase): 並行マークフェーズの終了を調整し、全てのGoルーチンがマークフェーズを完了したことを確認します。このフェーズはSTW (Stop The World) が発生する可能性がありますが、非常に短時間です。
  • スイープフェーズ (Sweep Phase): マークされなかった(到達不可能と判断された)オブジェクトが占めていたメモリを解放し、再利用可能な状態にします。このフェーズも並行して行われます(並行スイープ)。

2. sweepgen

sweepgenは、GoランタイムのGCにおいて、メモリブロックがどのGCサイクルでスイープされたかを示す世代カウンタです。GCのスイープフェーズが完了し、新しいGCサイクルが開始される準備が整うと、sweepgenの値がインクリメントされます。

  • sweepgenは、メモリブロックが「現在のGCサイクルでスイープ済みである」か「まだスイープされていない(または前のGCサイクルでスイープされた)」かを区別するために使用されます。
  • 特に、アロケーションキャッシュのような高速パスでメモリを割り当てる際に、sweepgenの値とメモリブロックのsweepgen値を比較することで、そのブロックが再利用可能かどうかを判断します。

3. per-P allocation caches (Pごとのアロケーションキャッシュ)

Goランタイムは、Goルーチンを効率的にスケジューリングするために、P(プロセッサ)という抽象概念を使用します。各Pは、Goルーチンを実行するためのコンテキストを提供し、ローカルなリソースを保持します。

per-P allocation cachesは、各Pが持つ小さなメモリキャッシュです。Goルーチンが小さなオブジェクトを割り当てる際、グローバルなヒープロックを取得するオーバーヘッドを避けるために、まずこのPごとのキャッシュからメモリを割り当てようとします。キャッシュが空の場合や、大きなオブジェクトを割り当てる場合は、グローバルなヒープからメモリが割り当てられます。

GCが開始される前には、これらのPごとのキャッシュはフラッシュされます。これは、キャッシュ内のオブジェクトもGCの対象となるため、GCが正確に全ての到達可能なオブジェクトをスキャンできるようにするためです。

4. スタック縮小 (stack shrinking)

Goルーチンは、必要に応じてスタックサイズを動的に増減させることができます。スタック縮小は、Goルーチンが割り当てられたスタック領域の大部分を使用していない場合に、そのスタックをより小さな領域にコピーし、元の大きな領域を解放するプロセスです。これにより、メモリ使用量を最適化し、メモリの断片化を減らすことができます。

スタック縮小は、Goルーチンがシステムコールから戻った後や、GCの特定のフェーズでトリガーされることがあります。

5. スタックコピー (stack copying)

Goルーチンのスタックは、必要に応じて新しいメモリ領域にコピーされることがあります。これは、スタックが拡張される場合(スタックオーバーフローを避けるため)や、スタックが縮小される場合(メモリを解放するため)に発生します。スタックコピーは、Goの効率的なスタック管理の重要な側面です。

技術的詳細

このコミットの技術的な核心は、Goランタイムのガベージコレクション(GC)とスタック管理の間の同期と整合性を改善することにあります。

問題点と解決策

前述の「変更の背景」で述べたように、以前の実装では、GCのマークフェーズ中にスタック縮小が行われる可能性がありました。スタック縮小は、Goルーチンのスタックを新しい、より小さなメモリ領域にコピーするプロセスです。このコピー操作中に、新しいメモリ割り当てが発生し、その割り当てがper-P allocation cachesに格納される可能性がありました。

問題は、これらのキャッシュがGCの開始時にフラッシュされるにもかかわらず、GC中にスタック縮小によって再び埋まってしまうことでした。さらに深刻なのは、GCのスイープフェーズが完了し、sweepgenがインクリメントされる前に、これらのキャッシュにデータが書き込まれてしまうことでした。これにより、キャッシュ内のデータは、sweepgenの観点から見ると「まだスイープされていない」状態であるにもかかわらず、実際にはGCサイクルが終了し、sweepgenが更新された後のメモリ状態と整合性が取れない状態になっていました。これは、後続のメモリ割り当てやGCの正確性に影響を与える可能性がありました。

このコミットは、この問題を解決するために、スタック縮小の呼び出しタイミングをGCの終了後、具体的にはsweepgenがインクリメントされた後に移動しました。

コード変更の分析

  1. src/pkg/runtime/mgc0.c の変更:

    • 以前は、addstackroots関数内でGoルーチンのスタックをスキャンする前にruntime·shrinkstack(gp);が呼び出されていました。addstackrootsはGCのマークフェーズ中にGoルーチンのスタックをスキャンしてルートを特定する役割を担っています。この位置では、スタック縮小がGCの途中で行われることになり、前述の問題を引き起こしていました。
    • このコミットでは、addstackrootsからのruntime·shrinkstack(gp);の呼び出しを削除しました。
    • 代わりに、gc関数の最後、sweepgenがインクリメントされた後(runtime·MProf_GC();の直前)に、全てのGoルーチンに対してループでruntime·shrinkstack(runtime·allg[i]);を呼び出すように変更しました。
    • この新しい位置では、GCのスイープフェーズが完了し、sweepgenが更新された後であるため、スタック縮小中に発生する可能性のあるメモリ割り当ては、既にスイープされた(つまり、有効なsweepgen値を持つ)メモリ領域に対して行われることになります。これにより、キャッシュ内のデータとGCの状態との間の整合性が保たれます。
  2. src/pkg/runtime/proc.c の変更:

    • runtime·schedinit関数内で、runtime·copystack = false; // TODO: remove という行が削除されました。これは、以前にスタックコピー機能を一時的に無効にしていたデバッグまたは開発用のコードであったと考えられます。
    • この行の削除により、スタックコピー機能がデフォルトで有効になり、runtime·copystack = runtime·precisestack; の設定が適用されるようになります。これは、スタック縮小のタイミング変更と合わせて、Goルーチンのスタック管理の全体的な改善と安定化を目指していることを示唆しています。

影響

この変更により、GoランタイムのGCの正確性と効率性が向上します。特に、GC中のデータ不整合のリスクが低減され、メモリ割り当てのパスがより堅牢になります。また、スタックコピー機能の再有効化は、Goルーチンのスタック管理の柔軟性と最適化に貢献します。

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

src/pkg/runtime/mgc0.c

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -1601,9 +1601,6 @@ addstackroots(G *gp, Workbuf **wbufp)
 	if((mp = gp->m) != nil && mp->helpgc)
 		runtime·throw("can't scan gchelper stack");
 
-	// Shrink stack if not much of it is being used.
-	runtime·shrinkstack(gp);
-
 	if(gp->syscallstack != (uintptr)nil) {
 		// Scanning another goroutine that is about to enter or might
 		// have just exited a system call. It may be executing code such
@@ -2426,6 +2423,11 @@ gc(struct gc_args *args)
 		tgcstats.npausesweep++;
 	}
 
+	// Shrink a stack if not much of it is being used.
+	// TODO: do in a parfor
+	for(i = 0; i < runtime·allglen; i++)
+		runtime·shrinkstack(runtime·allg[i]);
+
 	runtime·MProf_GC();
 }
 

src/pkg/runtime/proc.c

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -174,7 +174,6 @@ runtime·schedinit(void)
 	procresize(procs);
 
 	runtime·copystack = runtime·precisestack;
-	runtime·copystack = false; // TODO: remove
 	p = runtime·getenv("GOCOPYSTACK");
 	if(p != nil && !runtime·strcmp(p, (byte*)"0"))
 		runtime·copystack = false;

コアとなるコードの解説

src/pkg/runtime/mgc0.c の変更点

  • addstackroots 関数からの runtime·shrinkstack の削除:

    • 元のコードでは、addstackroots 関数(GCのマークフェーズ中にGoルーチンのスタックをスキャンしてGCルートを特定する役割を持つ)の冒頭で、各Goルーチン(gp)に対して runtime·shrinkstack(gp); が呼び出されていました。
    • この変更により、GCのマークフェーズ中にスタック縮小が行われることがなくなりました。これにより、GC中にper-P allocation cachesが不適切に再充填される問題が回避されます。
  • gc 関数への runtime·shrinkstack の移動とループでの適用:

    • gc 関数(GoのGCのメインエントリポイント)の末尾、具体的にはスイープフェーズが完了し、sweepgenがインクリメントされた後(runtime·MProf_GC(); の直前)に、スタック縮小のロジックが移動されました。
    • 新しいコードでは、for(i = 0; i < runtime·allglen; i++) runtime·shrinkstack(runtime·allg[i]); というループが追加されています。これは、システム内の全てのGoルーチン(runtime·allg配列に格納されている)に対して、個別にスタック縮小(runtime·shrinkstack)を試みることを意味します。
    • この変更により、スタック縮小はGCサイクルが完全に終了し、メモリの状態が安定した後に実行されるようになります。これにより、スタック縮小中に発生する可能性のあるメモリ割り当てが、既にスイープされ、最新のsweepgen値を持つメモリ領域に対して行われることが保証され、データの一貫性が保たれます。
    • コメント // TODO: do in a parfor は、将来的にこのスタック縮小処理を並行化する可能性があることを示唆しています。

src/pkg/runtime/proc.c の変更点

  • runtime·copystack = false; // TODO: remove の削除:
    • runtime·schedinit 関数(Goランタイムの初期化処理)内にあった runtime·copystack = false; という行が削除されました。この行は、以前にスタックコピー機能を一時的に無効にするために使用されていたデバッグまたは開発用のコードであったと考えられます。
    • この行の削除により、runtime·copystack = runtime·precisestack; という直前の行の設定が有効になります。これは、Goランタイムがスタックコピーをデフォルトで有効にし、より正確なスタック管理を行うことを意味します。この変更は、スタック縮小のタイミング変更と合わせて、Goルーチンのスタック管理の全体的な堅牢性と最適化に貢献します。

これらの変更は、Goランタイムのメモリ管理、特にGCとスタック管理の間の相互作用をより堅牢で効率的にすることを目的としています。

関連リンク

参考にした情報源リンク

  • Goのガベージコレクションに関する公式ドキュメントやブログ記事 (一般的なGo GCの仕組み理解のため)
  • Goランタイムのソースコード (特にsrc/runtimeディレクトリ内のファイル)
  • Goのスタック管理に関する議論や設計ドキュメント (スタック縮小やコピーの理解のため)
  • GoのP(プロセッサ)とスケジューラに関する情報 (per-P allocation cachesの理解のため)
  • sweepgenに関するGoランタイムの内部実装に関する解説記事やドキュメント