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

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

このコミットは、Go言語のランタイムにおけるガベージコレクション(GC)のコードベースに対するマイナーなリファクタリングを導入しています。具体的には、sweep 関数から sweepspan 関数を分離し、将来的な並行GCの実装に向けた準備を行っています。論理的な変更は含まれておらず、コードの構造改善が主目的です。

コミット

commit 9903d6870f75f7c174ceb1bb8ea67e303920c8e5
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Thu Apr 5 21:02:20 2012 +0400

    runtime: minor refactoring in preparation for parallel GC
    factor sweepspan() out of sweep(), no logical changes
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/5991047

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

https://github.com/golang/go/commit/9903d6870f75f7c174ceb1bb8ea67e303920c8e5

元コミット内容

runtime: minor refactoring in preparation for parallel GC
factor sweepspan() out of sweep(), no logical changes

変更の背景

このコミットの主な背景は、Go言語のガベージコレクション(GC)メカニズムを将来的に並行化するための準備です。当時のGoのGCは、主にストップ・ザ・ワールド(STW)方式を採用しており、GC実行中にアプリケーションの実行が一時停止する時間が存在しました。このSTW時間を短縮し、GCの効率とスループットを向上させるために、並行GCへの移行が計画されていました。

並行GCを導入するためには、既存のGCコードベースをモジュール化し、並行処理に適した粒度で関数を分割する必要があります。sweep 関数はGCの「スイープフェーズ」を担当する重要な関数であり、その内部で複数の処理(メモリ領域の走査、マークビットのクリア、解放処理など)を行っていました。この複雑な関数をより小さな、独立した関数に分割することで、将来的に各処理を並行して実行しやすくするための基盤を築くことが目的でした。

このリファクタリングは、直接的な機能変更やバグ修正ではなく、コードの保守性、可読性、そして将来の拡張性を高めるための「内部品質改善」の一環として行われました。

前提知識の解説

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

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

Go言語のGCは、主に以下のフェーズで構成されます(当時のバージョンにおける一般的な概念):

  1. マークフェーズ (Mark Phase): プログラムが使用しているオブジェクト(到達可能なオブジェクト)を特定し、マーク(印付け)します。GoのGCは、通常、ルート(グローバル変数、スタック上の変数など)から到達可能なオブジェクトを辿ってマークします。
  2. スイープフェーズ (Sweep Phase): マークされなかったオブジェクト(到達不能なオブジェクト、つまりガベージ)をメモリから解放し、再利用可能な状態にします。このフェーズでは、マークビットをクリアして次のGCサイクルに備える作業も行われます。

ストップ・ザ・ワールド (Stop-The-World, STW)

STWは、GCの特定のフェーズ(特にマークフェーズの初期やスイープフェーズの一部)において、アプリケーションの実行を完全に一時停止させるGC方式です。これにより、GCがメモリの状態を安全かつ一貫性のある形で操作できるようになります。しかし、STW時間はアプリケーションの応答性やスループットに直接影響を与えるため、GCの進化はSTW時間の短縮を目指す傾向にあります。

並行GC (Concurrent GC)

並行GCは、GC処理の一部または大部分を、アプリケーションの実行と並行して(同時に)行うGC方式です。これにより、STW時間を大幅に短縮し、アプリケーションの停止時間を最小限に抑えることができます。並行GCの実装は複雑であり、GCとアプリケーションが同時にメモリを操作する際のデータ競合や一貫性の問題を解決するための高度なアルゴリズムが必要となります。

MSpanMCache

Goのランタイムにおけるメモリ管理は、MSpanMCache という概念に基づいています。

  • MSpan: 連続したページ(メモリブロックの単位)の集合を表します。Goのヒープは、これらのMSpanの集まりとして管理されます。MSpanは、特定のサイズのオブジェクト(スモールオブジェクト、ラージオブジェクト)を格納するために使用されます。
  • MCache: 各M(OSスレッドに対応するGoの論理プロセッサ)にローカルなキャッシュです。これにより、頻繁に割り当てられるスモールオブジェクトの割り当てと解放を高速化し、グローバルなヒープロックの競合を減らします。

mgc0.c

