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

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

このコミットは、Goランタイムのメモリ管理における重要な改善を含んでいます。特に、ガベージコレクション(GC)のスイープフェーズにおけるMSpan(メモリ領域の管理単位)の処理方法が変更され、より堅牢で効率的なメカニズムが導入されました。従来のMSpanリストの操作に起因する無限ループの可能性を排除するため、明示的な「全スパンリスト(allspan list)」が導入されました。

コミット

commit 9f726c2c8ba98d55935acc1143d2b792ca74e303
Author: Russ Cox <rsc@golang.org>
Date:   Wed Jan 28 15:22:16 2009 -0800

    Use explicit allspan list instead of
    trying to find all the places where
    spans might be recorded.
    
    Free can cascade into complicated
    span manipulations that move them
    from list to list; the old code had the
    possibility of accidentally processing
    a span twice or jumping to a different
    list, causing an infinite loop.
    
    R=r
    DELTA=70  (28 added, 25 deleted, 17 changed)
    OCL=23704
    CL=23710

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

https://github.com/golang/go/commit/9f726c2c8ba98d55935acc1143d2b792ca74e303

元コミット内容

このコミットの元のメッセージは以下の通りです。

「スパンが記録される可能性のあるすべての場所を探そうとする代わりに、明示的な全スパンリストを使用する。

Freeは、スパンをリストからリストへ移動させる複雑なスパン操作に連鎖する可能性があり、古いコードでは、誤ってスパンを二重に処理したり、別のリストにジャンプしたりして、無限ループを引き起こす可能性があった。」

変更の背景

Goランタイムの初期のメモリ管理システムでは、MSpanと呼ばれるメモリ領域の単位が、その状態(使用中、空きなど)やサイズクラスに応じて様々なリンクリストに管理されていました。特に、メモリの解放(Free)処理中に、MSpanがこれらのリスト間を移動することがありました。

このリスト間の移動が複雑になると、ガベージコレクタのスイープフェーズで問題が発生する可能性がありました。具体的には、スイープ処理が進行中にMSpanが別のリストに移動したり、同じMSpanが複数のリストに存在するように見えたりすることで、ガベージコレクタが同じMSpanを複数回処理しようとしたり、予期せぬリストにジャンプしてしまったりする可能性がありました。このような状況は、ガベージコレクタが無限ループに陥る原因となり、システムの安定性を著しく損なう可能性がありました。

このコミットは、この根本的な問題を解決するために、すべてのMSpanを追跡する単一の「全スパンリスト」を導入することで、GCのスイープ処理を簡素化し、堅牢性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムのメモリ管理に関する基本的な概念を理解しておく必要があります。

  • Goのメモリ管理の基本: Goは独自のガベージコレクタ(GC)を持つランタイムシステムです。プログラムがメモリを要求すると、ランタイムはヒープからメモリを割り当てます。不要になったメモリはGCによって自動的に解放されます。
  • MSpan: Goランタイムのメモリ管理における基本的な単位です。これは、連続した物理メモリページ(通常は8KB)のブロックを表します。MSpanは、オブジェクトの割り当て、解放、およびGCによる追跡のために使用されます。
  • MHeap: Goランタイムのグローバルヒープを表す構造体です。MHeapは、すべてのMSpanを管理し、メモリの割り当てと解放を調整します。MHeap内には、様々なサイズのMSpanを管理するためのリスト(例: mheap.central)が存在します。
  • FixAlloc: Goランタイム内部で使用される固定サイズのアロケータです。これは、MSpanMCacheのような、ランタイム自身が頻繁に割り当て・解放する小さな固定サイズのオブジェクトのために設計されています。FixAllocは、大きなメモリチャンクを一度に取得し、そこから固定サイズのオブジェクトを効率的に切り出して提供します。
  • ガベージコレクションのスイープフェーズ: GoのGCは、マーク&スイープアルゴリズムをベースにしています。マークフェーズで到達可能なオブジェクトを特定した後、スイープフェーズでは、到達不能とマークされたオブジェクトが占めていたメモリを解放し、再利用可能な状態に戻します。このフェーズでは、MSpanの状態が更新され、空きリストに再追加されることがあります。
  • リンクリスト: データ構造の一種で、各要素(ノード)が次の要素へのポインタ(または参照)を持つことで、要素が線形に連結されています。Goのメモリ管理では、MSpanを管理するために様々なリンクリストが使用されていました。

技術的詳細

