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

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

このコミットは、Go言語のランタイムにおけるガベージコレクション (GC) のヘルパー機能の内部的なリファクタリングに関するものです。特に、並列GCの導入に備えて、ヘルパーゴルーチンの管理方法が変更されています。

コミット

commit 01826280eb3dec5dfa06fae0474caf1ba3942ec7
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue May 15 19:10:16 2012 +0400

    runtime: refactor helpgc functionality in preparation for parallel GC
    Parallel GC needs to know in advance how many helper threads will be there.
    Hopefully it's the last patch before I can tackle parallel sweep phase.
    The benchmarks are unaffected.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6200064

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

https://github.com/golang/go/commit/01826280eb3dec5dfa06fae0474caf1ba3942ec7

元コミット内容

このコミットは、Goランタイムのガベージコレクション (GC) における helpgc 機能のリファクタリングを目的としています。これは、将来的な並列GCの実装に向けた準備作業の一環です。並列GCでは、事前にいくつのヘルパースレッドが存在するかを把握する必要があるため、そのための変更が行われました。コミットメッセージには、これが並列スイープフェーズに取り組む前の最後のパッチになることを期待している旨が記されており、ベンチマークには影響がないことも確認されています。

変更の背景

Go言語の初期のガベージコレクタは、"Stop-the-World" (STW) 方式を採用していました。これは、GCが実行される間、すべてのアプリケーションゴルーチン(スレッド)の実行を一時停止させる方式です。STW GCは実装が比較的単純ですが、GCの実行中にアプリケーションが完全に停止するため、レイテンシが大きくなるという問題がありました。特に、ヒープサイズが大きくなるとSTWの時間が長くなり、リアルタイム性が求められるアプリケーションや、ユーザーインタラクションが重要なアプリケーションでは問題となります。

このコミットが作成された2012年頃は、GoのGCがより高度な並列・並行GCへと進化していく過渡期にあたります。並列GCは、複数のCPUコアやスレッドを活用してGC作業を並行して実行することで、STW時間を短縮し、全体のスループットを向上させることを目指します。しかし、並列処理を導入するには、GCヘルパー(GC作業を手伝うゴルーチンやスレッド)の管理方法を根本的に見直す必要がありました。特に、並列GCが効率的に動作するためには、GCヘルパーの数を事前に正確に把握し、それらを適切に調整するメカニズムが不可欠です。

このコミットは、まさにその「並列GCが事前にヘルパースレッドの数を把握する必要がある」という要件を満たすための基盤固めとして行われました。helpgc 機能は、GC中にGC作業を支援するゴルーチンを起動・管理する役割を担っていましたが、並列GCの要件に合わせてそのインターフェースと内部ロジックが変更されました。

前提知識の解説

ガベージコレクション (GC)

ガベージコレクションは、プログラムが動的に確保したメモリ領域のうち、もはやどの部分からも参照されなくなった(到達不能になった)領域を自動的に解放し、再利用可能にする仕組みです。これにより、プログラマは手動でのメモリ管理から解放され、メモリリークなどのバグを減らすことができます。

Go言語のGCは、主に以下のフェーズで構成されます(当時のGoのGCの一般的なフェーズ):

  1. マークフェーズ (Mark Phase): プログラムが使用しているオブジェクト(到達可能なオブジェクト)を特定し、マークします。ルートセット(グローバル変数、スタック上の変数など)から参照をたどり、到達可能なすべてのオブジェクトにマークを付けます。
  2. スイープフェーズ (Sweep Phase): マークされなかったオブジェクト(到達不能なオブジェクト、つまりガベージ)が占めるメモリ領域を解放し、フリーリストに戻します。これにより、これらのメモリ領域は将来のメモリ割り当てに再利用できるようになります。

Stop-the-World (STW)

STWは、GCの特定のフェーズ(特にマークフェーズの初期やスイープフェーズの一部)において、アプリケーションの実行を完全に一時停止させるGC方式です。これにより、GCがメモリの状態を安全にスキャン・変更できるようになります。STWの時間は、GCの効率性やアプリケーションの応答性に直接影響します。

並列GC (Parallel GC) と 並行GC (Concurrent GC)

  • 並列GC (Parallel GC): 複数のCPUコアやスレッドを使用して、GCの特定のフェーズ(例: マークフェーズ)を並行して実行する方式です。これにより、STW時間を短縮し、GCのスループットを向上させることができます。
  • 並行GC (Concurrent GC): GC作業の一部を、アプリケーションの実行と並行して(同時に)実行する方式です。これにより、STW時間をさらに短縮し、アプリケーションの応答性を向上させることができます。GoのGCは、最終的に並行マーク&スイープGCへと進化していきます。