src/pkg/runtime/mgc0.c は、Goランタイムのガベージコレクションの初期実装(または主要な部分)を含むC言語のソースファイルです。Goランタイムの一部は、パフォーマンスと低レベルのメモリ操作のためにC言語で書かれていました(後にGo言語自体で書き直される部分も増えます)。

技術的詳細

このコミットは、src/pkg/runtime/mgc0.c ファイル内の sweep 関数から、sweepspan という新しい静的関数を抽出しています。

元の sweep 関数は、GCのスイープフェーズにおいて、ヒープ全体を走査し、マークされていないメモリブロックを解放する役割を担っていました。この処理は、MSpan(メモリ領域の単位)ごとにループし、各MSpan内のオブジェクトを個別に処理していました。

抽出された sweepspan 関数は、単一の MSpan を受け取り、その MSpan 内のすべてのオブジェクトに対してスイープ処理(マークビットのクリア、ガベージの解放、ファイナライザの処理など)を実行します。

この変更により、sweep 関数は、MSpan のリストをイテレートし、各 MSpan に対して sweepspan を呼び出すという、より高レベルなロジックに簡素化されました。これにより、sweep 関数自体の複雑性が軽減され、各 MSpan の処理が独立した関数としてカプセル化されました。

このリファクタリングは、並行GCの文脈で非常に重要です。将来的に、複数のゴルーチン(またはOSスレッド)が並行してスイープ処理を行う場合、各ゴルーチンが独立した MSpan を取得し、その MSpan に対して sweepspan を並行して呼び出すことが可能になります。これにより、スイープフェーズ全体のスループットが向上し、STW時間の短縮に貢献できます。

コードの変更点を見ると、sweep 関数から、cl, n, npages, size, p, c, arena_start といったローカル変数の宣言と、for(; n > 0; n--, p += size) ループ内の処理が sweepspan 関数に移動していることがわかります。これにより、sweep 関数は for ループ内で sweepspan(s); を呼び出すだけのシンプルな構造になっています。

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

変更は src/pkg/runtime/mgc0.c ファイルに集中しています。

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -719,22 +719,17 @@ handlespecial(byte *p, uintptr size)
 	return true;
 }
 