このコミットの主要な技術的変更点は、MSpanの管理方法とGCのスイープ処理のロジックにあります。

  1. MSpan構造体の拡張:

    • src/runtime/malloc.hにおいて、MSpan構造体にallnextフィールドが追加されました。これは、すべてのMSpanを連結する単一のリンクリストを形成するためのポインタです。
    • MSpanの状態を表すenumMSpanListHeadMSpanDeadが追加されました。MSpanListHeadはリストのヘッドであることを示し、MSpanDeadはスパンが解放され、もはや使用されていないことを示します。
  2. MHeap構造体の拡張:

    • src/runtime/malloc.hにおいて、MHeap構造体にallspansフィールドが追加されました。これは、すべてのMSpanを追跡する新しいリンクリストのヘッドポインタとなります。
  3. FixAllocの機能拡張とRecordSpanの導入:

    • src/runtime/malloc.hsrc/runtime/mfixalloc.cにおいて、FixAlloc_Init関数に新しい引数first(コールバック関数)とargが追加されました。これにより、FixAllocが新しいオブジェクトを割り当てる際に、カスタムのコールバック関数を実行できるようになりました。
    • src/runtime/mheap.cRecordSpanという新しい関数が導入されました。この関数は、FixAllocによって新しいMSpanが割り当てられるたびに呼び出されるコールバックとして機能します。RecordSpanは、新しく割り当てられたMSpanmheap.allspansリンクリストの先頭に追加します。これにより、すべてのMSpanが自動的にこのグローバルリストに登録されるようになります。
  4. GCスイープ処理の変更:

    • src/runtime/mgc0.cにおいて、sweep関数のロジックが大幅に変更されました。従来のコードでは、mheap.central配列内の各サイズクラスに対応するnonemptyおよびemptyリストを繰り返し処理してMSpanをスイープしていました。
    • 新しい実装では、sweep関数はmheap.allspansリストを直接イテレートするようになりました。これにより、GCはすべてのMSpanを一度に、かつ確実に処理できるようになり、リスト間の移動による二重処理や無限ループのリスクが排除されます。sweepspanlist関数は削除されました。
  5. MSpanの解放と状態管理の改善:

    • src/runtime/mheap.cMHeap_FreeLocked関数において、MSpanが解放され、隣接する空きスパンと結合される際に、結合されたスパンの古い状態がMSpanDeadに設定されるようになりました。これは、GCがもはや存在しないスパンを誤って参照しないようにするための重要な変更です。
    • MSpanList_Init関数で、リストのヘッドのstateMSpanListHeadに設定されるようになりました。

これらの変更により、Goランタイムのメモリ管理は、特にGCのスイープフェーズにおいて、より予測可能で堅牢な動作をするようになりました。

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

このコミットにおける主要なコード変更は以下のファイルに集中しています。

  • src/runtime/malloc.h:
    • FixAlloc構造体にfirstargフィールドが追加され、FixAlloc_Init関数のシグネチャが変更されました。
    • MSpan構造体にallnextフィールドが追加されました。
    • MSpanの状態を表すenumMSpanListHeadMSpanDeadが追加されました。
    • MHeap構造体にallspansフィールドが追加されました。
  • src/runtime/mfixalloc.c:
    • FixAlloc_Init関数が新しい引数を受け入れるように変更され、FixAlloc_Alloc関数内でf->firstコールバックが呼び出されるようになりました。
  • src/runtime/mgc0.c:
    • sweepspanlist関数が削除されました。
    • sweep関数がmheap.allspansリストをイテレートするように変更されました。
  • src/runtime/mheap.c:
    • RecordSpan関数が追加され、MHeap_Init内でFixAlloc_Initのコールバックとして登録されました。
    • MHeap_FreeLocked関数で、解放されたスパンの状態がMSpanDeadに設定されるようになりました。
    • MSpanList_Initでリストヘッドの状態が設定されるようになりました。

コアとなるコードの解説

src/runtime/malloc.h の変更

--- a/src/runtime/malloc.h
+++ b/src/runtime/malloc.h
@@ -131,16 +131,20 @@ void	SysUnused(void *v, uintptr nbytes);
 //
 // Memory returned by FixAlloc_Alloc is not zeroed.
 // The caller is responsible for locking around FixAlloc calls.
+// Callers can keep state in the object but the first word is
+// smashed by freeing and reallocating.
 struct FixAlloc
 {
 	uintptr size;
 	void *(*alloc)(uintptr);
+	void (*first)(void *arg, byte *p);	// called first time p is returned
+	void *arg;
 	MLink *list;
 	byte *chunk;
 	uint32 nchunk;
 };
 
-void	FixAlloc_Init(FixAlloc *f, uintptr size, void *(*alloc)(uintptr));
+void	FixAlloc_Init(FixAlloc *f, uintptr size, void *(*alloc)(uintptr), void (*first)(void*, byte*), void *arg);
 void*	FixAlloc_Alloc(FixAlloc *f);
 void	FixAlloc_Free(FixAlloc *f, void *p);
 
