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

[インデックス 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 (グローバルキャッシュ): システム全体で共有されるキャッシュ。

技術的詳細

このコミットの主要な変更点は、スタックセグメントのキャッシュ戦略を、スレッドごとのアグレッシブなキャッシュから、グローバルキャッシュとスレッドごとのキャッシュの組み合わせへと移行したことです。

  1. グローバルスタックキャッシュの導入 (src/pkg/runtime/malloc.goc):

    • StackCacheNode構造体が定義され、スタックセグメントのバッチを保持できるようになりました。
    • stackcacheというグローバルなStackCacheNodeのリンクリストが導入され、グローバルなスタックセグメントのプールとして機能します。
    • stackcachemuというロックが導入され、グローバルキャッシュへのアクセスを保護します。
    • stackcacherefill()関数が追加されました。これは、スレッドごとのキャッシュが空になったときに、グローバルキャッシュからスタックセグメントのバッチを取得するか、必要に応じて新しいメモリをシステムから割り当ててグローバルキャッシュに追加する役割を担います。
    • stackcacherelease()関数が追加されました。これは、スレッドごとのキャッシュが満杯になったときに、スタックセグメントのバッチをグローバルキャッシュに解放する役割を担います。
  2. スレッドごとのキャッシュの変更 (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()を呼び出してグローバルキャッシュに解放します。
  3. メモリ統計の更新 (src/pkg/runtime/mgc0.c):

    • cachestats()関数が変更され、mstats.stacks_inuseの計算がmp->stackinuse*FixedStackを使用するように修正されました。これにより、実際に使用されているスタックメモリの量が正確に反映されるようになりました。
    • mstats.stacks_sysの計算から、各Mのstackalloc->sysの合計が削除されました。これは、FixAllocベースのスタック割り当てが廃止されたためです。
  4. FixAllocの廃止 (src/pkg/runtime/proc.c):

    • mcommoninit()関数から、各MのstackallocFixAllocインスタンス)の初期化が削除されました。これにより、スタックセグメントの割り当てがFixAllocから新しいキャッシュメカニズムに完全に移行しました。
  5. ベンチマークの更新 (src/pkg/runtime/proc_test.go, src/pkg/runtime/stack_test.go):

    • BenchmarkStackGrowthBenchmarkStackGrowthDeepが、新しいスタック管理のパフォーマンスを測定するために調整されました。
    • 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におけるstackcacherefillstackcacherelease関数、およびruntime·stackallocruntime·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ランタイムがスタックセグメントをどのように取得し、解放するかというメカニズムの変更にあります。

  1. グローバルキャッシュの役割:

    • StackCacheNodeは、複数のスタックセグメント(batch配列)をまとめて管理するための構造体です。
    • stackcacheは、これらのStackCacheNodeのリンクリストであり、システム全体で共有されるスタックセグメントのプールとして機能します。
    • stackcachemuは、複数のM(OSスレッド)が同時にグローバルキャッシュにアクセスする際の競合を防ぐためのロックです。
  2. stackcacherefill():

    • この関数は、特定のM(OSスレッド)のスレッドごとのスタックキャッシュ(m->stackcache)が空になったときに呼び出されます。
    • まず、グローバルキャッシュ(stackcache)からStackCacheNodeを取得しようとします。
    • もしグローバルキャッシュが空であれば、runtime·SysAllocを呼び出して、FixedStack * StackCacheBatchバイトの新しいメモリをシステムから直接割り当てます。これは、新しいスタックセグメントのバッチを確保することを意味します。
    • 取得した(または新しく割り当てた)StackCacheNode内のスタックセグメントを、Mのスレッドごとのキャッシュ(m->stackcache)にコピーします。これにより、Mは次にスタックが必要になったときに、グローバルキャッシュにアクセスすることなく、自身のキャッシュから迅速にスタックセグメントを取得できます。
  3. stackcacherelease():

    • この関数は、特定のMのスレッドごとのスタックキャッシュが満杯になったときに呼び出されます。
    • Mのスレッドごとのキャッシュから、StackCacheBatch個のスタックセグメントを含むStackCacheNodeを再構築します。
    • 再構築されたStackCacheNodeをグローバルキャッシュ(stackcache)の先頭に追加し、他のMが利用できるようにします。
  4. runtime·stackalloc()runtime·stackfree()の変更:

    • runtime·stackalloc()は、スタックセグメントを割り当てる際に、まずm->stackcachecnt(スレッドごとのキャッシュ内のスタックセグメント数)を確認します。
    • もしm->stackcachecntが0であれば、stackcacherefill()を呼び出してキャッシュを補充します。
    • その後、スレッドごとのキャッシュからスタックセグメントを取得し、m->stackinuseをインクリメントします。
    • runtime·stackfree()は、スタックセグメントを解放する際に、まずm->stackcachecntStackCacheSize(スレッドごとのキャッシュの最大サイズ)に達しているかを確認します。
    • もしm->stackcachecntStackCacheSizeに達していれば、stackcacherelease()を呼び出してスタックセグメントのバッチをグローバルキャッシュに解放します。
    • そうでなければ、解放されたスタックセグメントをスレッドごとのキャッシュに追加し、m->stackinuseをデクリメントします。

この新しいメカニズムにより、各Mは一定数のスタックセグメントをローカルにキャッシュしつつ、必要に応じてグローバルキャッシュと連携することで、システム全体のメモリ使用量を最適化します。特に、短命なゴルーチンが多数生成されるようなシナリオでは、スタックセグメントがグローバルキャッシュを通じて効率的に再利用されるため、OSへのメモリ割り当て/解放の頻度が減り、StackSysメモリが大幅に削減されます。

関連リンク

参考にした情報源リンク

  • (特になし。コミットメッセージとコードから直接情報を抽出しました。)