+static void sweepspan(MSpan *s);
+
 // Sweep frees or collects finalizers for blocks not marked in the mark phase.
 // It clears the mark bits in preparation for the next GC round.
 static void
 sweep(void)
 {
 	MSpan *s;
 -	int32 cl, n, npages;
 -	uintptr size;
 -	byte *p;
 -	MCache *c;
 -	byte *arena_start;
  	int64 now;
 
 -	arena_start = runtime·mheap.arena_start;
  	now = runtime·nanotime();
 -
  	for(;;) {
  		s = work.spans;
  		if(s == nil)
@@ -750,69 +745,82 @@ sweep(void)
  		if(s->state != MSpanInUse)
  			continue;
 
 -		p = (byte*)(s->start << PageShift);
 -		cl = s->sizeclass;
 -		if(cl == 0) {
 -			size = s->npages<<PageShift;
 -			n = 1;
 -		} else {
 -			// Chunk full of small blocks.
 -			size = runtime·class_to_size[cl];
 -			npages = runtime·class_to_allocnpages[cl];
 -			n = (npages << PageShift) / size;
 -		}
 +		sweepspan(s);
 +	}
 +}
  
 -		// Sweep through n objects of given size starting at p.
 -		// This thread owns the span now, so it can manipulate
 -		// the block bitmap without atomic operations.
 -		for(; n > 0; n--, p += size) {
 -			uintptr off, *bitp, shift, bits;
 +static void
 +sweepspan(MSpan *s)
 +{
 +\tint32 cl, n, npages;
 +\tuintptr size;
 +\tbyte *p;
 +\tMCache *c;
 +\tbyte *arena_start;
  
 -			off = (uintptr*)p - (uintptr*)arena_start;
 -			bitp = (uintptr*)arena_start - off/wordsPerBitmapWord - 1;
 -			shift = off % wordsPerBitmapWord;
 -			bits = *bitp>>shift;
 +	arena_start = runtime·mheap.arena_start;
 +\tp = (byte*)(s->start << PageShift);\n+\tcl = s->sizeclass;
 +\tif(cl == 0) {
 +\t\tsize = s->npages<<PageShift;
 +\t\tn = 1;
 +\t} else {
 +\t\t// Chunk full of small blocks.
 +\t\tsize = runtime·class_to_size[cl];
 +\t\tnpages = runtime·class_to_allocnpages[cl];
 +\t\tn = (npages << PageShift) / size;
 +\t}
  
 -			if((bits & bitAllocated) == 0)
 -				continue;
 +	// Sweep through n objects of given size starting at p.
 +	// This thread owns the span now, so it can manipulate
 +	// the block bitmap without atomic operations.
 +	for(; n > 0; n--, p += size) {
 +\t\tuintptr off, *bitp, shift, bits;
  
 -			if((bits & bitMarked) != 0) {
 -				if(DebugMark) {
 -					if(!(bits & bitSpecial))
 -						runtime·printf("found spurious mark on %p\n", p);
 -					*bitp &= ~(bitSpecial<<shift);
 -				}
 -				*bitp &= ~(bitMarked<<shift);
 -				continue;
 -			}
 +\t\toff = (uintptr*)p - (uintptr*)arena_start;
 +\t\tbitp = (uintptr*)arena_start - off/wordsPerBitmapWord - 1;
 +\t\tshift = off % wordsPerBitmapWord;
 +\t\tbits = *bitp>>shift;
  
 -			// Special means it has a finalizer or is being profiled.
 -			// In DebugMark mode, the bit has been coopted so
 -			// we have to assume all blocks are special.
 -			if(DebugMark || (bits & bitSpecial) != 0) {
 -				if(handlespecial(p, size))
 -					continue;
 -			}
 +\t\tif((bits & bitAllocated) == 0)
 +\t\t\tcontinue;
  
 -			// Mark freed; restore block boundary bit.
 -			*bitp = (*bitp & ~(bitMask<<shift)) | (bitBlockBoundary<<shift);
 +\t\tif((bits & bitMarked) != 0) {
 +\t\t\tif(DebugMark) {
 +\t\t\t\tif(!(bits & bitSpecial))
 +\t\t\t\t\truntime·printf("found spurious mark on %p\\n", p);
 +\t\t\t\t*bitp &= ~(bitSpecial<<shift);
 +\t\t\t}
 +\t\t\t*bitp &= ~(bitMarked<<shift);
 +\t\t\tcontinue;
 +\t\t}
  
 -			c = m->mcache;
 -			if(s->sizeclass == 0) {
 -				// Free large span.
 -				runtime·unmarkspan(p, 1<<PageShift);
 -				*(uintptr*)p = 1;	// needs zeroing
 -				runtime·MHeap_Free(&runtime·mheap, s, 1);
 -			} else {
 -				// Free small object.
 -				if(size > sizeof(uintptr))
 -					((uintptr*)p)[1] = 1;	// mark as "needs to be zeroed"
 -				c->local_by_size[s->sizeclass].nfree++;
 -				runtime·MCache_Free(c, p, s->sizeclass, size);
 -			}
 -			c->local_alloc -= size;
 -			c->local_nfree++;
 +\t\t// Special means it has a finalizer or is being profiled.
 +\t\t// In DebugMark mode, the bit has been coopted so
 +\t\t// we have to assume all blocks are special.
 +\t\tif(DebugMark || (bits & bitSpecial) != 0) {
 +\t\t\tif(handlespecial(p, size))
 +\t\t\t\tcontinue;
 +\t\t}
  
 -		}
 +\t\t// Mark freed; restore block boundary bit.
 +\t\t*bitp = (*bitp & ~(bitMask<<shift)) | (bitBlockBoundary<<shift);
 +\n+\t\tc = m->mcache;
 +\t\tif(s->sizeclass == 0) {
 +\t\t\t// Free large span.
 +\t\t\truntime·unmarkspan(p, 1<<PageShift);
 +\t\t\t*(uintptr*)p = 1;\t// needs zeroing
 +\t\t\truntime·MHeap_Free(&runtime·mheap, s, 1);
 +\t\t} else {
 +\t\t\t// Free small object.
 +\t\t\tif(size > sizeof(uintptr))
 +\t\t\t\t((uintptr*)p)[1] = 1;\t// mark as "needs to be zeroed"
 +\t\t\tc->local_by_size[s->sizeclass].nfree++;
 +\t\t\truntime·MCache_Free(c, p, s->sizeclass, size);
 +\t\t}
 +\t\tc->local_alloc -= size;
 +\t\tc->local_nfree++;
 +\t}
  }
  