GoランタイムのM, P, Gモデル

Goランタイムは、ゴルーチン(G)、論理プロセッサ(P)、OSスレッド(M)という3つのエンティティで並行性を管理します。

  • G (Goroutine): Go言語の軽量スレッド。数百万個作成することも可能。
  • P (Processor): 論理プロセッサ。Gを実行するためのコンテキストを提供し、Mに割り当てられます。GOMAXPROCS 環境変数で数を制御できます。
  • M (Machine/OS Thread): 実際のOSスレッド。Pに割り当てられたGを実行します。

GCヘルパーは、これらのMやPを活用してGC作業を行います。

helpgc 関数

helpgc は、Goランタイム内部でGCの作業を支援するために呼び出される関数です。GCが実行される際に、必要に応じて追加のM(OSスレッド)を起動し、それらをGCヘルパーとして利用することで、GC作業を並行して進めることを可能にします。

技術的詳細

このコミットの主要な変更点は、helpgc 関数のシグネチャと、GCヘルパーの数を決定するロジックの変更です。

runtime·helpgc の変更

変更前: int32 runtime·helpgc(bool *extra); この関数は、GCヘルパーとして起動したMの数を返し、extra ポインタを通じて、さらにヘルパーを追加できる余地があるかどうかを通知していました。

変更後: void runtime·helpgc(int32 nproc); 新しい helpgc は、起動すべきGCヘルパーの目標数 nproc を引数として受け取るようになりました。これにより、呼び出し元(runtime·gc)がGCヘルパーの数を明示的に制御できるようになります。

runtime·gcprocs の導入

新しい関数 runtime·gcprocs(void) が導入されました。この関数は、GC中に使用すべき論理プロセッサ(P)の数を計算して返します。この計算は、以下の要素を考慮します。

  • runtime·gomaxprocs: ユーザーが設定した論理プロセッサの最大数。
  • runtime·ncpu: 実際のCPUコア数。
  • MaxGcproc: GCヘルパーとして使用できるプロセッサの最大数(ランタイム内部で定義された定数)。
  • runtime·sched.mwait+1: 現在アイドル状態のMの数と、現在実行中のMの数を考慮し、利用可能なMの数を反映します。

この runtime·gcprocs が返す値が、新しい runtime·helpgc に渡される nproc の値となります。これにより、並列GCが事前に必要なヘルパーの数を把握し、それらを起動する準備が整います。

runtime·gc の変更

runtime·gc 関数は、GCのメインロジックを担う関数です。このコミットでは、runtime·gc 内で helpgc の呼び出し方が変更されました。

  • 変更前は、runtime·helpgc(&extra) を呼び出し、返されたヘルパー数と extra フラグに基づいて追加のMを起動するかどうかを判断していました。
  • 変更後は、まず work.nproc = runtime·gcprocs(); を呼び出してGCヘルパーの目標数を取得し、その数を runtime·helpgc(work.nproc); に渡すようになりました。これにより、GCヘルパーの起動ロジックがより明確かつ制御可能になりました。

runtime·starttheworld の変更

