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

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

このコミットは、Goランタイムのメモリ管理におけるMCacheの構造とロジックを簡素化することを目的としています。具体的には、tcmallocから継承されていたnlistminsizeといった閾値管理を削除し、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からコピーされたnlistminsizeといった閾値に基づく複雑なキャッシュ管理ロジックは、Goのメモリ割り当てパターンにおいては過剰であり、不要なオーバーヘッドや複雑性をもたらしていました。

このコミットの背景には、Goのメモリ管理をGo自身の特性(GCによる自動管理、明示的な解放の少なさ)に合わせて最適化し、tcmallocの設計に盲目的に従うのではなく、よりシンプルで効率的な実装を目指すという意図があります。

前提知識の解説

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

  1. Goのメモリ管理の階層構造:

    • MHeap (Memory Heap): グローバルなヒープ領域で、すべてのGoプログラムが使用するメモリの大部分を管理します。大きなオブジェクトや、MCacheから溢れたオブジェクトの割り当て、およびMSpanの管理を行います。
    • MCentral (Memory Central): MHeapMCacheの中間に位置し、特定のサイズクラスのオブジェクトを管理するMSpanのリストを保持します。MCacheがメモリを要求すると、MCentralからMSpanが提供され、MCacheがメモリを解放すると、MCentralに返却されます。
    • MCache (Memory Cache): 各P(論理プロセッサ)に紐付けられたスレッドローカルなキャッシュです。小さなオブジェクトの割り当てを高速化するために使用されます。Goルーチンがメモリを割り当てる際、まず自身のPに紐付けられたMCacheからメモリを取得しようとします。これにより、グローバルなロックの競合を減らし、並行性を高めます。
  2. MSpan (Memory Span): 連続したページ(通常は8KB)のブロックを表します。MHeapによって管理され、MCentralを通じてMCacheに提供されます。MSpanは、特定のサイズクラスのオブジェクトを格納するために分割されます。

  3. MLink: メモリブロックを連結リストとして管理するための構造体です。解放されたオブジェクトはMLinkとして連結リストに格納され、再利用されます。

  4. サイズクラス (Size Class): Goのメモリ管理では、オブジェクトのサイズに応じて事前に定義された「サイズクラス」に分類されます。これにより、異なるサイズのオブジェクトに対して効率的なメモリ割り当てと管理が行われます。

  5. tcmalloc (Thread-Caching Malloc): Googleが開発したC++用の高性能メモリ割り当てライブラリです。Goのメモリ管理は、その設計思想(特にスレッドローカルキャッシュの概念)に強く影響を受けています。tcmallocは、スレッドごとに小さなオブジェクトのキャッシュを持ち、中央のヒープと連携して動作します。

  6. ガベージコレクション (GC): Goのランタイムは、不要になったメモリを自動的に回収するガベージコレクタを備えています。これにより、プログラマは手動でのメモリ解放から解放され、メモリリークのリスクが軽減されます。GCの存在は、tcmallocのような明示的なfreeを前提としたメモリ管理戦略とは異なるアプローチをGoに要求します。

技術的詳細