コアとなるコードの解説

sweep 関数の変更

変更前: sweep 関数は、work.spans リストをイテレートし、各 MSpan に対して直接スイープ処理を行っていました。この処理には、MSpan のサイズクラスに応じたオブジェクトサイズの計算、各オブジェクトのマークビットのチェックとクリア、ガベージの解放(MCache_FreeMHeap_Free の呼び出し)、ファイナライザの処理などが含まれていました。

変更後: sweep 関数は大幅に簡素化されました。work.spans リストをイテレートするループはそのままですが、各 MSpan s に対して、新しく抽出された sweepspan(s); を呼び出すだけになりました。これにより、sweep 関数は「どの MSpan をスイープするか」という高レベルな制御に特化し、「どのようにスイープするか」という詳細なロジックは sweepspan に委譲されました。

sweepspan 関数の追加

新しく追加された static void sweepspan(MSpan *s) 関数は、単一の MSpan s を引数に取り、その MSpan 内のすべてのメモリブロックに対してスイープ処理を実行します。

この関数は、元の sweep 関数から移動してきた以下の主要なロジックを含んでいます。

  1. MSpan の情報取得: MSpansizeclass に基づいて、オブジェクトのサイズ (size) と MSpan 内のオブジェクト数 (n) を計算します。
  2. オブジェクトの走査: for(; n > 0; n--, p += size) ループを使って、MSpan 内の各オブジェクトを走査します。
  3. マークビットのチェックとクリア: 各オブジェクトのメモリブロックに対応するビットマップを操作し、bitAllocatedbitMarkedbitSpecial などのビットをチェックします。
    • bitMarked が設定されている場合(オブジェクトがまだ使用されている場合)、bitMarked をクリアして次のGCサイクルに備えます。
    • bitMarked が設定されていない場合(オブジェクトがガベージである場合)、handlespecial を呼び出してファイナライザの処理などを行います。
  4. メモリの解放: ガベージと判断されたオブジェクトは、そのサイズクラスに応じて runtime·MCache_Free(スモールオブジェクトの場合)または runtime·MHeap_Free(ラージオブジェクトの場合)を呼び出して解放されます。また、解放されたメモリブロックの先頭にゼロを書き込むことで、再利用時の安全性を確保しています。
  5. MCache の更新: ローカルキャッシュ m->mcache の統計情報(local_alloc, local_nfree)を更新します。

この分離により、sweepspanMSpan レベルでのスイープ処理の完全なカプセル化を提供します。これは、将来的に複数のスレッドが異なる MSpan を並行してスイープする際に、各スレッドが独立して sweepspan を呼び出すことができるため、並行GCの実装を容易にします。

関連リンク

  • Go言語のガベージコレクションに関する公式ドキュメントやブログ記事(当時の情報にアクセスするのは難しい場合がありますが、GoのGCの進化を追うことで理解が深まります)。
  • Goのランタイムソースコード(特に src/runtime/mgc.gosrc/runtime/mheap.go など、メモリ管理とGCに関連するファイル)。

参考にした情報源リンク

  • Go言語の公式リポジトリ: https://github.com/golang/go
  • Goのガベージコレクションに関する一般的な情報源(例: GoのGCの歴史や進化に関するブログ記事、論文など)。
  • Goのランタイムの内部構造に関する技術解説記事。
  • Goのコードレビューシステム (Gerrit) のCL (Change List) リンク: https://golang.org/cl/5991047 (これはコミットメッセージに記載されているもので、当時のコードレビューの議論を辿るのに役立ちます)

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