runtime·starttheworld 関数も、bool extra 引数が削除され、引数なしの void runtime·starttheworld(void) に変更されました。これは、GCヘルパーの追加起動ロジックが runtime·gcprocsruntime·helpgc に集約されたためです。ただし、starttheworld 内で、GCがさらにヘルパーを利用できた可能性があり、かつ canaddmcpu() が真の場合に、将来のGCのために新しいMを起動するロジックは残されています。これは、GCの最初の数ラウンドでは最大のプロセッサ数を利用できない可能性があるが、この遅延的なアプローチで実用上問題ない、というコメントが示唆しています。

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

  • src/pkg/runtime/malloc.h: runtime·helpgc の関数シグネチャが変更され、runtime·gcprocs が追加されました。

    --- a/src/pkg/runtime/malloc.h
    +++ b/src/pkg/runtime/malloc.h
    @@ -414,7 +414,8 @@ enum
     void	runtime·MProf_Malloc(void*, uintptr);
     void	runtime·MProf_Free(void*, uintptr);
     void	runtime·MProf_GC(void);
    -int32	runtime·helpgc(bool*);
    +int32	runtime·gcprocs(void);
    +void	runtime·helpgc(int32 nproc);
     void	runtime·gchelper(void);
     
     bool	runtime·getfinalizer(void *p, bool del, void (**fn)(void*), int32 *nret);
    
  • src/pkg/runtime/mgc0.c: runtime·gc 関数内で helpgc の呼び出しロジックが変更されました。

    --- a/src/pkg/runtime/mgc0.c
    +++ b/src/pkg/runtime/mgc0.c
    @@ -966,18 +964,21 @@ runtime·gc(int32 force)
     	m->gcing = 1;
     	runtime·stoptheworld();
     
    -	cachestats(nil);
    -	heap0 = mstats.heap_alloc;
    -	obj0 = mstats.nmalloc - mstats.nfree;
    +	heap0 = 0;
    +	obj0 = 0;
    +	if(gctrace) {
    +		cachestats(nil);
    +		heap0 = mstats.heap_alloc;
    +		obj0 = mstats.nmalloc - mstats.nfree;
    +	}
     
     	runtime·lock(&work.markgate);
     	runtime·lock(&work.sweepgate);
     
    -	extra = false;
    -	work.nproc = 1;
    -	if(runtime·gomaxprocs > 1 && runtime·ncpu > 1) {
    +	work.nproc = runtime·gcprocs();
    +	if(work.nproc > 1) {
     		runtime·noteclear(&work.alldone);
    -		work.nproc += runtime·helpgc(&extra);
    +		runtime·helpgc(work.nproc);
     	}
     	work.nwait = 0;
     	work.ndone = 0;
    @@ -1036,15 +1037,7 @@ runtime·gc(int32 force)
     	
     	runtime·MProf_GC();
     	runtime·semrelease(&runtime·worldsema);
    -
    -	// If we could have used another helper proc, start one now,
    -	// in the hope that it will be available next time.
    -	// It would have been even better to start it before the collection,
    -	// but doing so requires allocating memory, so it's tricky to
    -	// coordinate.  This lazy approach works out in practice:
    -	// we don't mind if the first couple gc rounds don't have quite
    -	// the maximum number of procs.
    -	runtime·starttheworld(extra);
    +	runtime·starttheworld();
     
     	// give the queued finalizers, if any, a chance to run	
     	if(finq != nil)	
    @@ -1068,7 +1061,7 @@ runtime·ReadMemStats(MStats *stats)
     	*stats = mstats;
     	m->gcing = 0;
     	runtime·semrelease(&runtime·worldsema);
    -	runtime·starttheworld(false);
    +	runtime·starttheworld();
     }
    
  • src/pkg/runtime/proc.c: runtime·gcprocs が実装され、runtime·helpgcruntime·starttheworld のシグネチャと実装が変更されました。

    --- a/src/pkg/runtime/proc.c
    +++ b/src/pkg/runtime/proc.c
    @@ -646,35 +646,38 @@ top:
     }
     
     int32
    -runtime·helpgc(bool *extra)
    +runtime·gcprocs(void)
     {
    -	M *mp;
    -	int32 n, max;
    -
    -	// Figure out how many CPUs to use.
    +	int32 n;
    +	
    +	// Figure out how many CPUs to use during GC.
     	// Limited by gomaxprocs, number of actual CPUs, and MaxGcproc.
    -	max = runtime·gomaxprocs;
    -	if(max > runtime·ncpu)
    -		max = runtime·ncpu;
    -	if(max > MaxGcproc)
    -		max = MaxGcproc;
    +	n = runtime·gomaxprocs;
    +	if(n > runtime·ncpu)
    +		n = runtime·ncpu;
    +	if(n > MaxGcproc)
    +		n = MaxGcproc;
    +	if(n > runtime·sched.mwait+1) // one M is currently running
    +		n = runtime·sched.mwait+1;
    +	return n;
    +}
     
    -	// We're going to use one CPU no matter what.
    -	// Figure out the max number of additional CPUs.
    -	max--;
    +void
    +runtime·helpgc(int32 nproc)
    +{
    +	M *mp;
    +	int32 n;
     
     	runtime·lock(&runtime·sched);
    -	n = 0;
    -	while(n < max && (mp = mget(nil)) != nil) {
    -		n++;
    +	for(n = 1; n < nproc; n++) { // one M is currently running
    +		mp = mget(nil);
    +		if(mp == nil)
    +			runtime·throw("runtime·gcprocs inconsistency");
     		mp->helpgc = 1;
     		mp->waitnextg = 0;
     		runtime·notewakeup(&mp->havenextg);
     	}
     	runtime·unlock(&runtime·sched);\
    -	if(extra)
    -		*extra = n != max;
    -	return n;
     }
     
     void
    @@ -714,18 +717,30 @@ runtime·stoptheworld(void)
     }
     
     void
    -runtime·starttheworld(bool extra)
    +runtime·starttheworld(void)
     {
     	M *m;
    +	int32 max;
    +	
    +	// Figure out how many CPUs GC could possibly use.
    +	max = runtime·gomaxprocs;
    +	if(max > runtime·ncpu)
    +		max = runtime·ncpu;
    +	if(max > MaxGcproc)
    +		max = MaxGcproc;
     
     	schedlock();
     	runtime·gcwaiting = 0;
     	setmcpumax(runtime·gomaxprocs);
     	matchmg();
    -	if(extra && canaddmcpu()) {
    -		// Start a new m that will (we hope) be idle
    -		// and so available to help when the next
    -		// garbage collection happens.
    +	if(runtime·gcprocs() < max && canaddmcpu()) {
    +		// If GC could have used another helper proc, start one now,
    +		// in the hope that it will be available next time.
    +		// It would have been even better to start it before the collection,
    +		// but doing so requires allocating memory, so it's tricky to
    +		// coordinate.  This lazy approach works out in practice:
    +		// we don't mind if the first couple gc rounds don't have quite
    +		// the maximum number of procs.
     		// canaddmcpu above did mcpu++
     		// (necessary, because m will be doing various
     		// initialization work so is definitely running),
    
  • src/pkg/runtime/runtime.h: runtime·starttheworld の関数シグネチャが変更されました。

    --- a/src/pkg/runtime/runtime.h
    +++ b/src/pkg/runtime/runtime.h
    @@ -636,7 +636,7 @@ int64	runtime·cputicks(void);
     #pragma	varargck	type	"S"	String
     
     void	runtime·stoptheworld(void);
    -void	runtime·starttheworld(bool);
    +void	runtime·starttheworld(void);
     extern uint32 runtime·worldsema;
     
     /*
    

コアとなるコードの解説

このコミットの核心は、GCヘルパーの数を動的に、かつより予測可能に制御するためのメカニズムの導入です。

  1. runtime·gcprocs() の導入: この関数は、現在のシステム状態(GOMAXPROCS、CPUコア数、GCヘルパーの最大制限 MaxGcproc、およびアイドル状態のMの数)に基づいて、GCが利用できる最適なプロセッサ数を計算します。これにより、GCは自身の作業を並列化するために利用可能なリソースを事前に把握できるようになります。これは、並列GCが「事前にいくつのヘルパースレッドが存在するかを知る必要がある」という要件を満たすための重要なステップです。

  2. runtime·helpgc(int32 nproc) への変更: 以前の helpgc は、追加で起動したヘルパーの数を返すだけでした。新しい helpgc は、runtime·gcprocs() が計算した目標のヘルパー数 nproc を引数として受け取ります。これにより、helpgcnproc に達するまでMを起動し、それらをGCヘルパーとして設定します。この変更により、GCヘルパーの起動がより意図的かつ制御可能になりました。

  3. runtime·gc() 内のロジック変更: runtime·gc() は、GCの開始時に runtime·gcprocs() を呼び出して最適なヘルパー数を決定し、その数を runtime·helpgc() に渡すようになりました。これにより、GCは開始時に必要なヘルパーを効率的に起動し、並列処理の準備を整えることができます。

  4. runtime·starttheworld() の簡素化: starttheworld から bool extra 引数が削除されました。これは、GCヘルパーの起動ロジックが gcprocshelpgc に集約されたためです。ただし、starttheworld 内には、将来のGCのためにアイドル状態のMを起動する「遅延的な」ロジックが残されており、これはGCの最初の数ラウンドで最適なヘルパー数を確保できない場合でも、実用上問題ないという設計思想を反映しています。

これらの変更は、GoのGCが「Stop-the-World」の時間を最小限に抑え、より効率的な並列・並行GCへと進化していくための重要な基盤を築きました。特に、GCヘルパーの数を事前に決定し、制御する能力は、複雑な並列アルゴリズムを実装する上で不可欠です。

関連リンク

  • Go言語のガベージコレクションに関する公式ドキュメントやブログ記事 (当時の情報源を探すのが難しい場合がありますが、GoのGCの進化に関する記事は多数存在します)
  • Goのランタイムソースコード (特に src/runtime ディレクトリ)
  • GoのIssueトラッカーやデザインドキュメント (並列GCの設計に関する議論)

参考にした情報源リンク

  • https://golang.org/cl/6200064 (このコミットのChange List)
  • Go言語の公式ドキュメント (GoのGCの仕組みに関する一般的な情報)
  • Goのソースコードリポジトリ (特に src/runtime ディレクトリ内の mgc0.c, proc.c, malloc.h, runtime.h ファイル)
  • Dmitriy Vyukov氏の他のコミットや関連するGoのIssue (並列GCに関する彼の貢献を追跡するため)
  • GoのGCに関する技術ブログや論文 (GoのGCの進化を解説しているもの)