このコミットでは、主に以下の技術的変更が行われています。

  1. MCache構造体からのsizeフィールドの削除:

    • src/pkg/runtime/malloc.hにおいて、MCache構造体からuintptr size;フィールドが削除されました。このフィールドは、MCacheが現在保持しているメモリの合計バイト数を追跡するために使用されていましたが、Goのメモリ管理の特性上、その必要性が低いと判断されました。
  2. MCacheList構造体からのnlistminフィールドの削除:

    • src/pkg/runtime/malloc.hにおいて、MCacheList構造体からuint32 nlistmin;フィールドが削除されました。このフィールドは、MCacheListが過去に保持していたオブジェクトの最小数を記録し、キャッシュのスカベンジング(不要なメモリの返却)の判断基準として使用されていましたが、これも不要とされました。
  3. MCache_Alloc関数の簡素化:

    • src/pkg/runtime/mcache.cruntime·MCache_Alloc関数において、MCentral_AllocListからの戻り値の処理が簡素化されました。以前は、n(割り当てられたオブジェクト数)とfirst(最初のオブジェクトへのポインタ)を別々に受け取っていましたが、変更後はl->nlistに直接オブジェクト数を、l->listに最初のオブジェクトへのポインタを格納するように変更されました。これにより、MCacheへの補充ロジックがより直接的になりました。
  4. ReleaseN関数の変更とMCache_Freeからのスカベンジングロジックの削除:

    • src/pkg/runtime/mcache.cReleaseN関数から、MCachesizeフィールドを更新するロジックが削除されました。
    • 最も大きな変更は、runtime·MCache_Free関数から、MaxMCacheListLenMaxMCacheSizeに基づく複雑なスカベンジング(キャッシュから中央ヒープへのメモリ返却)ロジックが完全に削除されたことです。以前は、MCacheListのオブジェクト数がMaxMCacheListLenを超えた場合や、MCacheの合計サイズがMaxMCacheSizeを超えた場合に、キャッシュの一部を中央ヒープに返却する処理が行われていました。このロジックはtcmallocの設計に由来するものですが、Goのメモリ管理では不要と判断されました。
    • 代わりに、MCache_Freeでは、l->nlist(現在のリストのオブジェクト数)が、MCentralから一度に転送されるスパンのオブジェクト数の2倍以上になった場合に、リストの半分をReleaseNを通じて中央ヒープに返却するという、よりシンプルなロジックが導入されました。これは、スパン単位での転送を考慮した、よりGoらしい最適化です。
  5. MCache_ReleaseAll関数の簡素化:

    • src/pkg/runtime/mcache.cruntime·MCache_ReleaseAll関数において、nlistminの初期化が不要になったため、関連する行が削除されました。また、ReleaseNを呼び出す代わりに、直接MCentral_FreeListを呼び出して、MCache内のすべてのオブジェクトを中央ヒープに返却するように変更されました。
  6. MCentral_FreeList関数の引数変更と簡素化:

    • src/pkg/runtime/mcentral.cruntime·MCentral_FreeList関数において、引数からint32 n(解放するオブジェクト数)が削除され、MLink *start(解放するオブジェクトリストの先頭)のみを受け取るようになりました。これは、MCache側でオブジェクト数を管理する必要がなくなったためです。
    • 関数内部のループも簡素化され、startポインタを直接進めることで、リストのすべてのオブジェクトを中央ヒープに解放するように変更されました。

これらの変更により、MCacheの管理ロジックが大幅に簡素化され、Goのメモリ割り当ての特性により適合するようになりました。特に、tcmalloc由来の複雑な閾値管理とスカベンジングロジックが削除されたことで、コードの可読性と保守性が向上し、不要なオーバーヘッドが削減される可能性があります。

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

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

  1. src/pkg/runtime/malloc.h:

    • MCacheList構造体から uint32 nlistmin; が削除。
    • MCache構造体から uintptr size; が削除。
    • MaxMCacheListLenMaxMCacheSize の定義が削除。
    • runtime·MCentral_FreeList 関数のプロトタイプ宣言から int32 n 引数が削除。
  2. src/pkg/runtime/mcache.c:

    • runtime·MCache_Alloc 関数内のn変数の削除と、MCentral_AllocListからの戻り値の処理の簡素化。
    • ReleaseN 関数の引数から MCache *c が削除され、c->size の更新ロジックが削除。
    • runtime·MCache_Free 関数から、MaxMCacheListLenMaxMCacheSize に基づく複雑なスカベンジングロジックが完全に削除。
    • runtime·MCache_Free に、l->nlist >= 2*(runtime·class_to_allocnpages[sizeclass]<<PageShift)/size という新しいシンプルな解放条件が追加。
    • runtime·MCache_ReleaseAll 関数内の nlistmin の初期化と ReleaseN の呼び出しが変更され、直接 MCentral_FreeList を呼び出すように簡素化。
  3. 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);

この変更は、MCacheMCacheListの構造から、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からのオブジェクト補充がより直接的になり、nfirstといった一時変数を介さずにl->nlistl->listに直接値が設定されます。
  • ReleaseN関数は、MCachesizeを更新するロジックを削除し、MCentral_FreeListの呼び出しも簡素化されました。
  • 最も重要な変更はMCache_Freeです。以前のMaxMCacheListLenMaxMCacheSizeに基づく複雑なスカベンジングロジックが削除され、代わりに「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ポインタを直接進めることで、リストのすべてのオブジェクトを解放するように簡素化されています。これにより、MCentralMCacheから受け取る解放要求の処理がよりシンプルになりました。

これらの変更は全体として、Goのメモリ管理におけるMCacheの役割を、tcmallocの設計から独立させ、Goのガベージコレクションとメモリ割り当ての特性に合わせた、よりシンプルで効率的なものへと進化させています。

関連リンク

参考にした情報源リンク

  • Goのソースコード(特にsrc/runtimeディレクトリ)
  • Goの公式ブログ
  • tcmallocのドキュメント
  • Goのメモリ管理に関する技術ブログや解説記事 (Web検索結果に基づく)
    • "Go MCache"
    • "Go runtime memory management"
    • "tcmalloc vs Go malloc"
    • "Go memory allocator design"