このコミットは、Go言語のランタイムにおけるガベージコレクション(GC)のコードベースに対するマイナーなリファクタリングを導入しています。具体的には、sweep 関数から sweepspan 関数を分離し、将来的な並行GCの実装に向けた準備を行っています。論理的な変更は含まれておらず、コードの構造改善が主目的です。

コミット

commit 9903d6870f75f7c174ceb1bb8ea67e303920c8e5
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Thu Apr 5 21:02:20 2012 +0400

    runtime: minor refactoring in preparation for parallel GC
    factor sweepspan() out of sweep(), no logical changes
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/5991047

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

https://github.com/golang/go/commit/9903d6870f75f7c174ceb1bb8ea67e303920c8e5

元コミット内容

runtime: minor refactoring in preparation for parallel GC
factor sweepspan() out of sweep(), no logical changes

変更の背景

このコミットの主な背景は、Go言語のガベージコレクション(GC)メカニズムを将来的に並行化するための準備です。当時のGoのGCは、主にストップ・ザ・ワールド(STW)方式を採用しており、GC実行中にアプリケーションの実行が一時停止する時間が存在しました。このSTW時間を短縮し、GCの効率とスループットを向上させるために、並行GCへの移行が計画されていました。

並行GCを導入するためには、既存のGCコードベースをモジュール化し、並行処理に適した粒度で関数を分割する必要があります。sweep 関数はGCの「スイープフェーズ」を担当する重要な関数であり、その内部で複数の処理(メモリ領域の走査、マークビットのクリア、解放処理など)を行っていました。この複雑な関数をより小さな、独立した関数に分割することで、将来的に各処理を並行して実行しやすくするための基盤を築くことが目的でした。

このリファクタリングは、直接的な機能変更やバグ修正ではなく、コードの保守性、可読性、そして将来の拡張性を高めるための「内部品質改善」の一環として行われました。

前提知識の解説

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

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

Go言語のGCは、主に以下のフェーズで構成されます(当時のバージョンにおける一般的な概念):

  1. マークフェーズ (Mark Phase): プログラムが使用しているオブジェクト(到達可能なオブジェクト)を特定し、マーク(印付け)します。GoのGCは、通常、ルート(グローバル変数、スタック上の変数など)から到達可能なオブジェクトを辿ってマークします。
  2. スイープフェーズ (Sweep Phase): マークされなかったオブジェクト(到達不能なオブジェクト、つまりガベージ)をメモリから解放し、再利用可能な状態にします。このフェーズでは、マークビットをクリアして次のGCサイクルに備える作業も行われます。

ストップ・ザ・ワールド (Stop-The-World, STW)

STWは、GCの特定のフェーズ(特にマークフェーズの初期やスイープフェーズの一部)において、アプリケーションの実行を完全に一時停止させるGC方式です。これにより、GCがメモリの状態を安全かつ一貫性のある形で操作できるようになります。しかし、STW時間はアプリケーションの応答性やスループットに直接影響を与えるため、GCの進化はSTW時間の短縮を目指す傾向にあります。

並行GC (Concurrent GC)

並行GCは、GC処理の一部または大部分を、アプリケーションの実行と並行して(同時に)行うGC方式です。これにより、STW時間を大幅に短縮し、アプリケーションの停止時間を最小限に抑えることができます。並行GCの実装は複雑であり、GCとアプリケーションが同時にメモリを操作する際のデータ競合や一貫性の問題を解決するための高度なアルゴリズムが必要となります。

MSpanMCache

Goのランタイムにおけるメモリ管理は、MSpanMCache という概念に基づいています。

  • MSpan: 連続したページ(メモリブロックの単位)の集合を表します。Goのヒープは、これらのMSpanの集まりとして管理されます。MSpanは、特定のサイズのオブジェクト(スモールオブジェクト、ラージオブジェクト)を格納するために使用されます。
  • MCache: 各M(OSスレッドに対応するGoの論理プロセッサ)にローカルなキャッシュです。これにより、頻繁に割り当てられるスモールオブジェクトの割り当てと解放を高速化し、グローバルなヒープロックの競合を減らします。

mgc0.c

