[インデックス 14847] ファイルの概要
このコミットは、Goランタイムにおけるスタックセグメントのキャッシュ戦略を改善し、特に多数のスレッドを生成するワークロードにおけるStackSys
メモリの使用量を大幅に削減することを目的としています。具体的には、スレッドごとのスタックセグメントキャッシュのアグレッシブさを緩和し、グローバルなスタックセグメントキャッシュを導入することで、メモリ効率を向上させています。
コミット
f82db7d9e4ccf04b19a087561ab0f521fc36e5b1
Dmitriy Vyukov <dvyukov@google.com>
Thu Jan 10 09:57:06 2013 +0400
runtime: less aggressive per-thread stack segment caching
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f82db7d9e4ccf04b19a087561ab0f521fc36e5b1
元コミット内容
commit f82db7d9e4ccf04b19a087561ab0f521fc36e5b1
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Jan 10 09:57:06 2013 +0400
runtime: less aggressive per-thread stack segment caching
Introduce global stack segment cache and limit per-thread cache size.
This greatly reduces StackSys memory on workloads that create lots of threads.
benchmark old ns/op new ns/op delta
BenchmarkStackGrowth 665 656 -1.35%
BenchmarkStackGrowth-2 333 328 -1.50%
BenchmarkStackGrowth-4 224 172 -23.21%
BenchmarkStackGrowth-8 124 91 -26.13%
BenchmarkStackGrowth-16 82 47 -41.94%
BenchmarkStackGrowth-32 73 40 -44.79%
BenchmarkStackGrowthDeep 97231 94391 -2.92%
BenchmarkStackGrowthDeep-2 47230 58562 +23.99%
BenchmarkStackGrowthDeep-4 24993 49356 +97.48%
BenchmarkStackGrowth-8 15105 30072 +99.09%
BenchmarkStackGrowth-16 10005 15623 +56.15%
BenchmarkStackGrowth-32 12517 13069 +4.41%
TestStackMem#1,MB 310 12 -96.13%
TestStackMem#2,MB 296 14 -95.27%
TestStackMem#3,MB 479 14 -97.08%
TestStackMem#1,sec 3.22 2.26 -29.81%
TestStackMem#2,sec 2.43 2.15 -11.52%
TestStackMem#3,sec 2.50 2.38 -4.80%
R=sougou, no.smile.face, rsc
CC=golang-dev, msolomon
https://golang.org/cl/7029044
変更の背景
Goランタイムは、ゴルーチン(軽量スレッド)のスタックを動的に管理します。ゴルーチンが関数呼び出しを深くネストしたり、大きなローカル変数を確保したりすると、スタックが不足する可能性があります。この場合、ランタイムはより大きなスタックセグメントを割り当て、既存のスタック内容を新しいセグメントにコピーして、古いセグメントを解放します。このプロセスは「スタックの成長(stack growth)」と呼ばれます。
以前のGoランタイムでは、各M(OSスレッドに相当)が自身のスタックセグメントのキャッシュを保持していました。これは、スタックの再利用を効率化するためのものでしたが、多数のM(OSスレッド)が生成されるようなワークロード、特に短命なゴルーチンが頻繁に生成・破棄されるようなシナリオでは、各Mがそれぞれ独立してスタックセグメントをキャッシュするため、システム全体のメモリ使用量(StackSys
)が過度に増加するという問題がありました。
このコミットは、このメモリ消費の問題に対処するために導入されました。特に、TestStackMem
ベンチマークの結果が示すように、以前は数百MBに達していたStackSys
メモリが、この変更によって劇的に削減されています。
前提知識の解説
- Goランタイム: Goプログラムの実行を管理するシステム。スケジューラ、メモリ管理、ガベージコレクションなどが含まれます。
- Goroutine (ゴルーチン): Goにおける軽量な実行単位。OSスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。ゴルーチンはGoランタイムによってOSスレッド(M)に多重化されて実行されます。
- M (Machine): GoランタイムにおけるOSスレッドの抽象化。P(Processor)と連携してゴルーチンを実行します。
- Stack (スタック): 関数呼び出しの際に、ローカル変数、引数、戻りアドレスなどを格納するために使用されるメモリ領域。Goのスタックは動的にサイズが変更されます。
- Stack Segment (スタックセグメント): Goランタイムがスタックに割り当てるメモリのチャンク。スタックが成長する必要がある場合、新しいセグメントが割り当てられます。
- Stack Growth (スタックの成長): ゴルーチンのスタックが不足した際に、より大きなスタックセグメントを割り当ててスタックを拡張するプロセス。
- Memory Allocation (メモリ割り当て): プログラムが実行時にメモリを要求し、システムから割り当てられること。Goランタイムは独自のメモリマネージャを持ち、効率的なメモリ割り当てと解放を行います。
mstats
: Goランタイムのメモリ統計情報を含む構造体。StackSys
はシステムから割り当てられたスタックメモリの総量を示します。FixAlloc
: Goランタイム内部で使用される固定サイズのオブジェクトを効率的に割り当てるためのアロケータ。以前はスタックセグメントの割り当てにも使用されていました。- Per-thread cache (スレッドごとのキャッシュ): 各OSスレッド(M)が独立して保持するキャッシュ。
- Global cache (グローバルキャッシュ): システム全体で共有されるキャッシュ。
技術的詳細
このコミットの主要な変更点は、スタックセグメントのキャッシュ戦略を、スレッドごとのアグレッシブなキャッシュから、グローバルキャッシュとスレッドごとのキャッシュの組み合わせへと移行したことです。
-
グローバルスタックキャッシュの導入 (
src/pkg/runtime/malloc.goc
):StackCacheNode
構造体が定義され、スタックセグメントのバッチを保持できるようになりました。stackcache
というグローバルなStackCacheNode
のリンクリストが導入され、グローバルなスタックセグメントのプールとして機能します。stackcachemu
というロックが導入され、グローバルキャッシュへのアクセスを保護します。stackcacherefill()
関数が追加されました。これは、スレッドごとのキャッシュが空になったときに、グローバルキャッシュからスタックセグメントのバッチを取得するか、必要に応じて新しいメモリをシステムから割り当ててグローバルキャッシュに追加する役割を担います。stackcacherelease()
関数が追加されました。これは、スレッドごとのキャッシュが満杯になったときに、スタックセグメントのバッチをグローバルキャッシュに解放する役割を担います。
-
スレッドごとのキャッシュの変更 (
src/pkg/runtime/runtime.h
,src/pkg/runtime/malloc.goc
):M
構造体(OSスレッドのコンテキスト)に、スレッドごとのスタックキャッシュを管理するためのフィールドが追加されました。stackinuse
: 現在使用中のスタックセグメントの数。stackcachepos
:stackcache
配列内の現在の位置。stackcachecnt
:stackcache
配列内のキャッシュされたスタックセグメントの数。stackcache[StackCacheSize]
: スレッドごとのスタックセグメントキャッシュ配列。
StackCacheSize
(32) とStackCacheBatch
(16) という定数が導入されました。StackCacheSize
はスレッドごとのキャッシュの最大サイズを、StackCacheBatch
はグローバルキャッシュとの間で一度に転送されるスタックセグメントの数を定義します。runtime·stackalloc()
関数とruntime·stackfree()
関数が変更され、スレッドごとのキャッシュとグローバルキャッシュを連携して使用するように修正されました。runtime·stackalloc()
は、まずスレッドごとのキャッシュからスタックセグメントを取得しようとします。キャッシュが空の場合、stackcacherefill()
を呼び出してグローバルキャッシュから補充します。runtime·stackfree()
は、解放されたスタックセグメントをスレッドごとのキャッシュに戻します。スレッドごとのキャッシュが満杯の場合、stackcacherelease()
を呼び出してグローバルキャッシュに解放します。
-
メモリ統計の更新 (
src/pkg/runtime/mgc0.c
):cachestats()
関数が変更され、mstats.stacks_inuse
の計算がmp->stackinuse*FixedStack
を使用するように修正されました。これにより、実際に使用されているスタックメモリの量が正確に反映されるようになりました。mstats.stacks_sys
の計算から、各Mのstackalloc->sys
の合計が削除されました。これは、FixAlloc
ベースのスタック割り当てが廃止されたためです。
-
FixAlloc
の廃止 (src/pkg/runtime/proc.c
):mcommoninit()
関数から、各Mのstackalloc
(FixAlloc
インスタンス)の初期化が削除されました。これにより、スタックセグメントの割り当てがFixAlloc
から新しいキャッシュメカニズムに完全に移行しました。
-
ベンチマークの更新 (
src/pkg/runtime/proc_test.go
,src/pkg/runtime/stack_test.go
):BenchmarkStackGrowth
とBenchmarkStackGrowthDeep
が、新しいスタック管理のパフォーマンスを測定するために調整されました。TestStackMem
という新しいテストが追加されました。このテストは、多数のゴルーチンを生成し、スタックメモリの使用量を測定することで、このコミットのメモリ削減効果を検証します。このテストは、以前のバージョンで最大500MBのメモリを消費していた問題に対処するために設計されました。
この変更により、スタックセグメントがOSスレッド(M)間でより効率的に共有されるようになり、特に多数のゴルーチンが生成されるようなシナリオでのメモリフットプリントが大幅に削減されました。
コアとなるコードの変更箇所
主要な変更は以下のファイルに集中しています。
src/pkg/runtime/malloc.goc
: グローバルスタックキャッシュのロジック (StackCacheNode
,stackcache
,stackcachemu
,stackcacherefill
,stackcacherelease
) と、runtime·stackalloc
,runtime·stackfree
の変更。src/pkg/runtime/runtime.h
:M
構造体へのスレッドごとのスタックキャッシュ関連フィールドの追加と、StackCacheSize
,StackCacheBatch
定数の定義。src/pkg/runtime/mgc0.c
: メモリ統計の更新。src/pkg/runtime/proc.c
:FixAlloc
ベースのスタックアロケータの削除。src/pkg/runtime/stack_test.go
:TestStackMem
テストの追加。
特に、src/pkg/runtime/malloc.goc
におけるstackcacherefill
とstackcacherelease
関数、およびruntime·stackalloc
とruntime·stackfree
内のスタックキャッシュ関連のロジックがコアとなります。
--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -748,9 +748,74 @@ runtime·cnew(Type *typ)
return ret;
}
+typedef struct StackCacheNode StackCacheNode;
+struct StackCacheNode
+{
+ StackCacheNode *next;
+ void* batch[StackCacheBatch-1];
+};
+
+static StackCacheNode *stackcache;
+static Lock stackcachemu;
+
+// stackcacherefill/stackcacherelease implement global cache of stack segments.
+// The cache is required to prevent unlimited growth of per-thread caches.
+static void
+stackcacherefill(void)
+{
+ StackCacheNode *n;
+ int32 i, pos;
+
+ runtime·lock(&stackcachemu);
+ n = stackcache;
+ if(n)
+ stackcache = n->next;
+ runtime·unlock(&stackcachemu);
+ if(n == nil) {
+ n = (StackCacheNode*)runtime·SysAlloc(FixedStack*StackCacheBatch);
+ if(n == nil)
+ runtime·throw("out of memory (staccachekrefill)");
+ runtime·xadd64(&mstats.stacks_sys, FixedStack*StackCacheBatch);
+ for(i = 0; i < StackCacheBatch-1; i++)
+ n->batch[i] = (byte*)n + (i+1)*FixedStack;
+ }
+ pos = m->stackcachepos;
+ for(i = 0; i < StackCacheBatch-1; i++) {
+ m->stackcache[pos] = n->batch[i];
+ pos = (pos + 1) % StackCacheSize;
+ }
+ m->stackcache[pos] = n;
+ pos = (pos + 1) % StackCacheSize;
+ m->stackcachepos = pos;
+ m->stackcachecnt += StackCacheBatch;
+}
+
+static void
+stackcacherelease(void)
+{
+ StackCacheNode *n;
+ uint32 i, pos;
+
+ pos = (m->stackcachepos - m->stackcachecnt) % StackCacheSize;
+ n = (StackCacheNode*)m->stackcache[pos];
+ pos = (pos + 1) % StackCacheSize;
+ for(i = 0; i < StackCacheBatch-1; i++) {
+ n->batch[i] = m->stackcache[pos];
+ pos = (pos + 1) % StackCacheSize;
+ }
+ m->stackcachecnt -= StackCacheBatch;
+ runtime·lock(&stackcachemu);
+ n->next = stackcache;
+ stackcache = n;
+ runtime·unlock(&stackcachemu);
+}
+
void*
runtime·stackalloc(uint32 n)
{
+ uint32 pos;
+ void *v;
+
// Stackalloc must be called on scheduler stack, so that we
// never try to grow the stack during the code that stackalloc runs.
// Doing so would cause a deadlock (issue 1547).
@@ -769,8 +834,16 @@ runtime·stackalloc(uint32 n)
runtime·printf("stackalloc: in malloc, size=%d want %d", FixedStack, n);
runtime·throw("stackalloc");
}
- return runtime·FixAlloc_Alloc(m->stackalloc);
+ if(m->stackcachecnt == 0)
+ stackcacherefill();
+ pos = m->stackcachepos;
+ pos = (pos - 1) % StackCacheSize;
+ v = m->stackcache[pos];
+ m->stackcachepos = pos;
+ m->stackcachecnt--;
+ m->stackinuse++;
+ return v;
}
return runtime·mallocgc(n, FlagNoProfiling|FlagNoGC, 0, 0);
}
@@ -778,8 +850,16 @@ runtime·stackalloc(uint32 n)
void
runtime·stackfree(void *v, uintptr n)
{
+ uint32 pos;
+
if(m->mallocing || m->gcing || n == FixedStack) {
- runtime·FixAlloc_Free(m->stackalloc, v);
+ if(m->stackcachecnt == StackCacheSize)
+ stackcacherelease();
+ pos = m->stackcachepos;
+ m->stackcache[pos] = v;
+ m->stackcachepos = (pos + 1) % StackCacheSize;
+ m->stackcachecnt++;
+ m->stackinuse--;
return;
}
runtime·free(v);
コアとなるコードの解説
このコミットの核心は、Goランタイムがスタックセグメントをどのように取得し、解放するかというメカニズムの変更にあります。
-
グローバルキャッシュの役割:
StackCacheNode
は、複数のスタックセグメント(batch
配列)をまとめて管理するための構造体です。stackcache
は、これらのStackCacheNode
のリンクリストであり、システム全体で共有されるスタックセグメントのプールとして機能します。stackcachemu
は、複数のM(OSスレッド)が同時にグローバルキャッシュにアクセスする際の競合を防ぐためのロックです。
-
stackcacherefill()
:- この関数は、特定のM(OSスレッド)のスレッドごとのスタックキャッシュ(
m->stackcache
)が空になったときに呼び出されます。 - まず、グローバルキャッシュ(
stackcache
)からStackCacheNode
を取得しようとします。 - もしグローバルキャッシュが空であれば、
runtime·SysAlloc
を呼び出して、FixedStack * StackCacheBatch
バイトの新しいメモリをシステムから直接割り当てます。これは、新しいスタックセグメントのバッチを確保することを意味します。 - 取得した(または新しく割り当てた)
StackCacheNode
内のスタックセグメントを、Mのスレッドごとのキャッシュ(m->stackcache
)にコピーします。これにより、Mは次にスタックが必要になったときに、グローバルキャッシュにアクセスすることなく、自身のキャッシュから迅速にスタックセグメントを取得できます。
- この関数は、特定のM(OSスレッド)のスレッドごとのスタックキャッシュ(
-
stackcacherelease()
:- この関数は、特定のMのスレッドごとのスタックキャッシュが満杯になったときに呼び出されます。
- Mのスレッドごとのキャッシュから、
StackCacheBatch
個のスタックセグメントを含むStackCacheNode
を再構築します。 - 再構築された
StackCacheNode
をグローバルキャッシュ(stackcache
)の先頭に追加し、他のMが利用できるようにします。
-
runtime·stackalloc()
とruntime·stackfree()
の変更:runtime·stackalloc()
は、スタックセグメントを割り当てる際に、まずm->stackcachecnt
(スレッドごとのキャッシュ内のスタックセグメント数)を確認します。- もし
m->stackcachecnt
が0であれば、stackcacherefill()
を呼び出してキャッシュを補充します。 - その後、スレッドごとのキャッシュからスタックセグメントを取得し、
m->stackinuse
をインクリメントします。 runtime·stackfree()
は、スタックセグメントを解放する際に、まずm->stackcachecnt
がStackCacheSize
(スレッドごとのキャッシュの最大サイズ)に達しているかを確認します。- もし
m->stackcachecnt
がStackCacheSize
に達していれば、stackcacherelease()
を呼び出してスタックセグメントのバッチをグローバルキャッシュに解放します。 - そうでなければ、解放されたスタックセグメントをスレッドごとのキャッシュに追加し、
m->stackinuse
をデクリメントします。
この新しいメカニズムにより、各Mは一定数のスタックセグメントをローカルにキャッシュしつつ、必要に応じてグローバルキャッシュと連携することで、システム全体のメモリ使用量を最適化します。特に、短命なゴルーチンが多数生成されるようなシナリオでは、スタックセグメントがグローバルキャッシュを通じて効率的に再利用されるため、OSへのメモリ割り当て/解放の頻度が減り、StackSys
メモリが大幅に削減されます。
関連リンク
参考にした情報源リンク
- (特になし。コミットメッセージとコードから直接情報を抽出しました。)