[インデックス 16370] ファイルの概要
このコミットは、Goランタイムのメモリ管理におけるMCache
の構造とロジックを簡素化することを目的としています。具体的には、tcmalloc
から継承されていたnlistmin
やsize
といった閾値管理を削除し、Goのメモリ割り当ての特性に合わせて最適化しています。これにより、コードの複雑性が減少し、より効率的なメモリ管理が実現されます。
コミット
commit c4cfef075edcbf93919933152ceede9948595d15a
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed May 22 13:29:17 2013 +0400
runtime: simplify MCache
The nlistmin/size thresholds are copied from tcmalloc,
but are unnecesary for Go malloc. We do not do explicit
frees into MCache. For sparse cases when we do (mainly hashmap),
simpler logic will do.
R=rsc, dave, iant
CC=gobot, golang-dev, r, remyoudompheng
https://golang.org/cl/9373043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c4cfef075edcbf9391933152ceede9948595d15a
元コミット内容
このコミットの元々の内容は、Goランタイムのメモリ管理サブシステムであるMCache
の簡素化です。特に、MCache
が保持していたnlistmin
(リストの最小要素数)とsize
(キャッシュの合計サイズ)という閾値管理ロジックが、Goのメモリ割り当ての挙動には不要であると判断され、これらを削除することが主眼となっています。これらの閾値は、GoogleのC++用メモリ割り当てライブラリであるtcmalloc
から借用されたものでしたが、Goのガベージコレクションとメモリ割り当ての特性とは合致しない部分がありました。
変更の背景
Goランタイムのメモリ管理は、tcmalloc
(Thread-Caching Malloc)の設計思想に強く影響を受けています。tcmalloc
は、スレッドごとに小さなオブジェクトのキャッシュ(thread-local cache)を持ち、アロケーションの高速化を図ることで、マルチスレッド環境でのメモリ割り当ての競合を減らすことを目的としています。GoのMCache
(Memory Cache)も同様に、各P(Processor、Goスケジューラにおける論理プロセッサ)に紐付けられたローカルキャッシュとして機能し、小さなオブジェクトの割り当てを高速化します。
しかし、tcmalloc
の設計には、明示的なメモリ解放(free
)が頻繁に行われるC++のワークロードを想定した最適化が含まれています。具体的には、tcmalloc
では、スレッドローカルキャッシュが一定の閾値を超えた場合や、キャッシュ内のオブジェクト数が最小閾値を下回った場合に、中央のヒープにメモリを返却するロジックが存在します。これは、キャッシュのサイズを適切に保ち、メモリの断片化を防ぐためのものです。
Goのメモリ管理は、ガベージコレクション(GC)によって自動的にメモリが管理されるため、C++のようにプログラマが明示的にfree
を呼び出すことは稀です。GoのMCache
への明示的な解放は、主にハッシュマップのような特定のデータ構造で発生するスパースなケースに限られます。このため、tcmalloc
からコピーされたnlistmin
やsize
といった閾値に基づく複雑なキャッシュ管理ロジックは、Goのメモリ割り当てパターンにおいては過剰であり、不要なオーバーヘッドや複雑性をもたらしていました。
このコミットの背景には、Goのメモリ管理をGo自身の特性(GCによる自動管理、明示的な解放の少なさ)に合わせて最適化し、tcmalloc
の設計に盲目的に従うのではなく、よりシンプルで効率的な実装を目指すという意図があります。
前提知識の解説
このコミットを理解するためには、以下のGoランタイムのメモリ管理に関する基本的な概念を理解しておく必要があります。
-
Goのメモリ管理の階層構造:
- MHeap (Memory Heap): グローバルなヒープ領域で、すべてのGoプログラムが使用するメモリの大部分を管理します。大きなオブジェクトや、
MCache
から溢れたオブジェクトの割り当て、およびMSpan
の管理を行います。 - MCentral (Memory Central):
MHeap
とMCache
の中間に位置し、特定のサイズクラスのオブジェクトを管理するMSpan
のリストを保持します。MCache
がメモリを要求すると、MCentral
からMSpan
が提供され、MCache
がメモリを解放すると、MCentral
に返却されます。 - MCache (Memory Cache): 各P(論理プロセッサ)に紐付けられたスレッドローカルなキャッシュです。小さなオブジェクトの割り当てを高速化するために使用されます。Goルーチンがメモリを割り当てる際、まず自身のPに紐付けられた
MCache
からメモリを取得しようとします。これにより、グローバルなロックの競合を減らし、並行性を高めます。
- MHeap (Memory Heap): グローバルなヒープ領域で、すべてのGoプログラムが使用するメモリの大部分を管理します。大きなオブジェクトや、
-
MSpan (Memory Span): 連続したページ(通常は8KB)のブロックを表します。
MHeap
によって管理され、MCentral
を通じてMCache
に提供されます。MSpan
は、特定のサイズクラスのオブジェクトを格納するために分割されます。 -
MLink: メモリブロックを連結リストとして管理するための構造体です。解放されたオブジェクトは
MLink
として連結リストに格納され、再利用されます。 -
サイズクラス (Size Class): Goのメモリ管理では、オブジェクトのサイズに応じて事前に定義された「サイズクラス」に分類されます。これにより、異なるサイズのオブジェクトに対して効率的なメモリ割り当てと管理が行われます。
-
tcmalloc (Thread-Caching Malloc): Googleが開発したC++用の高性能メモリ割り当てライブラリです。Goのメモリ管理は、その設計思想(特にスレッドローカルキャッシュの概念)に強く影響を受けています。
tcmalloc
は、スレッドごとに小さなオブジェクトのキャッシュを持ち、中央のヒープと連携して動作します。 -
ガベージコレクション (GC): Goのランタイムは、不要になったメモリを自動的に回収するガベージコレクタを備えています。これにより、プログラマは手動でのメモリ解放から解放され、メモリリークのリスクが軽減されます。GCの存在は、
tcmalloc
のような明示的なfree
を前提としたメモリ管理戦略とは異なるアプローチをGoに要求します。
技術的詳細
このコミットでは、主に以下の技術的変更が行われています。
-
MCache
構造体からのsize
フィールドの削除:src/pkg/runtime/malloc.h
において、MCache
構造体からuintptr size;
フィールドが削除されました。このフィールドは、MCache
が現在保持しているメモリの合計バイト数を追跡するために使用されていましたが、Goのメモリ管理の特性上、その必要性が低いと判断されました。
-
MCacheList
構造体からのnlistmin
フィールドの削除:src/pkg/runtime/malloc.h
において、MCacheList
構造体からuint32 nlistmin;
フィールドが削除されました。このフィールドは、MCacheList
が過去に保持していたオブジェクトの最小数を記録し、キャッシュのスカベンジング(不要なメモリの返却)の判断基準として使用されていましたが、これも不要とされました。
-
MCache_Alloc
関数の簡素化:src/pkg/runtime/mcache.c
のruntime·MCache_Alloc
関数において、MCentral_AllocList
からの戻り値の処理が簡素化されました。以前は、n
(割り当てられたオブジェクト数)とfirst
(最初のオブジェクトへのポインタ)を別々に受け取っていましたが、変更後はl->nlist
に直接オブジェクト数を、l->list
に最初のオブジェクトへのポインタを格納するように変更されました。これにより、MCache
への補充ロジックがより直接的になりました。
-
ReleaseN
関数の変更とMCache_Free
からのスカベンジングロジックの削除:src/pkg/runtime/mcache.c
のReleaseN
関数から、MCache
のsize
フィールドを更新するロジックが削除されました。- 最も大きな変更は、
runtime·MCache_Free
関数から、MaxMCacheListLen
とMaxMCacheSize
に基づく複雑なスカベンジング(キャッシュから中央ヒープへのメモリ返却)ロジックが完全に削除されたことです。以前は、MCacheList
のオブジェクト数がMaxMCacheListLen
を超えた場合や、MCache
の合計サイズがMaxMCacheSize
を超えた場合に、キャッシュの一部を中央ヒープに返却する処理が行われていました。このロジックはtcmalloc
の設計に由来するものですが、Goのメモリ管理では不要と判断されました。 - 代わりに、
MCache_Free
では、l->nlist
(現在のリストのオブジェクト数)が、MCentral
から一度に転送されるスパンのオブジェクト数の2倍以上になった場合に、リストの半分をReleaseN
を通じて中央ヒープに返却するという、よりシンプルなロジックが導入されました。これは、スパン単位での転送を考慮した、よりGoらしい最適化です。
-
MCache_ReleaseAll
関数の簡素化:src/pkg/runtime/mcache.c
のruntime·MCache_ReleaseAll
関数において、nlistmin
の初期化が不要になったため、関連する行が削除されました。また、ReleaseN
を呼び出す代わりに、直接MCentral_FreeList
を呼び出して、MCache
内のすべてのオブジェクトを中央ヒープに返却するように変更されました。
-
MCentral_FreeList
関数の引数変更と簡素化:src/pkg/runtime/mcentral.c
のruntime·MCentral_FreeList
関数において、引数からint32 n
(解放するオブジェクト数)が削除され、MLink *start
(解放するオブジェクトリストの先頭)のみを受け取るようになりました。これは、MCache
側でオブジェクト数を管理する必要がなくなったためです。- 関数内部のループも簡素化され、
start
ポインタを直接進めることで、リストのすべてのオブジェクトを中央ヒープに解放するように変更されました。
これらの変更により、MCache
の管理ロジックが大幅に簡素化され、Goのメモリ割り当ての特性により適合するようになりました。特に、tcmalloc
由来の複雑な閾値管理とスカベンジングロジックが削除されたことで、コードの可読性と保守性が向上し、不要なオーバーヘッドが削減される可能性があります。
コアとなるコードの変更箇所
このコミットにおける主要な変更は、以下の3つのファイルに集中しています。
-
src/pkg/runtime/malloc.h
:MCacheList
構造体からuint32 nlistmin;
が削除。MCache
構造体からuintptr size;
が削除。MaxMCacheListLen
とMaxMCacheSize
の定義が削除。runtime·MCentral_FreeList
関数のプロトタイプ宣言からint32 n
引数が削除。
-
src/pkg/runtime/mcache.c
:runtime·MCache_Alloc
関数内のn
変数の削除と、MCentral_AllocList
からの戻り値の処理の簡素化。ReleaseN
関数の引数からMCache *c
が削除され、c->size
の更新ロジックが削除。runtime·MCache_Free
関数から、MaxMCacheListLen
とMaxMCacheSize
に基づく複雑なスカベンジングロジックが完全に削除。runtime·MCache_Free
に、l->nlist >= 2*(runtime·class_to_allocnpages[sizeclass]<<PageShift)/size
という新しいシンプルな解放条件が追加。runtime·MCache_ReleaseAll
関数内のnlistmin
の初期化とReleaseN
の呼び出しが変更され、直接MCentral_FreeList
を呼び出すように簡素化。
-
src/pkg/runtime/mcentral.c
:runtime·MCentral_FreeList
関数の引数からint32 n
が削除。runtime·MCentral_FreeList
関数内のループが簡素化され、start
ポインタを直接進めるように変更。
コアとなるコードの解説
src/pkg/runtime/malloc.h
の変更
--- a/src/pkg/runtime/malloc.h
+++ b/src/pkg/runtime/malloc.h
@@ -109,8 +109,6 @@ enum
MaxSmallSize = 32<<10,
FixAllocChunk = 128<<10, // Chunk size for FixAlloc
- MaxMCacheListLen = 256, // Maximum objects on MCacheList
- MaxMCacheSize = 2<<20, // Maximum bytes in one MCache
MaxMHeapList = 1<<(20 - PageShift), // Maximum page length for fixed-size list in MHeap.
HeapAllocChunk = 1<<20, // Chunk size for heap growth
@@ -283,13 +281,11 @@ struct MCacheList
{
MLink *list;
uint32 nlist;
- uint32 nlistmin;
};
struct MCache
{
MCacheList list[NumSizeClasses];
- uintptr size;
intptr local_cachealloc; // bytes allocated (or freed) from cache since last lock of heap
intptr local_objects; // objects allocated (or freed) from cache since last lock of heap
intptr local_alloc; // bytes allocated (or freed) since last lock of heap
@@ -396,7 +392,7 @@ struct MCentral
void runtime·MCentral_Init(MCentral *c, int32 sizeclass);
int32 runtime·MCentral_AllocList(MCentral *c, MLink **first);
-void runtime·MCentral_FreeList(MCentral *c, int32 n, MLink *first);
+void runtime·MCentral_FreeList(MCentral *c, MLink *first);
void runtime·MCentral_FreeSpan(MCentral *c, MSpan *s, int32 n, MLink *start, MLink *end);
この変更は、MCache
とMCacheList
の構造から、tcmalloc
由来の不要なフィールド(nlistmin
, size
)と定数(MaxMCacheListLen
, MaxMCacheSize
)を削除しています。これにより、メモリ管理のデータ構造がシンプルになり、これらのフィールドを維持するためのロジックが不要になります。また、MCentral_FreeList
の関数シグネチャも変更され、解放するオブジェクトの数を明示的に渡す必要がなくなりました。
src/pkg/runtime/mcache.c
の変更
--- a/src/pkg/runtime/mcache.c
+++ b/src/pkg/runtime/mcache.c
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// Per-thread (in Go, per-M) malloc cache for small objects.
+// Per-P malloc cache for small objects.
//
// See malloc.h for an overview.
@@ -14,26 +14,19 @@ void*
runtime·MCache_Alloc(MCache *c, int32 sizeclass, uintptr size, int32 zeroed)
{
MCacheList *l;
- MLink *first, *v;
- int32 n;
+ MLink *v;
// Allocate from list.
l = &c->list[sizeclass];
if(l->list == nil) {
// Replenish using central lists.
- n = runtime·MCentral_AllocList(&runtime·mheap->central[sizeclass], &first);
- if(n == 0)
+ l->nlist = runtime·MCentral_AllocList(&runtime·mheap->central[sizeclass], &l->list);
+ if(l->list == nil)
runtime·throw("out of memory");
- l->list = first;
- l->nlist = n;
- c->size += n*size;
}
v = l->list;
l->list = v->next;
l->nlist--;
- if(l->nlist < l->nlistmin)
- l->nlistmin = l->nlist;
- c->size -= size;
// v is zeroed except for the link pointer
// that we used above; zero that.
@@ -50,7 +43,7 @@ runtime·MCache_Alloc(MCache *c, int32 sizeclass, uintptr size, int32 zeroed)
// Take n elements off l and return them to the central free list.
static void
-ReleaseN(MCache *c, MCacheList *l, int32 n, int32 sizeclass)
+ReleaseN(MCacheList *l, int32 n, int32 sizeclass)
{
MLink *first, **lp;
int32 i;
@@ -63,18 +56,14 @@ ReleaseN(MCache *c, MCacheList *l, int32 n, int32 sizeclass)
l->list = *lp;
*lp = nil;
l->nlist -= n;
- if(l->nlist < l->nlistmin)
- l->nlistmin = l->nlist;
- c->size -= n*runtime·class_to_size[sizeclass];
// Return them to central free list.
- runtime·MCentral_FreeList(&runtime·mheap->central[sizeclass], n, first);
+ runtime·MCentral_FreeList(&runtime·mheap->central[sizeclass], first);
}
void
runtime·MCache_Free(MCache *c, void *v, int32 sizeclass, uintptr size)
{
- int32 i, n;
MCacheList *l;\n MLink *p;
// Put back on list.
@@ -84,34 +73,13 @@ runtime·MCache_Free(MCache *c, void *v, int32 sizeclass, uintptr size)
p->next = l->list;
l->list = p;
l->nlist++;
- c->size += size;
c->local_cachealloc -= size;
c->local_objects--;
- if(l->nlist >= MaxMCacheListLen) {
- // Release a chunk back.
- ReleaseN(c, l, l->nlist/2, sizeclass);
- }
-
- if(c->size >= MaxMCacheSize) {
- // Scavenge.
- for(i=0; i<NumSizeClasses; i++) {
- l = &c->list[i];
- n = l->nlistmin;
-
- // n is the minimum number of elements we've seen on
- // the list since the last scavenge. If n > 0, it means that
- // we could have gotten by with n fewer elements
- // without needing to consult the central free list.
- // Move toward that situation by releasing n/2 of them.
- if(n > 0) {
- if(n > 1)
- n /= 2;
- ReleaseN(c, l, n, i);
- }
- l->nlistmin = l->nlist;
- }
- }
+ // We transfer span at a time from MCentral to MCache,
+ // if we have 2 times more than that, release a half back.
+ if(l->nlist >= 2*(runtime·class_to_allocnpages[sizeclass]<<PageShift)/size)
+ ReleaseN(l, l->nlist/2, sizeclass);
}
void
@@ -122,7 +90,10 @@ runtime·MCache_ReleaseAll(MCache *c)
for(i=0; i<NumSizeClasses; i++) {
l = &c->list[i];
- ReleaseN(c, l, l->nlist, i);
- l->nlistmin = 0;
+ if(l->list) {
+ runtime·MCentral_FreeList(&runtime·mheap->central[i], l->list);
+ l->list = nil;
+ l->nlist = 0;
+ }
}
}
このファイルでは、MCache
の割り当てと解放のロジックが大幅に簡素化されています。
MCache_Alloc
では、MCentral_AllocList
からのオブジェクト補充がより直接的になり、n
やfirst
といった一時変数を介さずにl->nlist
とl->list
に直接値が設定されます。ReleaseN
関数は、MCache
のsize
を更新するロジックを削除し、MCentral_FreeList
の呼び出しも簡素化されました。- 最も重要な変更は
MCache_Free
です。以前のMaxMCacheListLen
やMaxMCacheSize
に基づく複雑なスカベンジングロジックが削除され、代わりに「MCentral
から一度に転送されるスパンのオブジェクト数の2倍以上になったら、リストの半分を返却する」という、よりGoのメモリ管理モデルに即したシンプルなロジックが導入されました。これは、MCache
が過剰なメモリを保持しないようにするための、より効率的なアプローチです。 MCache_ReleaseAll
も同様に簡素化され、不要なnlistmin
の初期化が削除され、直接MCentral_FreeList
を呼び出すようになりました。
src/pkg/runtime/mcentral.c
の変更
--- a/src/pkg/runtime/mcentral.c
+++ b/src/pkg/runtime/mcentral.c
@@ -62,21 +62,16 @@ runtime·MCentral_AllocList(MCentral *c, MLink **pfirst)
return n;
}
-// Free n objects back into the central free list.
+// Free the list of objects back into the central free list.
void
-runtime·MCentral_FreeList(MCentral *c, int32 n, MLink *start)
+runtime·MCentral_FreeList(MCentral *c, MLink *start)
{
-\tMLink *v, *next;\n-\n-\t// Assume next == nil marks end of list.\n-\t// n and end would be useful if we implemented\n-\t// the transfer cache optimization in the TODO above.\n-\tUSED(n);\n+\tMLink *next;
runtime·lock(c);
-\tfor(v=start; v; v=next) {
-\t\tnext = v->next;\n-\t\tMCentral_Free(c, v);\n+\tfor(; start != nil; start = next) {
+\t\tnext = start->next;
+\t\tMCentral_Free(c, start);
}
runtime·unlock(c);
}
このファイルでは、runtime·MCentral_FreeList
関数のシグネチャが変更され、解放するオブジェクトの数(n
)を受け取らなくなりました。これは、MCache
側でオブジェクト数を管理する必要がなくなったためです。関数内部のループも、start
ポインタを直接進めることで、リストのすべてのオブジェクトを解放するように簡素化されています。これにより、MCentral
がMCache
から受け取る解放要求の処理がよりシンプルになりました。
これらの変更は全体として、Goのメモリ管理におけるMCache
の役割を、tcmalloc
の設計から独立させ、Goのガベージコレクションとメモリ割り当ての特性に合わせた、よりシンプルで効率的なものへと進化させています。
関連リンク
- Goのメモリ管理に関する公式ドキュメントやブログ記事:
- Go's Memory Allocator (古い記事ですが、基本的な概念は参考になります)
- Go Memory Management (より詳細な情報)
- tcmallocに関する情報:
参考にした情報源リンク
- Goのソースコード(特に
src/runtime
ディレクトリ) - Goの公式ブログ
- tcmallocのドキュメント
- Goのメモリ管理に関する技術ブログや解説記事 (Web検索結果に基づく)
- "Go MCache"
- "Go runtime memory management"
- "tcmalloc vs Go malloc"
- "Go memory allocator design"