src/pkg/runtime/mgc0.c は、Goランタイムのガベージコレクションの初期実装(または主要な部分)を含むC言語のソースファイルです。Goランタイムの一部は、パフォーマンスと低レベルのメモリ操作のためにC言語で書かれていました(後にGo言語自体で書き直される部分も増えます)。

技術的詳細

このコミットは、src/pkg/runtime/mgc0.c ファイル内の sweep 関数から、sweepspan という新しい静的関数を抽出しています。

元の sweep 関数は、GCのスイープフェーズにおいて、ヒープ全体を走査し、マークされていないメモリブロックを解放する役割を担っていました。この処理は、MSpan(メモリ領域の単位)ごとにループし、各MSpan内のオブジェクトを個別に処理していました。

抽出された sweepspan 関数は、単一の MSpan を受け取り、その MSpan 内のすべてのオブジェクトに対してスイープ処理(マークビットのクリア、ガベージの解放、ファイナライザの処理など)を実行します。

この変更により、sweep 関数は、MSpan のリストをイテレートし、各 MSpan に対して sweepspan を呼び出すという、より高レベルなロジックに簡素化されました。これにより、sweep 関数自体の複雑性が軽減され、各 MSpan の処理が独立した関数としてカプセル化されました。

このリファクタリングは、並行GCの文脈で非常に重要です。将来的に、複数のゴルーチン(またはOSスレッド)が並行してスイープ処理を行う場合、各ゴルーチンが独立した MSpan を取得し、その MSpan に対して sweepspan を並行して呼び出すことが可能になります。これにより、スイープフェーズ全体のスループットが向上し、STW時間の短縮に貢献できます。

コードの変更点を見ると、sweep 関数から、cl, n, npages, size, p, c, arena_start といったローカル変数の宣言と、for(; n > 0; n--, p += size) ループ内の処理が sweepspan 関数に移動していることがわかります。これにより、sweep 関数は for ループ内で sweepspan(s); を呼び出すだけのシンプルな構造になっています。

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

変更は src/pkg/runtime/mgc0.c ファイルに集中しています。

--- a/src/pkg/runtime/mgc0.c
+++ b/src/pkg/runtime/mgc0.c
@@ -719,22 +719,17 @@ handlespecial(byte *p, uintptr size)
 	return true;
 }
 
