[インデックス 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ランタイム内部で使用される固定サイズのアロケータです。これは、MSpan
やMCache
のような、ランタイム自身が頻繁に割り当て・解放する小さな固定サイズのオブジェクトのために設計されています。FixAlloc
は、大きなメモリチャンクを一度に取得し、そこから固定サイズのオブジェクトを効率的に切り出して提供します。- ガベージコレクションのスイープフェーズ: GoのGCは、マーク&スイープアルゴリズムをベースにしています。マークフェーズで到達可能なオブジェクトを特定した後、スイープフェーズでは、到達不能とマークされたオブジェクトが占めていたメモリを解放し、再利用可能な状態に戻します。このフェーズでは、
MSpan
の状態が更新され、空きリストに再追加されることがあります。 - リンクリスト: データ構造の一種で、各要素(ノード)が次の要素へのポインタ(または参照)を持つことで、要素が線形に連結されています。Goのメモリ管理では、
MSpan
を管理するために様々なリンクリストが使用されていました。
技術的詳細
このコミットの主要な技術的変更点は、MSpan
の管理方法とGCのスイープ処理のロジックにあります。
-
MSpan
構造体の拡張:src/runtime/malloc.h
において、MSpan
構造体にallnext
フィールドが追加されました。これは、すべてのMSpan
を連結する単一のリンクリストを形成するためのポインタです。MSpan
の状態を表すenum
にMSpanListHead
とMSpanDead
が追加されました。MSpanListHead
はリストのヘッドであることを示し、MSpanDead
はスパンが解放され、もはや使用されていないことを示します。
-
MHeap
構造体の拡張:src/runtime/malloc.h
において、MHeap
構造体にallspans
フィールドが追加されました。これは、すべてのMSpan
を追跡する新しいリンクリストのヘッドポインタとなります。
-
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
が自動的にこのグローバルリストに登録されるようになります。
-
GCスイープ処理の変更:
src/runtime/mgc0.c
において、sweep
関数のロジックが大幅に変更されました。従来のコードでは、mheap.central
配列内の各サイズクラスに対応するnonempty
およびempty
リストを繰り返し処理してMSpan
をスイープしていました。- 新しい実装では、
sweep
関数はmheap.allspans
リストを直接イテレートするようになりました。これにより、GCはすべてのMSpan
を一度に、かつ確実に処理できるようになり、リスト間の移動による二重処理や無限ループのリスクが排除されます。sweepspanlist
関数は削除されました。
-
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
の変更
--- 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
構造体にfirst
とarg
が追加され、FixAlloc_Init
のシグネチャが変更されたことで、FixAlloc
がオブジェクトを割り当てる際にカスタムの初期化処理を実行できるようになりました。MSpan
構造体にはallnext
が追加され、すべてのスパンを連結する新しいリストのポインタとなります。MSpan
の状態を示すenum
にはMSpanListHead
とMSpanDead
が追加され、スパンのライフサイクル管理がより明確になりました。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
構造体のfirst
とarg
フィールドを初期化します。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
構造体にfirst
とarg
が追加され、FixAlloc_Init
のシグネチャが変更されたことで、FixAlloc
がオブジェクトを割り当てる際にカスタムの初期化処理を実行できるようになりました。MSpan
構造体にはallnext
が追加され、すべてのスパンを連結する新しいリストのポインタとなります。MSpan
の状態を示すenum
にはMSpanListHead
とMSpanDead
が追加され、スパンのライフサイクル管理がより明確になりました。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
構造体のfirst
とarg
フィールドを初期化します。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)