@@ -203,18 +207,21 @@ void	MCache_Free(MCache *c, void *p, int32 sizeclass, uintptr size);
 enum
 {
 	MSpanInUse = 0,
-	MSpanFree
+	MSpanFree,
+	MSpanListHead,
+	MSpanDead,
 };
 struct MSpan
 {
 	MSpan	*next;		// in a span linked list
 	MSpan	*prev;		// in a span linked list
+	MSpan	*allnext;		// in the list of all spans
 	PageID	start;		// starting page number
 	uintptr	npages;		// number of pages in span
 	MLink	*freelist;	// list of free objects
 	uint32	ref;		// number of allocated objects in this span
 	uint32	sizeclass;	// size class
-	uint32	state;		// MSpanInUse or MSpanFree
+	uint32	state;		// MSpanInUse etc
 	union {
 		uint32	*gcref;	// sizeclass > 0
 		uint32	gcref0;	// sizeclass == 0
@@ -349,6 +356,7 @@ struct MHeap
 	Lock;
 	MSpan free[MaxMHeapList];	// free lists of given length
 	MSpan large;			// free lists length >= MaxMHeapList
+	MSpan *allspans;
 
 	// span lookup
 	MHeapMap map;

FixAlloc構造体にfirstargが追加され、FixAlloc_Initのシグネチャが変更されたことで、FixAllocがオブジェクトを割り当てる際にカスタムの初期化処理を実行できるようになりました。MSpan構造体にはallnextが追加され、すべてのスパンを連結する新しいリストのポインタとなります。MSpanの状態を示すenumにはMSpanListHeadMSpanDeadが追加され、スパンのライフサイクル管理がより明確になりました。MHeap構造体にはallspansが追加され、これがすべてのMSpanを追跡するリストのヘッドとなります。

src/runtime/mfixalloc.c の変更

--- a/src/runtime/mfixalloc.c
+++ b/src/runtime/mfixalloc.c
@@ -12,10 +12,12 @@
 // Initialize f to allocate objects of the given size,
 // using the allocator to obtain chunks of memory.
 void
-FixAlloc_Init(FixAlloc *f, uintptr size, void *(*alloc)(uintptr))\n
+FixAlloc_Init(FixAlloc *f, uintptr size, void *(*alloc)(uintptr), void (*first)(void*, byte*), void *arg)
 {
 	f->size = size;
 	f->alloc = alloc;
+	f->first = first;
+	f->arg = arg;
 	f->list = nil;
 	f->chunk = nil;
 	f->nchunk = 0;
@@ -38,6 +40,8 @@ FixAlloc_Alloc(FixAlloc *f)
 		f->nchunk = FixAllocChunk;
 	}
 	v = f->chunk;
+	if(f->first)
+		f->first(f->arg, v);
 	f->chunk += f->size;
 	f->nchunk -= f->size;
 	return v;

FixAlloc_Initは新しい引数を受け取り、FixAlloc構造体のfirstargフィールドを初期化します。FixAlloc_Allocでは、新しくオブジェクトが割り当てられた直後にf->firstコールバックが呼び出されるようになりました。これにより、MSpanが割り当てられる際にRecordSpan関数が実行され、allspansリストに自動的に追加されるようになります。

src/runtime/mgc0.c の変更

--- a/src/runtime/mgc0.c
+++ b/src/runtime/mgc0.c
@@ -109,7 +109,7 @@ sweepspan(MSpan *s)
 
 	if(s->state != MSpanInUse)
 		return;
-
+	
 	p = (byte*)(s->start << PageShift);
 	if(s->sizeclass == 0) {
 		// Large block.
@@ -157,33 +157,14 @@ sweepspan(MSpan *s)
 	}
 }
 
-static void
-sweepspanlist(MSpan *list)
-{
-	MSpan *s, *next;
-
-	for(s=list->next; s != list; s=next) {
-		next = s->next;	// in case s gets moved
-		sweepspan(s);
-	}
-}
-
 static void
 sweep(void)
 {
-	int32 i;
+	MSpan *s;
 
 	// Sweep all the spans.
-
-	for(i=0; i<nelem(mheap.central); i++) {
-		// Sweep nonempty (has some free blocks available)
-		// before sweeping empty (is completely allocated),
-		// because finding something to free in a span from empty
-		// will move it into nonempty, and we must not sweep
-		// the same span twice.
-		sweepspanlist(&mheap.central[i].nonempty);
-		sweepspanlist(&mheap.central[i].empty);
-	}
+	for(s = mheap.allspans; s != nil; s = s->allnext)
+		sweepspan(s);
 }
 
 // Semaphore, not Lock, so that the goroutine

sweepspanlist関数が削除され、sweep関数がmheap.allspansリストを直接イテレートするように変更されました。これにより、GCのスイープ処理は、すべてのMSpanを単一のリストから確実に処理できるようになり、従来の複雑なリスト構造に起因する問題を回避します。

src/runtime/mheap.c の変更

--- a/src/runtime/mheap.c
+++ b/src/runtime/mheap.c
@@ -21,14 +21,26 @@ static void MHeap_FreeLocked(MHeap*, MSpan*);\n static MSpan *MHeap_AllocLarge(MHeap*, uintptr);\n static MSpan *BestFit(MSpan*, uintptr, MSpan*);\n \n+static void\n+RecordSpan(void *vh, byte *p)\n+{\n+\tMHeap *h;\n+\tMSpan *s;\n+\n+\th = vh;\n+\ts = (MSpan*)p;\n+\ts->allnext = h->allspans;\n+\th->allspans = s;\n+}\n+\n // Initialize the heap; fetch memory using alloc.\n void\n MHeap_Init(MHeap *h, void *(*alloc)(uintptr))\n {\n 	uint32 i;\n \n-\tFixAlloc_Init(&h->spanalloc, sizeof(MSpan), alloc);\n-\tFixAlloc_Init(&h->cachealloc, sizeof(MCache), alloc);\n+\tFixAlloc_Init(&h->spanalloc, sizeof(MSpan), alloc, RecordSpan, h);\n+\tFixAlloc_Init(&h->cachealloc, sizeof(MCache), alloc, nil, nil);\n 	MHeapMap_Init(&h->map, alloc);\n 	// h->mapcache needs no init\n 	for(i=0; i<nelem(h->free); i++)\n@@ -110,11 +122,6 @@ HaveSpan:\n \t\tfor(n=0; n<npage; n++)\n \t\t\tif(MHeapMapCache_GET(&h->mapcache, s->start+n, tmp) != 0)\n \t\t\t\tMHeapMapCache_SET(&h->mapcache, s->start+n, 0);\n-\n-\t\t// Need a list of large allocated spans.\n-\t\t// They have sizeclass == 0, so use heap.central[0].empty,\n-\t\t// since central[0] is otherwise unused.\n-\t\tMSpanList_Insert(&h->central[0].empty, s);\n \t} else {\n \t\t// Save cache entries for this span.\n \t\t// If there\'s a size class, there aren\'t that many pages.\n@@ -252,12 +259,14 @@ MHeap_FreeLocked(MHeap *h, MSpan *s)\n \t\ts->npages += t->npages;\n \t\tMHeapMap_Set(&h->map, s->start, s);\n \t\tMSpanList_Remove(t);\n+\t\tt->state = MSpanDead;\n \t\tFixAlloc_Free(&h->spanalloc, t);\n \t}\n \tif((t = MHeapMap_Get(&h->map, s->start + s->npages)) != nil && t->state != MSpanInUse) {\n \t\ts->npages += t->npages;\n \t\tMHeapMap_Set(&h->map, s->start + s->npages - 1, s);\n \t\tMSpanList_Remove(t);\n+\t\tt->state = MSpanDead;\n \t\tFixAlloc_Free(&h->spanalloc, t);\n \t}\n \n@@ -395,6 +404,7 @@ MSpan_Init(MSpan *span, PageID start, uintptr npages)\n void\n MSpanList_Init(MSpan *list)\n {\n+\tlist->state = MSpanListHead;\n \tlist->next = list;\n \tlist->prev = list;\n }\n```
`RecordSpan`関数は、`FixAlloc`によって割り当てられた新しい`MSpan`を`h->allspans`リストの先頭にリンクする役割を担います。`MHeap_Init`では、`h->spanalloc`(`MSpan`を割り当てる`FixAlloc`インスタンス)の初期化時に`RecordSpan`がコールバックとして登録されます。これにより、すべての`MSpan`が自動的に`allspans`リストに登録されるようになります。`MHeap_FreeLocked`では、スパンが解放され結合される際に、結合されたスパンの古い状態が`MSpanDead`に設定され、GCが誤って参照しないようにします。

これらの変更は、Goランタイムのメモリ管理の基盤を強化し、GCの信頼性と効率性を向上させる上で重要な役割を果たしました。

## 関連リンク

*   Goのガベージコレクションに関する公式ドキュメント(現在のバージョン): [https://go.dev/doc/gc-guide](https://go.dev/doc/gc-guide)
*   Goのメモリ管理の歴史と進化に関する記事(より深い理解のために): [https://go.dev/blog/go15gc](https://go.dev/blog/go15gc) (Go 1.5でのGCの大きな変更に関する記事ですが、初期のGCの概念を理解するのに役立つ場合があります)

## 参考にした情報源リンク

*   Goのソースコード(特に`src/runtime`ディレクトリ)
*   Goのガベージコレクションに関する技術ブログや論文
*   リンクリスト、固定サイズアロケータなどのデータ構造とアルゴリズムに関する一般的な情報源
*   Goの初期のコミット履歴と関連する議論
# [インデックス 1580] ファイルの概要

このコミットは、Goランタイムのメモリ管理における重要な改善を含んでいます。特に、ガベージコレクション(GC)のスイープフェーズにおける`MSpan`(メモリ領域の管理単位)の処理方法が変更され、より堅牢で効率的なメカニズムが導入されました。従来の`MSpan`リストの操作に起因する無限ループの可能性を排除するため、明示的な「全スパンリスト(allspan list)」が導入されました。

## コミット

commit 9f726c2c8ba98d55935acc1143d2b792ca74e303 Author: Russ Cox rsc@golang.org Date: Wed Jan 28 15:22:16 2009 -0800

Use explicit allspan list instead of
trying to find all the places where
spans might be recorded.

Free can cascade into complicated
span manipulations that move them
from list to list; the old code had the
possibility of accidentally processing
a span twice or jumping to a different
list, causing an infinite loop.

R=r
DELTA=70  (28 added, 25 deleted, 17 changed)
OCL=23704
CL=23710

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

[https://github.com/golang/go/commit/9f726c2c8ba98d55935acc1143d2b792ca74e303](https://github.com/golang/go/commit/9f726c2c8ba98d55935acc1143d2b792ca74e303)

## 元コミット内容

このコミットの元のメッセージは以下の通りです。

「スパンが記録される可能性のあるすべての場所を探そうとする代わりに、明示的な全スパンリストを使用する。

Freeは、スパンをリストからリストへ移動させる複雑なスパン操作に連鎖する可能性があり、古いコードでは、誤ってスパンを二重に処理したり、別のリストにジャンプしたりして、無限ループを引き起こす可能性があった。」

## 変更の背景

Goランタイムの初期のメモリ管理システムでは、`MSpan`と呼ばれるメモリ領域の単位が、その状態(使用中、空きなど)やサイズクラスに応じて様々なリンクリストに管理されていました。特に、メモリの解放(`Free`)処理中に、`MSpan`がこれらのリスト間を移動することがありました。

このリスト間の移動が複雑になると、ガベージコレクタのスイープフェーズで問題が発生する可能性がありました。具体的には、スイープ処理が進行中に`MSpan`が別のリストに移動したり、同じ`MSpan`が複数のリストに存在するように見えたりすることで、ガベージコレクタが同じ`MSpan`を複数回処理しようとしたり、予期せぬリストにジャンプしてしまったりする可能性がありました。このような状況は、ガベージコレクタが無限ループに陥る原因となり、システムの安定性を著しく損なう可能性がありました。

このコミットは、この根本的な問題を解決するために、すべての`MSpan`を追跡する単一の「全スパンリスト」を導入することで、GCのスイープ処理を簡素化し、堅牢性を向上させることを目的としています。

## 前提知識の解説

このコミットを理解するためには、以下のGoランタイムのメモリ管理に関する基本的な概念を理解しておく必要があります。

*   **Goのメモリ管理の基本**: Goは独自のガベージコレクタ(GC)を持つランタイムシステムです。プログラムがメモリを要求すると、ランタイムはヒープからメモリを割り当てます。不要になったメモリはGCによって自動的に解放されます。
*   **`MSpan`**: Goランタイムのメモリ管理における基本的な単位です。これは、連続した物理メモリページ(通常は8KB)のブロックを表します。`MSpan`は、オブジェクトの割り当て、解放、およびGCによる追跡のために使用されます。
*   **`MHeap`**: Goランタイムのグローバルヒープを表す構造体です。`MHeap`は、すべての`MSpan`を管理し、メモリの割り当てと解放を調整します。`MHeap`内には、様々なサイズの`MSpan`を管理するためのリスト(例: `mheap.central`)が存在します。
*   **`FixAlloc`**: Goランタイム内部で使用される固定サイズのアロケータです。これは、`MSpan`や`MCache`のような、ランタイム自身が頻繁に割り当て・解放する小さな固定サイズのオブジェクトのために設計されています。`FixAlloc`は、大きなメモリチャンクを一度に取得し、そこから固定サイズのオブジェクトを効率的に切り出して提供します。
*   **ガベージコレクションのスイープフェーズ**: GoのGCは、マーク&スイープアルゴリズムをベースにしています。マークフェーズで到達可能なオブジェクトを特定した後、スイープフェーズでは、到達不能とマークされたオブジェクトが占めていたメモリを解放し、再利用可能な状態に戻します。このフェーズでは、`MSpan`の状態が更新され、空きリストに再追加されることがあります。
*   **リンクリスト**: データ構造の一種で、各要素(ノード)が次の要素へのポインタ(または参照)を持つことで、要素が線形に連結されています。Goのメモリ管理では、`MSpan`を管理するために様々なリンクリストが使用されていました。

## 技術的詳細

このコミットの主要な技術的変更点は、`MSpan`の管理方法とGCのスイープ処理のロジックにあります。

1.  **`MSpan`構造体の拡張**:
    *   `src/runtime/malloc.h`において、`MSpan`構造体に`allnext`フィールドが追加されました。これは、すべての`MSpan`を連結する単一のリンクリストを形成するためのポインタです。
    *   `MSpan`の状態を表す`enum`に`MSpanListHead`と`MSpanDead`が追加されました。`MSpanListHead`はリストのヘッドであることを示し、`MSpanDead`はスパンが解放され、もはや使用されていないことを示します。

2.  **`MHeap`構造体の拡張**:
    *   `src/runtime/malloc.h`において、`MHeap`構造体に`allspans`フィールドが追加されました。これは、すべての`MSpan`を追跡する新しいリンクリストのヘッドポインタとなります。

3.  **`FixAlloc`の機能拡張と`RecordSpan`の導入**:
    *   `src/runtime/malloc.h`と`src/runtime/mfixalloc.c`において、`FixAlloc_Init`関数に新しい引数`first`(コールバック関数)と`arg`が追加されました。これにより、`FixAlloc`が新しいオブジェクトを割り当てる際に、カスタムのコールバック関数を実行できるようになりました。
    *   `src/runtime/mheap.c`に`RecordSpan`という新しい関数が導入されました。この関数は、`FixAlloc`によって新しい`MSpan`が割り当てられるたびに呼び出されるコールバックとして機能します。`RecordSpan`は、新しく割り当てられた`MSpan`を`mheap.allspans`リンクリストの先頭に追加します。これにより、すべての`MSpan`が自動的にこのグローバルリストに登録されるようになります。

4.  **GCスイープ処理の変更**:
    *   `src/runtime/mgc0.c`において、`sweep`関数のロジックが大幅に変更されました。従来のコードでは、`mheap.central`配列内の各サイズクラスに対応する`nonempty`および`empty`リストを繰り返し処理して`MSpan`をスイープしていました。
    *   新しい実装では、`sweep`関数は`mheap.allspans`リストを直接イテレートするようになりました。これにより、GCはすべての`MSpan`を一度に、かつ確実に処理できるようになり、リスト間の移動による二重処理や無限ループのリスクが排除されます。`sweepspanlist`関数は削除されました。

5.  **`MSpan`の解放と状態管理の改善**:
    *   `src/runtime/mheap.c`の`MHeap_FreeLocked`関数において、`MSpan`が解放され、隣接する空きスパンと結合される際に、結合されたスパンの古い状態が`MSpanDead`に設定されるようになりました。これは、GCがもはや存在しないスパンを誤って参照しないようにするための重要な変更です。
    *   `MSpanList_Init`関数で、リストのヘッドの`state`が`MSpanListHead`に設定されるようになりました。

これらの変更により、Goランタイムのメモリ管理は、特にGCのスイープフェーズにおいて、より予測可能で堅牢な動作をするようになりました。

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

このコミットにおける主要なコード変更は以下のファイルに集中しています。

*   **`src/runtime/malloc.h`**:
    *   `FixAlloc`構造体に`first`と`arg`フィールドが追加され、`FixAlloc_Init`関数のシグネチャが変更されました。
    *   `MSpan`構造体に`allnext`フィールドが追加されました。
    *   `MSpan`の状態を表す`enum`に`MSpanListHead`と`MSpanDead`が追加されました。
    *   `MHeap`構造体に`allspans`フィールドが追加されました。
*   **`src/runtime/mfixalloc.c`**:
    *   `FixAlloc_Init`関数が新しい引数を受け入れるように変更され、`FixAlloc_Alloc`関数内で`f->first`コールバックが呼び出されるようになりました。
*   **`src/runtime/mgc0.c`**:
    *   `sweepspanlist`関数が削除されました。
    *   `sweep`関数が`mheap.allspans`リストをイテレートするように変更されました。
*   **`src/runtime/mheap.c`**:
    *   `RecordSpan`関数が追加され、`MHeap_Init`内で`FixAlloc_Init`のコールバックとして登録されました。
    *   `MHeap_FreeLocked`関数で、解放されたスパンの状態が`MSpanDead`に設定されるようになりました。
    *   `MSpanList_Init`でリストヘッドの状態が設定されるようになりました。

## コアとなるコードの解説

### `src/runtime/malloc.h` の変更

```diff
--- a/src/runtime/malloc.h
+++ b/src/runtime/malloc.h
@@ -131,16 +131,20 @@ void	SysUnused(void *v, uintptr nbytes);
 //
 // Memory returned by FixAlloc_Alloc is not zeroed.
 // The caller is responsible for locking around FixAlloc calls.
+// Callers can keep state in the object but the first word is
+// smashed by freeing and reallocating.
 struct FixAlloc
 {
 	uintptr size;
 	void *(*alloc)(uintptr);
+	void (*first)(void *arg, byte *p);	// called first time p is returned
+	void *arg;
 	MLink *list;
 	byte *chunk;
 	uint32 nchunk;
 };
 
-void	FixAlloc_Init(FixAlloc *f, uintptr size, void *(*alloc)(uintptr));
+void	FixAlloc_Init(FixAlloc *f, uintptr size, void *(*alloc)(uintptr), void (*first)(void*, byte*), void *arg);
 void*	FixAlloc_Alloc(FixAlloc *f);
 void	FixAlloc_Free(FixAlloc *f, void *p);
 
@@ -203,18 +207,21 @@ void	MCache_Free(MCache *c, void *p, int32 sizeclass, uintptr size);
 enum
 {
 	MSpanInUse = 0,
-	MSpanFree
+	MSpanFree,
+	MSpanListHead,
+	MSpanDead,
 };
 struct MSpan
 {
 	MSpan	*next;		// in a span linked list
 	MSpan	*prev;		// in a span linked list
+	MSpan	*allnext;		// in the list of all spans
 	PageID	start;		// starting page number
 	uintptr	npages;		// number of pages in span
 	MLink	*freelist;	// list of free objects
 	uint32	ref;		// number of allocated objects in this span
 	uint32	sizeclass;	// size class
-	uint32	state;		// MSpanInUse or MSpanFree
+	uint32	state;		// MSpanInUse etc
 	union {
 		uint32	*gcref;	// sizeclass > 0
 		uint32	gcref0;	// sizeclass == 0
@@ -349,6 +356,7 @@ struct MHeap
 	Lock;
 	MSpan free[MaxMHeapList];	// free lists of given length
 	MSpan large;			// free lists length >= MaxMHeapList
+	MSpan *allspans;
 
 	// span lookup
 	MHeapMap map;

FixAlloc構造体にfirstargが追加され、FixAlloc_Initのシグネチャが変更されたことで、FixAllocがオブジェクトを割り当てる際にカスタムの初期化処理を実行できるようになりました。MSpan構造体にはallnextが追加され、すべてのスパンを連結する新しいリストのポインタとなります。MSpanの状態を示すenumにはMSpanListHeadMSpanDeadが追加され、スパンのライフサイクル管理がより明確になりました。MHeap構造体にはallspansが追加され、これがすべてのMSpanを追跡するリストのヘッドとなります。

src/runtime/mfixalloc.c の変更

--- a/src/runtime/mfixalloc.c
+++ b/src/runtime/mfixalloc.c
@@ -12,10 +12,12 @@
 // Initialize f to allocate objects of the given size,
 // using the allocator to obtain chunks of memory.
 void
-FixAlloc_Init(FixAlloc *f, uintptr size, void *(*alloc)(uintptr))\n
+FixAlloc_Init(FixAlloc *f, uintptr size, void *(*alloc)(uintptr), void (*first)(void*, byte*), void *arg)
 {
 	f->size = size;
 	f->alloc = alloc;
+	f->first = first;
+	f->arg = arg;
 	f->list = nil;
 	f->chunk = nil;
 	f->nchunk = 0;
@@ -38,6 +40,8 @@ FixAlloc_Alloc(FixAlloc *f)
 		f->nchunk = FixAllocChunk;
 	}
 	v = f->chunk;
+	if(f->first)
+		f->first(f->arg, v);
 	f->chunk += f->size;
 	f->nchunk -= f->size;
 	return v;

FixAlloc_Initは新しい引数を受け取り、FixAlloc構造体のfirstargフィールドを初期化します。FixAlloc_Allocでは、新しくオブジェクトが割り当てられた直後にf->firstコールバックが呼び出されるようになりました。これにより、MSpanが割り当てられる際にRecordSpan関数が実行され、allspansリストに自動的に追加されるようになります。

src/runtime/mgc0.c の変更

--- a/src/runtime/mgc0.c
+++ b/src/runtime/mgc0.c
@@ -109,7 +109,7 @@ sweepspan(MSpan *s)
 
 	if(s->state != MSpanInUse)
 		return;
-
+	
 	p = (byte*)(s->start << PageShift);
 	if(s->sizeclass == 0) {
 		// Large block.
@@ -157,33 +157,14 @@ sweepspan(MSpan *s)
 	}
 }
 
-static void
-sweepspanlist(MSpan *list)
-{
-	MSpan *s, *next;
-
-	for(s=list->next; s != list; s=next) {
-		next = s->next;	// in case s gets moved
-		sweepspan(s);
-	}
-}
-
 static void
 sweep(void)
 {
-	int32 i;
+	MSpan *s;
 
 	// Sweep all the spans.
-
-	for(i=0; i<nelem(mheap.central); i++) {
-		// Sweep nonempty (has some free blocks available)
-		// before sweeping empty (is completely allocated),
-		// because finding something to free in a span from empty
-		// will move it into nonempty, and we must not sweep
-		// the same span twice.
-		sweepspanlist(&mheap.central[i].nonempty);
-		sweepspanlist(&mheap.central[i].empty);
-	}
+	for(s = mheap.allspans; s != nil; s = s->allnext)
+		sweepspan(s);
 }
 
 // Semaphore, not Lock, so that the goroutine

sweepspanlist関数が削除され、sweep関数がmheap.allspansリストを直接イテレートするように変更されました。これにより、GCのスイープ処理は、すべてのMSpanを単一のリストから確実に処理できるようになり、従来の複雑なリスト構造に起因する問題を回避します。

src/runtime/mheap.c の変更

--- a/src/runtime/mheap.c
+++ b/src/runtime/mheap.c
@@ -21,14 +21,26 @@ static void MHeap_FreeLocked(MHeap*, MSpan*);\n static MSpan *MHeap_AllocLarge(MHeap*, uintptr);\n static MSpan *BestFit(MSpan*, uintptr, MSpan*);\n \n+static void\n+RecordSpan(void *vh, byte *p)\n+{\n+\tMHeap *h;\n+\tMSpan *s;\n+\n+\th = vh;\n+\ts = (MSpan*)p;\n+\ts->allnext = h->allspans;\n+\th->allspans = s;\n+}\n+\n // Initialize the heap; fetch memory using alloc.\n void
 MHeap_Init(MHeap *h, void *(*alloc)(uintptr))\n {\n 	uint32 i;\n \n-\tFixAlloc_Init(&h->spanalloc, sizeof(MSpan), alloc);\n-\tFixAlloc_Init(&h->cachealloc, sizeof(MCache), alloc);\n+\tFixAlloc_Init(&h->spanalloc, sizeof(MSpan), alloc, RecordSpan, h);\n+\tFixAlloc_Init(&h->cachealloc, sizeof(MCache), alloc, nil, nil);\n 	MHeapMap_Init(&h->map, alloc);\n 	// h->mapcache needs no init\n 	for(i=0; i<nelem(h->free); i++)\n@@ -110,11 +122,6 @@ HaveSpan:\n \t\tfor(n=0; n<npage; n++)\n \t\t\tif(MHeapMapCache_GET(&h->mapcache, s->start+n, tmp) != 0)\n \t\t\t\tMHeapMapCache_SET(&h->mapcache, s->start+n, 0);\n-\n-\t\t// Need a list of large allocated spans.\n-\t\t// They have sizeclass == 0, so use heap.central[0].empty,\n-\t\t// since central[0] is otherwise unused.\n-\t\tMSpanList_Insert(&h->central[0].empty, s);\n \t} else {\n \t\t// Save cache entries for this span.\n \t\t// If there\'s a size class, there aren\'t that many pages.\n@@ -252,12 +259,14 @@ MHeap_FreeLocked(MHeap *h, MSpan *s)\n \t\ts->npages += t->npages;\n \t\tMHeapMap_Set(&h->map, s->start, s);\n \t\tMSpanList_Remove(t);\n+\t\tt->state = MSpanDead;\n \t\tFixAlloc_Free(&h->spanalloc, t);\n \t}\n \tif((t = MHeapMap_Get(&h->map, s->start + s->npages)) != nil && t->state != MSpanInUse) {\n \t\ts->npages += t->npages;\n \t\tMHeapMap_Set(&h->map, s->start + s->npages - 1, s);\n \t\tMSpanList_Remove(t);\n+\t\tt->state = MSpanDead;\n \t\tFixAlloc_Free(&h->spanalloc, t);\n \t}\n \n@@ -395,6 +404,7 @@ MSpan_Init(MSpan *span, PageID start, uintptr npages)\n void\n MSpanList_Init(MSpan *list)\n {\n+\tlist->state = MSpanListHead;\n \tlist->next = list;\n \tlist->prev = list;\n }\n```
`RecordSpan`関数は、`FixAlloc`によって割り当てられた新しい`MSpan`を`h->allspans`リストの先頭にリンクする役割を担います。`MHeap_Init`では、`h->spanalloc`(`MSpan`を割り当てる`FixAlloc`インスタンス)の初期化時に`RecordSpan`がコールバックとして登録されます。これにより、すべての`MSpan`が自動的に`allspans`リストに登録されるようになります。`MHeap_FreeLocked`では、スパンが解放され結合される際に、結合されたスパンの古い状態が`MSpanDead`に設定され、GCが誤って参照しないようにします。

これらの変更は、Goランタイムのメモリ管理の基盤を強化し、GCの信頼性と効率性を向上させる上で重要な役割を果たしました。

## 関連リンク

*   Goのガベージコレクションに関する公式ドキュメント(現在のバージョン): [https://go.dev/doc/gc-guide](https://go.dev/doc/gc-guide)
*   Goのメモリ管理の歴史と進化に関する記事(より深い理解のために): [https://go.dev/blog/go15gc](https://go.dev/blog/go15gc) (Go 1.5でのGCの大きな変更に関する記事ですが、初期のGCの概念を理解するのに役立つ場合があります)

## 参考にした情報源リンク

*   Goのソースコード(特に`src/runtime`ディレクトリ)
*   Goのガベージコレクションに関する技術ブログや論文
*   リンクリスト、固定サイズアロケータなどのデータ構造とアルゴリズムに関する一般的な情報源
*   Goの初期のコミット履歴と関連する議論
*   Web search results for "Go runtime MSpan MHeap FixAlloc early versions" (Google Search)