+static void sweepspan(MSpan *s);
+
 // Sweep frees or collects finalizers for blocks not marked in the mark phase.
 // It clears the mark bits in preparation for the next GC round.
 static void
 sweep(void)
 {
 	MSpan *s;
 -	int32 cl, n, npages;
 -	uintptr size;
 -	byte *p;
 -	MCache *c;
 -	byte *arena_start;
  	int64 now;
 
 -	arena_start = runtime·mheap.arena_start;
  	now = runtime·nanotime();
 -
  	for(;;) {
  		s = work.spans;
  		if(s == nil)
@@ -750,69 +745,82 @@ sweep(void)
  		if(s->state != MSpanInUse)
  			continue;
 
 -		p = (byte*)(s->start << PageShift);
 -		cl = s->sizeclass;
 -		if(cl == 0) {
 -			size = s->npages<<PageShift;
 -			n = 1;
 -		} else {
 -			// Chunk full of small blocks.
 -			size = runtime·class_to_size[cl];
 -			npages = runtime·class_to_allocnpages[cl];
 -			n = (npages << PageShift) / size;
 -		}
 +		sweepspan(s);
 +	}
 +}
  
 -		// Sweep through n objects of given size starting at p.
 -		// This thread owns the span now, so it can manipulate
 -		// the block bitmap without atomic operations.
 -		for(; n > 0; n--, p += size) {
 -			uintptr off, *bitp, shift, bits;
 +static void
 +sweepspan(MSpan *s)
 +{
 +\tint32 cl, n, npages;
 +\tuintptr size;
 +\tbyte *p;
 +\tMCache *c;
 +\tbyte *arena_start;
  
 -			off = (uintptr*)p - (uintptr*)arena_start;
 -			bitp = (uintptr*)arena_start - off/wordsPerBitmapWord - 1;
 -			shift = off % wordsPerBitmapWord;
 -			bits = *bitp>>shift;
 +	arena_start = runtime·mheap.arena_start;
 +\tp = (byte*)(s->start << PageShift);\n+\tcl = s->sizeclass;
 +\tif(cl == 0) {
 +\t\tsize = s->npages<<PageShift;
 +\t\tn = 1;
 +\t} else {
 +\t\t// Chunk full of small blocks.
 +\t\tsize = runtime·class_to_size[cl];
 +\t\tnpages = runtime·class_to_allocnpages[cl];
 +\t\tn = (npages << PageShift) / size;
 +\t}
  
 -			if((bits & bitAllocated) == 0)
 -				continue;
 +	// Sweep through n objects of given size starting at p.
 +	// This thread owns the span now, so it can manipulate
 +	// the block bitmap without atomic operations.
 +	for(; n > 0; n--, p += size) {
 +\t\tuintptr off, *bitp, shift, bits;
  
 -			if((bits & bitMarked) != 0) {
 -				if(DebugMark) {
 -					if(!(bits & bitSpecial))
 -						runtime·printf("found spurious mark on %p\n", p);
 -					*bitp &= ~(bitSpecial<<shift);
 -				}
 -				*bitp &= ~(bitMarked<<shift);
 -				continue;
 -			}
 +\t\toff = (uintptr*)p - (uintptr*)arena_start;
 +\t\tbitp = (uintptr*)arena_start - off/wordsPerBitmapWord - 1;
 +\t\tshift = off % wordsPerBitmapWord;
 +\t\tbits = *bitp>>shift;
  
 -			// Special means it has a finalizer or is being profiled.
 -			// In DebugMark mode, the bit has been coopted so
 -			// we have to assume all blocks are special.
 -			if(DebugMark || (bits & bitSpecial) != 0) {
 -				if(handlespecial(p, size))
 -					continue;
 -			}
 +\t\tif((bits & bitAllocated) == 0)
 +\t\t\tcontinue;
  
 -			// Mark freed; restore block boundary bit.
 -			*bitp = (*bitp & ~(bitMask<<shift)) | (bitBlockBoundary<<shift);
 +\t\tif((bits & bitMarked) != 0) {
 +\t\t\tif(DebugMark) {
 +\t\t\t\tif(!(bits & bitSpecial))
 +\t\t\t\t\truntime·printf("found spurious mark on %p\\n", p);
 +\t\t\t\t*bitp &= ~(bitSpecial<<shift);
 +\t\t\t}
 +\t\t\t*bitp &= ~(bitMarked<<shift);
 +\t\t\tcontinue;
 +\t\t}
  
 -			c = m->mcache;
 -			if(s->sizeclass == 0) {
 -				// Free large span.
 -				runtime·unmarkspan(p, 1<<PageShift);
 -				*(uintptr*)p = 1;	// needs zeroing
 -				runtime·MHeap_Free(&runtime·mheap, s, 1);
 -			} else {
 -				// Free small object.
 -				if(size > sizeof(uintptr))
 -					((uintptr*)p)[1] = 1;	// mark as "needs to be zeroed"
 -				c->local_by_size[s->sizeclass].nfree++;
 -				runtime·MCache_Free(c, p, s->sizeclass, size);
 -			}
 -			c->local_alloc -= size;
 -			c->local_nfree++;
 +\t\t// Special means it has a finalizer or is being profiled.
 +\t\t// In DebugMark mode, the bit has been coopted so
 +\t\t// we have to assume all blocks are special.
 +\t\tif(DebugMark || (bits & bitSpecial) != 0) {
 +\t\t\tif(handlespecial(p, size))
 +\t\t\t\tcontinue;
 +\t\t}
  
 -		}
 +\t\t// Mark freed; restore block boundary bit.
 +\t\t*bitp = (*bitp & ~(bitMask<<shift)) | (bitBlockBoundary<<shift);
 +\n+\t\tc = m->mcache;
 +\t\tif(s->sizeclass == 0) {
 +\t\t\t// Free large span.
 +\t\t\truntime·unmarkspan(p, 1<<PageShift);
 +\t\t\t*(uintptr*)p = 1;\t// needs zeroing
 +\t\t\truntime·MHeap_Free(&runtime·mheap, s, 1);
 +\t\t} else {
 +\t\t\t// Free small object.
 +\t\t\tif(size > sizeof(uintptr))
 +\t\t\t\t((uintptr*)p)[1] = 1;\t// mark as "needs to be zeroed"
 +\t\t\tc->local_by_size[s->sizeclass].nfree++;
 +\t\t\truntime·MCache_Free(c, p, s->sizeclass, size);
 +\t\t}
 +\t\tc->local_alloc -= size;
 +\t\tc->local_nfree++;
 +\t}
  }
  

