[インデックス 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"