コアとなるコードの解説

sweep 関数の変更

変更前: sweep 関数は、work.spans リストをイテレートし、各 MSpan に対して直接スイープ処理を行っていました。この処理には、MSpan のサイズクラスに応じたオブジェクトサイズの計算、各オブジェクトのマークビットのチェックとクリア、ガベージの解放(MCache_FreeMHeap_Free の呼び出し)、ファイナライザの処理などが含まれていました。

変更後: sweep 関数は大幅に簡素化されました。work.spans リストをイテレートするループはそのままですが、各 MSpan s に対して、新しく抽出された sweepspan(s); を呼び出すだけになりました。これにより、sweep 関数は「どの MSpan をスイープするか」という高レベルな制御に特化し、「どのようにスイープするか」という詳細なロジックは sweepspan に委譲されました。

sweepspan 関数の追加

新しく追加された static void sweepspan(MSpan *s) 関数は、単一の MSpan s を引数に取り、その MSpan 内のすべてのメモリブロックに対してスイープ処理を実行します。

この関数は、元の sweep 関数から移動してきた以下の主要なロジックを含んでいます。

  1. MSpan の情報取得: MSpansizeclass に基づいて、オブジェクトのサイズ (size) と MSpan 内のオブジェクト数 (n) を計算します。
  2. オブジェクトの走査: for(; n > 0; n--, p += size) ループを使って、MSpan 内の各オブジェクトを走査します。
  3. マークビットのチェックとクリア: 各オブジェクトのメモリブロックに対応するビットマップを操作し、bitAllocatedbitMarkedbitSpecial などのビットをチェックします。
    • bitMarked が設定されている場合(オブジェクトがまだ使用されている場合)、bitMarked をクリアして次のGCサイクルに備えます。
    • bitMarked が設定されていない場合(オブジェクトがガベージである場合)、handlespecial を呼び出してファイナライザの処理などを行います。
  4. メモリの解放: ガベージと判断されたオブジェクトは、そのサイズクラスに応じて runtime·MCache_Free(スモールオブジェクトの場合)または runtime·MHeap_Free(ラージオブジェクトの場合)を呼び出して解放されます。また、解放されたメモリブロックの先頭にゼロを書き込むことで、再利用時の安全性を確保しています。
  5. MCache の更新: ローカルキャッシュ m->mcache の統計情報(local_alloc, local_nfree)を更新します。

この分離により、sweepspanMSpan レベルでのスイープ処理の完全なカプセル化を提供します。これは、将来的に複数のスレッドが異なる MSpan を並行してスイープする際に、各スレッドが独立して sweepspan を呼び出すことができるため、並行GCの実装を容易にします。

関連リンク

  • Go言語のガベージコレクションに関する公式ドキュメントやブログ記事(当時の情報にアクセスするのは難しい場合がありますが、GoのGCの進化を追うことで理解が深まります)。
  • Goのランタイムソースコード(特に src/runtime/mgc.gosrc/runtime/mheap.go など、メモリ管理とGCに関連するファイル)。

参考にした情報源リンク

  • Go言語の公式リポジトリ: https://github.com/golang/go
  • Goのガベージコレクションに関する一般的な情報源(例: GoのGCの歴史や進化に関するブログ記事、論文など)。
  • Goのランタイムの内部構造に関する技術解説記事。
  • Goのコードレビューシステム (Gerrit) のCL (Change List) リンク: https://golang.org/cl/5991047 (これはコミットメッセージに記載されているもので、当時のコードレビューの議論を辿るのに役立ちます)