[インデックス 19639] ファイルの概要
このコミットは、Goランタイムにおけるスタックアロケータを、従来のmallocgc
(ガベージコレクタ管理下のメモリ割り当て)から分離することを目的としています。これにより、malloc
関数自体をGo言語で実装する準備を進めるとともに、ガベージコレクションの対象外(FlagNoGC
)であったスタックメモリの管理を独立させ、ヒープ管理の簡素化を図っています。
コミット
commit 7c13860cd08352e785002cb97bd3baafd370e8bc
Author: Keith Randall <khr@golang.org>
Date: Mon Jun 30 18:59:24 2014 -0700
runtime: stack allocator, separate from mallocgc
In order to move malloc to Go, we need to have a
separate stack allocator. If we run out of stack
during malloc, malloc will not be available
to allocate a new stack.
Stacks are the last remaining FlagNoGC objects in the
GC heap. Once they are out, we can get rid of the
distinction between the allocated/blockboundary bits.
(This will be in a separate change.)
Fixes #7468
Fixes #7424
LGTM=rsc, dvyukov
R=golang-codereviews, dvyukov, khr, dave, rsc
CC=golang-codereviews
https://golang.org/cl/104200047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7c13860cd08352e785002cb97bd3baafd370e8bc
元コミット内容
このコミットは、Goランタイムにおいてスタックアロケータをmallocgc
から分離する変更を導入しています。これは、malloc
関数をGo言語で実装するために不可欠なステップです。もしmalloc
の実行中にスタックが不足した場合、新しいスタックを割り当てるためにmalloc
自身を利用することができないという循環的な問題が発生するため、スタックの割り当てを独立させる必要がありました。
また、スタックはガベージコレクタのヒープ内でFlagNoGC
(GC対象外)として扱われる最後のオブジェクトでした。スタックがヒープから分離されることで、割り当て済み/ブロック境界のビット間の区別をなくすことが可能になります(これは別の変更で実施される予定です)。
この変更は、Issue #7468 および #7424 を修正します。
変更の背景
Goランタイムの進化において、メモリ管理の効率化とGo言語自身の表現力の向上は常に重要なテーマでした。このコミットの背景には、主に以下の二つの大きな動機があります。
-
malloc
のGo言語への移行: Goランタイムの初期段階では、パフォーマンスが要求される低レベルなメモリ割り当て処理(malloc
)はC言語で実装されていました。しかし、Go言語の成熟とランタイムの複雑化に伴い、ランタイムの大部分をGo言語で記述することで、コードの可読性、保守性、そして開発効率を向上させるという目標がありました。malloc
をGo言語に移行する上で、スタックの割り当てがボトルネックとなる可能性がありました。具体的には、malloc
が新しいメモリを割り当てる際に、自身の実行に必要なスタックが不足した場合、そのスタックを確保するために再度malloc
を呼び出すというデッドロックのような状況が発生する可能性がありました。この循環的な依存関係を解消するためには、スタックの割り当てをmalloc
から完全に独立させる必要がありました。 -
ガベージコレクションの簡素化: Goのガベージコレクタは、ヒープ上のオブジェクトを追跡し、不要になったメモリを解放する役割を担っています。しかし、スタックは特殊なメモリ領域であり、GCの対象外(
FlagNoGC
)として扱われていました。これは、スタックがプログラムの実行フローに密接に関わっており、GCのサイクルとは異なるライフサイクルを持つためです。FlagNoGC
オブジェクトがヒープ内に存在することは、GCの実装を複雑にし、ヒープのメモリレイアウトに関する特定の制約を課していました。スタックをヒープから完全に分離することで、GCは純粋にヒープ上のGC対象オブジェクトのみを考慮すればよくなり、GCの実装を簡素化し、将来的なGCアルゴリズムの改善や最適化の余地を広げることができます。コミットメッセージにある「allocated/blockboundary bits
の区別をなくす」という言及は、このGCの簡素化に向けたステップを示唆しています。
これらの背景から、スタックアロケータの分離は、Goランタイムのアーキテクチャをよりクリーンにし、将来の機能拡張やパフォーマンス改善のための基盤を固める重要な変更であったと言えます。
前提知識の解説
このコミットの変更内容を理解するためには、Goランタイムのメモリ管理とスタック管理に関するいくつかの基本的な概念を理解しておく必要があります。
-
Goランタイム (Go Runtime): Goプログラムの実行を管理する部分です。ガベージコレクション、スケジューリング、メモリ割り当て、ゴルーチン管理など、多岐にわたる機能を提供します。Goランタイムの一部はGo言語で書かれていますが、低レベルな処理やOSとのインタラクションが必要な部分はC言語(またはアセンブリ言語)で書かれています。
-
ガベージコレクション (Garbage Collection, GC): プログラムが動的に割り当てたメモリのうち、もはや使用されていない(参照されていない)ものを自動的に解放する仕組みです。GoのGCは並行マーク&スイープ方式を採用しており、プログラムの実行と並行して動作することで、一時停止時間(Stop-the-World)を最小限に抑えています。
-
ヒープ (Heap): プログラムが動的にメモリを割り当てる領域です。Goでは、
make
やnew
で作成されるオブジェクト、および関数の呼び出しによってエスケープ解析の結果ヒープに割り当てられる変数がこの領域に配置されます。ヒープ上のメモリはGCによって管理されます。 -
スタック (Stack): 関数の呼び出しやローカル変数の保存に使用されるメモリ領域です。Goでは、各ゴルーチンが独自のスタックを持っています。スタックは通常、ヒープとは異なり、LIFO(Last-In, First-Out)の原則に従って自動的に割り当て・解放されます。Goのスタックは可変長であり、必要に応じて自動的に拡張・縮小されます。
-
mheap
: Goランタイムのグローバルなヒープアロケータです。OSから大きなメモリブロックを確保し、それをMSpan
と呼ばれるページ単位のチャンクに分割して管理します。 -
mcache
: 各P(プロセッサ、Goスケジューラの論理的なCPU)に紐付けられたローカルなメモリキャッシュです。小さなオブジェクトの割り当てを高速化するために使用されます。mcache
はmheap
からMSpan
を受け取り、それをさらに小さなオブジェクトに分割してゴルーチンに提供します。 -
MSpan
:mheap
が管理するメモリの基本的な単位です。連続したページ(通常4KB)のブロックを表します。MSpan
は、GC対象のオブジェクトを格納するMSpanInUse
、空き領域を表すMSpanFree
などの状態を持ちます。 -
FlagNoGC
: ガベージコレクションの対象外であることを示すフラグです。このフラグが設定されたオブジェクトは、GCによってスキャンされたり、解放されたりすることはありません。スタックは歴史的にこのフラグが設定されていました。 -
GoとCの相互運用: Goランタイムは、Go言語とC言語(およびアセンブリ言語)のコードが混在しています。GoコードからCコードを呼び出す場合や、その逆の場合には、特定の呼び出し規約やスタック管理の考慮が必要です。特に、Goのスケジューラが管理するGoスタックと、Cコードが使用するシステムスタックの間には違いがあります。
これらの概念を理解することで、スタックアロケータの分離がGoランタイムの全体的なアーキテクチャとパフォーマンスにどのように影響するかを深く理解することができます。
技術的詳細
このコミットの技術的な詳細は、主に以下の点に集約されます。
-
スタック専用のメモリ割り当てパスの導入:
runtime/malloc.h
に、スタックキャッシュのサイズ(StackCacheSize = 32*1024
)と、キャッシュされるスタックサイズのオーダー数(NumStackOrders = 3
)が定義されました。これは、異なるサイズのスタックセグメントを効率的に管理するためのものです。MSpan
構造体に、スタック専用の割り当てを示す新しい状態MSpanStack
が追加されました。これにより、MSpan
がGCヒープ用なのかスタック用なのかを明確に区別できるようになります。runtime/mheap.c
に、スタック専用のMSpan
を割り当てるruntime·MHeap_AllocStack
関数と、解放するruntime·MHeap_FreeStack
関数が追加されました。これらの関数は、従来のruntime·MHeap_Alloc
やruntime·MHeap_Free
とは異なり、スタックの特性(GC対象外であることなど)を考慮した処理を行います。runtime·MHeap_Alloc
関数自体も変更され、Goルーチンのスタック(Gスタック)上でヒープロックを伴う操作を行わないように、mcall
を使ってMスタック(OSスレッドのスタック)に切り替えてmheap_alloc
を呼び出すようになりました。これは、ヒープ割り当て中にスタック拡張が必要になった場合にデッドロックを避けるための重要な変更です。
-
スタックキャッシュの再設計:
runtime/stack.c
において、従来のグローバルなスタックキャッシュ(stackcache
)が廃止され、より洗練されたスタックプールとMCacheごとのスタックキャッシュが導入されました。- グローバルスタックプール:
stackpool
というMSpan
の配列が導入され、異なるサイズの空きスタックセグメントを管理します。stackpoolmu
というロックで保護されます。poolalloc
とpoolfree
関数がこのプールからの割り当てと解放を処理します。 - MCacheごとのスタックキャッシュ:
MCache
構造体(runtime/malloc.h
で定義)にstackcache[NumStackOrders]
というStackFreeList
の配列が追加されました。これにより、各P(プロセッサ)が自身のローカルなスタックキャッシュを持つことができるようになり、ロックの競合を減らし、スタック割り当てのパフォーマンスを向上させます。 stackcacherefill
とstackcacherelease
関数が、グローバルスタックプールとMCacheごとのスタックキャッシュ間でスタックセグメントを移動させるロジックに変更されました。runtime·stackinit
関数が追加され、スタックプールの初期化を行います。
-
メモリ統計の更新:
runtime/mem.go
のMemStats
構造体において、StackInuse
とStackSys
のコメントが更新され、スタックがヒープとは独立して管理されることを反映しています。runtime/mgc0.c
のruntime·updatememstats
関数が変更され、スタックの利用状況がヒープの統計から分離して計算されるようになりました。また、runtime·ReadMemStats
では、ユーザー向けにスタックの数値がヒープの数値から分離して報告されるようになりました。
-
スタック関連関数の変更:
runtime/proc.c
のgfput
関数(ゴルーチンをフリーリストに戻す関数)が、スタックサイズが固定サイズでない場合にruntime·stackfree
を呼び出すように変更されました。runtime/runtime.h
から、古いスタックキャッシュ関連のフィールド(stackinuse
,stackcachepos
,stackcachecnt
,stackcache
)がM
構造体から削除されました。また、Stktop
構造体からmalloced
フィールドが削除されました。runtime/stack.c
のruntime·stackalloc
とruntime·stackfree
関数が、新しいスタックキャッシュとMHeap_AllocStack
/MHeap_FreeStack
を利用するように全面的に書き換えられました。これにより、スタックの割り当てと解放がより効率的かつ独立して行われるようになります。runtime·shrinkstack
関数から、古いスタックの縮小ロジック(MHeap_SplitSpan
など)が削除され、copystack
によるコピーベースの縮小に一本化されました。これは、スタックがヒープから分離されたことで、ヒープのMSpan
を分割してスタックを縮小するというアプローチが不要になったためです。
これらの変更は、Goランタイムのメモリ管理サブシステムにおける重要な再構築であり、スタックとヒープの役割を明確に分離し、将来的な最適化とGo言語でのランタイム実装を可能にするための基盤を築いています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下のファイルと関数に集中しています。
-
src/pkg/runtime/malloc.h
:StackCacheSize
,NumStackOrders
といったスタックキャッシュ関連の定数定義。StackFreeList
構造体とMCache
構造体へのstackcache
フィールドの追加。MSpan
の状態を示すMSpanStack
の追加。MHeap
の関数プロトタイプにMHeap_AllocStack
とMHeap_FreeStack
が追加。
-
src/pkg/runtime/mheap.c
:runtime·MHeap_Alloc
関数の変更:g == g->m->g0
(Mスタック上)でない場合にmcall
を使ってMスタックに切り替えてmheap_alloc
を呼び出すロジックの追加。mheap_alloc
とmheap_free
という内部ヘルパー関数の導入。runtime·MHeap_AllocStack
関数の新規追加: スタック専用のMSpan
を割り当てる。runtime·MHeap_FreeStack
関数の新規追加: スタック専用のMSpan
を解放する。MHeap_AllocSpanLocked
関数の変更:MSpanInUse
とMSpanStack
の状態を区別。MHeap_FreeSpanLocked
関数の変更:MSpanStack
状態のMSpan
の解放を処理。runtime·MHeap_SplitSpan
関数の削除: スタックの縮小ロジックが変更されたため。
-
src/pkg/runtime/stack.c
:stackpool
とstackpoolmu
(グローバルスタックプール)の導入。runtime·stackinit
関数の新規追加: スタックプールの初期化。poolalloc
とpoolfree
関数の新規追加: グローバルスタックプールからのスタックセグメントの割り当てと解放。stackcacherefill
とstackcacherelease
関数の大幅な変更: MCacheごとのスタックキャッシュとグローバルスタックプール間の連携ロジック。runtime·stackcache_clear
関数の新規追加: MCacheのスタックキャッシュをクリアする。runtime·stackalloc
とruntime·stackfree
関数の全面的書き換え: 新しいスタックキャッシュとMHeap_AllocStack
/MHeap_FreeStack
を利用。copystack
とshrinkstack
関数の変更: スタックのコピーと縮小ロジックの簡素化。
-
src/pkg/runtime/runtime.h
:M
構造体からの古いスタックキャッシュ関連フィールドの削除。Stktop
構造体からのmalloced
フィールドの削除。M
構造体へのscalararg
とptrarg
フィールドの追加(mcall
で使用)。
-
src/pkg/runtime/mgc0.c
:flushallmcaches
とruntime·updatememstats
におけるスタック統計の更新ロジックの変更。runtime·ReadMemStats
におけるスタック統計の分離。
コアとなるコードの解説
src/pkg/runtime/malloc.h
の変更
StackCacheSize
とNumStackOrders
: これらはスタックキャッシュの設計における重要なパラメータです。StackCacheSize
は各Pのスタックキャッシュが保持できるスタックセグメントの合計サイズ(32KB)を示し、NumStackOrders
はキャッシュされるスタックセグメントのサイズクラスの数(3種類)を示します。これにより、Goランタイムは異なるサイズのスタック要求に効率的に対応できます。StackFreeList
とMCache
のstackcache
:StackFreeList
は空きスタックセグメントのリンクリストとその合計サイズを保持します。MCache
にこのstackcache
配列が追加されたことで、各Pが自身のローカルなスタックセグメントのキャッシュを持つことができ、スタックの割り当てと解放におけるグローバルロックの競合を大幅に削減します。MSpanStack
:MSpan
の状態にMSpanStack
が追加されたことで、メモリヒープが管理するMSpan
が、GC対象のオブジェクトを格納するMSpanInUse
と、スタックセグメントを格納するMSpanStack
に明確に区別されるようになりました。これにより、GCはMSpanStack
のメモリをスキャンする必要がなくなり、GCの効率が向上します。
src/pkg/runtime/mheap.c
の変更
runtime·MHeap_Alloc
のmcall
利用: この変更は、Goランタイムの設計思想における重要な進歩を示しています。runtime·MHeap_Alloc
はヒープメモリを割り当てるための主要な関数ですが、この関数がGoルーチンのスタック(Gスタック)上で実行中に、スタックの拡張が必要になった場合、そのスタック拡張自体がメモリ割り当てを必要とするため、デッドロックが発生する可能性がありました。これを避けるため、ヒープロックを伴う重要な処理(mheap_alloc
)は、g->m->g0
(OSスレッドのスタック、Mスタック)に切り替えて実行されるようになりました。runtime·mcall
は、現在のGスタックからMスタックに切り替えて指定された関数を実行し、その後Gスタックに戻るためのメカニズムです。これにより、ヒープ割り当てとスタック管理の間の循環的な依存関係が解消されます。runtime·MHeap_AllocStack
とruntime·MHeap_FreeStack
: これらの関数は、スタック専用のメモリ割り当てと解放を担当します。これらはMSpanStack
状態のMSpan
を操作し、mstats.stacks_inuse
(使用中のスタックメモリ量)を更新します。これにより、スタックメモリがGCヒープとは独立して管理されることが保証されます。runtime·MHeap_SplitSpan
の削除: 以前は、スタックの縮小時にMSpan
を分割してヒープに戻すロジックが存在しましたが、スタックがヒープから分離され、スタックの縮小がcopystack
によるコピーベースのメカニズムに一本化されたため、この関数は不要になりました。
src/pkg/runtime/stack.c
の変更
- グローバルスタックプール (
stackpool
):stackpool
は、異なるサイズの空きスタックセグメントを管理するグローバルなプールです。これは、各Pのローカルキャッシュが枯渇した場合に、スタックセグメントを供給する役割を担います。stackpoolmu
というロックで保護されており、複数のPからのアクセスを同期します。 poolalloc
とpoolfree
: これらの関数は、グローバルスタックプールからスタックセグメントを割り当てたり、プールに返却したりする低レベルな操作を行います。特にpoolalloc
では、プールに空きがない場合にruntime·MHeap_AllocStack
を呼び出して新しいMSpan
を割り当て、それをスタックセグメントに分割してプールに追加するロジックが含まれています。stackcacherefill
とstackcacherelease
: これらの関数は、MCacheごとのスタックキャッシュとグローバルスタックプール間のスタックセグメントの移動を管理します。stackcacherefill
は、ローカルキャッシュが不足した場合にグローバルプールからスタックセグメントを取得し、stackcacherelease
は、ローカルキャッシュが過剰になった場合にスタックセグメントをグローバルプールに返却します。これにより、スタックセグメントの再利用が促進され、メモリ割り当てのオーバーヘッドが削減されます。runtime·stackalloc
とruntime·stackfree
の再実装: これらの関数は、Goルーチンのスタックを割り当てたり解放したりする主要なインターフェースです。新しい実装では、まずMCacheごとのローカルキャッシュを試み、不足している場合はグローバルスタックプールから取得します。それでも不足する場合は、runtime·MHeap_AllocStack
を呼び出して新しいMSpan
を割り当てます。これにより、スタックの割り当てと解放が、ヒープのGCとは独立した、より効率的なパスで行われるようになりました。
これらの変更は、Goランタイムのメモリ管理において、スタックとヒープの役割を明確に分離し、それぞれの特性に合わせた最適化を可能にするための重要なステップです。これにより、ランタイムの複雑性が軽減され、将来的なパフォーマンス改善や機能拡張の余地が広がりました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/7c13860cd08352e785002cb97bd3baafd370e8bc
- Go CL (Code Review) ページ: https://golang.org/cl/104200047
参考にした情報源リンク
- GoのIssueトラッカー (Go issues #7468, #7424): 検索を試みましたが、具体的な情報は見つかりませんでした。古いIssueであるか、内部的なトラッキングによるものと考えられます。
- Goのメモリ管理に関する一般的なドキュメントやブログ記事 (Go runtime memory management, Go garbage collection, Go stack allocation)
- Goのソースコード (特に
src/pkg/runtime
ディレクトリ) - Goの設計に関する議論や提案 (Go design documents)
[インデックス 19639] ファイルの概要
このコミットは、Goランタイムにおけるスタックアロケータを、従来のmallocgc
(ガベージコレクタ管理下のメモリ割り当て)から分離することを目的としています。これにより、malloc
関数自体をGo言語で実装する準備を進めるとともに、ガベージコレクションの対象外(FlagNoGC
)であったスタックメモリの管理を独立させ、ヒープ管理の簡素化を図っています。
コミット
commit 7c13860cd08352e785002cb97bd3baafd370e8bc
Author: Keith Randall <khr@golang.org>
Date: Mon Jun 30 18:59:24 2014 -0700
runtime: stack allocator, separate from mallocgc
In order to move malloc to Go, we need to have a
separate stack allocator. If we run out of stack
during malloc, malloc will not be available
to allocate a new stack.
Stacks are the last remaining FlagNoGC objects in the
GC heap. Once they are out, we can get rid of the
distinction between the allocated/blockboundary bits.
(This will be in a separate change.)
Fixes #7468
Fixes #7424
LGTM=rsc, dvyukov
R=golang-codereviews, dvyukov, khr, dave, rsc
CC=golang-codereviews
https://golang.org/cl/104200047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7c13860cd08352e785002cb97bd3baafd370e8bc
元コミット内容
このコミットは、Goランタイムにおいてスタックアロケータをmallocgc
から分離する変更を導入しています。これは、malloc
関数をGo言語で実装するために不可欠なステップです。もしmalloc
の実行中にスタックが不足した場合、新しいスタックを割り当てるためにmalloc
自身を利用することができないという循環的な問題が発生するため、スタックの割り当てを独立させる必要がありました。
また、スタックはガベージコレクタのヒープ内でFlagNoGC
(GC対象外)として扱われる最後のオブジェクトでした。スタックがヒープから分離されることで、割り当て済み/ブロック境界のビット間の区別をなくすことが可能になります(これは別の変更で実施される予定です)。
この変更は、Issue #7468 および #7424 を修正します。
変更の背景
Goランタイムの進化において、メモリ管理の効率化とGo言語自身の表現力の向上は常に重要なテーマでした。このコミットの背景には、主に以下の二つの大きな動機があります。
-
malloc
のGo言語への移行: Goランタイムの初期段階では、パフォーマンスが要求される低レベルなメモリ割り当て処理(malloc
)はC言語で実装されていました。しかし、Go言語の成熟とランタイムの複雑化に伴い、ランタイムの大部分をGo言語で記述することで、コードの可読性、保守性、そして開発効率を向上させるという目標がありました。malloc
をGo言語に移行する上で、スタックの割り当てがボトルネックとなる可能性がありました。具体的には、malloc
が新しいメモリを割り当てる際に、自身の実行に必要なスタックが不足した場合、そのスタックを確保するために再度malloc
を呼び出すというデッドロックのような状況が発生する可能性がありました。この循環的な依存関係を解消するためには、スタックの割り当てをmalloc
から完全に独立させる必要がありました。 -
ガベージコレクションの簡素化: Goのガベージコレクタは、ヒープ上のオブジェクトを追跡し、不要になったメモリを解放する役割を担っています。しかし、スタックは特殊なメモリ領域であり、GCの対象外(
FlagNoGC
)として扱われていました。これは、スタックがプログラムの実行フローに密接に関わっており、GCのサイクルとは異なるライフサイクルを持つためです。FlagNoGC
オブジェクトがヒープ内に存在することは、GCの実装を複雑にし、ヒープのメモリレイアウトに関する特定の制約を課していました。スタックをヒープから完全に分離することで、GCは純粋にヒープ上のGC対象オブジェクトのみを考慮すればよくなり、GCの実装を簡素化し、将来的なGCアルゴリズムの改善や最適化の余地を広げることができます。コミットメッセージにある「allocated/blockboundary bits
の区別をなくす」という言及は、このGCの簡素化に向けたステップを示唆しています。
これらの背景から、スタックアロケータの分離は、Goランタイムのアーキテクチャをよりクリーンにし、将来の機能拡張やパフォーマンス改善のための基盤を固める重要な変更であったと言えます。
前提知識の解説
このコミットの変更内容を理解するためには、Goランタイムのメモリ管理とスタック管理に関するいくつかの基本的な概念を理解しておく必要があります。
-
Goランタイム (Go Runtime): Goプログラムの実行を管理する部分です。ガベージコレクション、スケジューリング、メモリ割り当て、ゴルーチン管理など、多岐にわたる機能を提供します。Goランタイムの一部はGo言語で書かれていますが、低レベルな処理やOSとのインタラクションが必要な部分はC言語(またはアセンブリ言語)で書かれています。
-
ガベージコレクション (Garbage Collection, GC): プログラムが動的に割り当てたメモリのうち、もはや使用されていない(参照されていない)ものを自動的に解放する仕組みです。GoのGCは並行マーク&スイープ方式を採用しており、プログラムの実行と並行して動作することで、一時停止時間(Stop-the-World)を最小限に抑えています。
-
ヒープ (Heap): プログラムが動的にメモリを割り当てる領域です。Goでは、
make
やnew
で作成されるオブジェクト、および関数の呼び出しによってエスケープ解析の結果ヒープに割り当てられる変数がこの領域に配置されます。ヒープ上のメモリはGCによって管理されます。 -
スタック (Stack): 関数の呼び出しやローカル変数の保存に使用されるメモリ領域です。Goでは、各ゴルーチンが独自のスタックを持っています。スタックは通常、ヒープとは異なり、LIFO(Last-In, First-Out)の原則に従って自動的に割り当て・解放されます。Goのスタックは可変長であり、必要に応じて自動的に拡張・縮小されます。
-
mheap
: Goランタイムのグローバルなヒープアロケータです。OSから大きなメモリブロックを確保し、それをMSpan
と呼ばれるページ単位のチャンクに分割して管理します。 -
mcache
: 各P(プロセッサ、Goスケジューラの論理的なCPU)に紐付けられたローカルなメモリキャッシュです。小さなオブジェクトの割り当てを高速化するために使用されます。mcache
はmheap
からMSpan
を受け取り、それをさらに小さなオブジェクトに分割してゴルーチンに提供します。 -
MSpan
:mheap
が管理するメモリの基本的な単位です。連続したページ(通常4KB)のブロックを表します。MSpan
は、GC対象のオブジェクトを格納するMSpanInUse
、空き領域を表すMSpanFree
などの状態を持ちます。 -
FlagNoGC
: ガベージコレクションの対象外であることを示すフラグです。このフラグが設定されたオブジェクトは、GCによってスキャンされたり、解放されたりすることはありません。スタックは歴史的にこのフラグが設定されていました。 -
GoとCの相互運用: Goランタイムは、Go言語とC言語(およびアセンブリ言語)のコードが混在しています。GoコードからCコードを呼び出す場合や、その逆の場合には、特定の呼び出し規約やスタック管理の考慮が必要です。特に、Goのスケジューラが管理するGoスタックと、Cコードが使用するシステムスタックの間には違いがあります。
これらの概念を理解することで、スタックアロケータの分離がGoランタイムの全体的なアーキテクチャとパフォーマンスにどのように影響するかを深く理解することができます。
技術的詳細
このコミットの技術的な詳細は、主に以下の点に集約されます。
-
スタック専用のメモリ割り当てパスの導入:
runtime/malloc.h
に、スタックキャッシュのサイズ(StackCacheSize = 32*1024
)と、キャッシュされるスタックサイズのオーダー数(NumStackOrders = 3
)が定義されました。これは、異なるサイズのスタックセグメントを効率的に管理するためのものです。MSpan
構造体に、スタック専用の割り当てを示す新しい状態MSpanStack
が追加されました。これにより、MSpan
がGCヒープ用なのかスタック用なのかを明確に区別できるようになります。runtime/mheap.c
に、スタック専用のMSpan
を割り当てるruntime·MHeap_AllocStack
関数と、解放するruntime·MHeap_FreeStack
関数が追加されました。これらの関数は、従来のruntime·MHeap_Alloc
やruntime·MHeap_Free
とは異なり、スタックの特性(GC対象外であることなど)を考慮した処理を行います。runtime·MHeap_Alloc
関数自体も変更され、Goルーチンのスタック(Gスタック)上でヒープロックを伴う操作を行わないように、mcall
を使ってMスタック(OSスレッドのスタック)に切り替えてmheap_alloc
を呼び出すようになりました。これは、ヒープ割り当て中にスタック拡張が必要になった場合にデッドロックを避けるための重要な変更です。
-
スタックキャッシュの再設計:
runtime/stack.c
において、従来のグローバルなスタックキャッシュ(stackcache
)が廃止され、より洗練されたスタックプールとMCacheごとのスタックキャッシュが導入されました。- グローバルスタックプール:
stackpool
というMSpan
の配列が導入され、異なるサイズの空きスタックセグメントを管理します。stackpoolmu
というロックで保護されます。poolalloc
とpoolfree
関数がこのプールからの割り当てと解放を処理します。 - MCacheごとのスタックキャッシュ:
MCache
構造体(runtime/malloc.h
で定義)にstackcache[NumStackOrders]
というStackFreeList
の配列が追加されました。これにより、各P(プロセッサ)が自身のローカルなスタックキャッシュを持つことができるようになり、ロックの競合を減らし、スタック割り当てのパフォーマンスを向上させます。 stackcacherefill
とstackcacherelease
関数が、グローバルスタックプールとMCacheごとのスタックキャッシュ間でスタックセグメントを移動させるロジックに変更されました。runtime·stackinit
関数が追加され、スタックプールの初期化を行います。
-
メモリ統計の更新:
runtime/mem.go
のMemStats
構造体において、StackInuse
とStackSys
のコメントが更新され、スタックがヒープとは独立して管理されることを反映しています。runtime/mgc0.c
のruntime·updatememstats
関数が変更され、スタックの利用状況がヒープの統計から分離して計算されるようになりました。また、runtime·ReadMemStats
では、ユーザー向けにスタックの数値がヒープの数値から分離して報告されるようになりました。
-
スタック関連関数の変更:
runtime/proc.c
のgfput
関数(ゴルーチンをフリーリストに戻す関数)が、スタックサイズが固定サイズでない場合にruntime·stackfree
を呼び出すように変更されました。runtime/runtime.h
から、古いスタックキャッシュ関連のフィールド(stackinuse
,stackcachepos
,stackcachecnt
,stackcache
)がM
構造体から削除されました。また、Stktop
構造体からmalloced
フィールドが削除されました。runtime/stack.c
のruntime·stackalloc
とruntime·stackfree
関数が、新しいスタックキャッシュとMHeap_AllocStack
/MHeap_FreeStack
を利用するように全面的に書き換えられました。これにより、スタックの割り当てと解放がより効率的かつ独立して行われるようになります。runtime·shrinkstack
関数から、古いスタックの縮小ロジック(MHeap_SplitSpan
など)が削除され、copystack
によるコピーベースの縮小に一本化されました。これは、スタックがヒープから分離されたことで、ヒープのMSpan
を分割してスタックを縮小するというアプローチが不要になったためです。
これらの変更は、Goランタイムのメモリ管理サブシステムにおける重要な再構築であり、スタックとヒープの役割を明確に分離し、将来的な最適化とGo言語でのランタイム実装を可能にするための基盤を築いています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下のファイルと関数に集中しています。
-
src/pkg/runtime/malloc.h
:StackCacheSize
,NumStackOrders
といったスタックキャッシュ関連の定数定義。StackFreeList
構造体とMCache
構造体へのstackcache
フィールドの追加。MSpan
の状態を示すMSpanStack
の追加。MHeap
の関数プロトタイプにMHeap_AllocStack
とMHeap_FreeStack
が追加。
-
src/pkg/runtime/mheap.c
:runtime·MHeap_Alloc
関数の変更:g == g->m->g0
(Mスタック上)でない場合にmcall
を使ってMスタックに切り替えてmheap_alloc
を呼び出すロジックの追加。mheap_alloc
とmheap_free
という内部ヘルパー関数の導入。runtime·MHeap_AllocStack
関数の新規追加: スタック専用のMSpan
を割り当てる。runtime·MHeap_FreeStack
関数の新規追加: スタック専用のMSpan
を解放する。MHeap_AllocSpanLocked
関数の変更:MSpanInUse
とMSpanStack
の状態を区別。MHeap_FreeSpanLocked
関数の変更:MSpanStack
状態のMSpan
の解放を処理。runtime·MHeap_SplitSpan
関数の削除: スタックの縮小ロジックが変更されたため。
-
src/pkg/runtime/stack.c
:stackpool
とstackpoolmu
(グローバルスタックプール)の導入。runtime·stackinit
関数の新規追加: スタックプールの初期化。poolalloc
とpoolfree
関数の新規追加: グローバルスタックプールからのスタックセグメントの割り当てと解放。stackcacherefill
とstackcacherelease
関数の大幅な変更: MCacheごとのスタックキャッシュとグローバルスタックプール間の連携ロジック。runtime·stackcache_clear
関数の新規追加: MCacheのスタックキャッシュをクリアする。runtime·stackalloc
とruntime·stackfree
関数の全面的書き換え: 新しいスタックキャッシュとMHeap_AllocStack
/MHeap_FreeStack
を利用。copystack
とshrinkstack
関数の変更: スタックのコピーと縮小ロジックの簡素化。
-
src/pkg/runtime/runtime.h
:M
構造体からの古いスタックキャッシュ関連フィールドの削除。Stktop
構造体からのmalloced
フィールドの削除。M
構造体へのscalararg
とptrarg
フィールドの追加(mcall
で使用)。
-
src/pkg/runtime/mgc0.c
:flushallmcaches
とruntime·updatememstats
におけるスタック統計の更新ロジックの変更。runtime·ReadMemStats
におけるスタック統計の分離。
コアとなるコードの解説
src/pkg/runtime/malloc.h
の変更
StackCacheSize
とNumStackOrders
: これらはスタックキャッシュの設計における重要なパラメータです。StackCacheSize
は各Pのスタックキャッシュが保持できるスタックセグメントの合計サイズ(32KB)を示し、NumStackOrders
はキャッシュされるスタックセグメントのサイズクラスの数(3種類)を示します。これにより、Goランタイムは異なるサイズのスタック要求に効率的に対応できます。StackFreeList
とMCache
のstackcache
:StackFreeList
は空きスタックセグメントのリンクリストとその合計サイズを保持します。MCache
にこのstackcache
配列が追加されたことで、各Pが自身のローカルなスタックセグメントのキャッシュを持つことができ、スタックの割り当てと解放におけるグローバルロックの競合を大幅に削減します。MSpanStack
:MSpan
の状態にMSpanStack
が追加されたことで、メモリヒープが管理するMSpan
が、GC対象のオブジェクトを格納するMSpanInUse
と、スタックセグメントを格納するMSpanStack
に明確に区別されるようになりました。これにより、GCはMSpanStack
のメモリをスキャンする必要がなくなり、GCの効率が向上します。
src/pkg/runtime/mheap.c
の変更
runtime·MHeap_Alloc
のmcall
利用: この変更は、Goランタイムの設計思想における重要な進歩を示しています。runtime·MHeap_Alloc
はヒープメモリを割り当てるための主要な関数ですが、この関数がGoルーチンのスタック(Gスタック)上で実行中に、スタックの拡張が必要になった場合、そのスタック拡張自体がメモリ割り当てを必要とするため、デッドロックが発生する可能性がありました。これを避けるため、ヒープロックを伴う重要な処理(mheap_alloc
)は、g->m->g0
(OSスレッドのスタック、Mスタック)に切り替えて実行されるようになりました。runtime·mcall
は、現在のGスタックからMスタックに切り替えて指定された関数を実行し、その後Gスタックに戻るためのメカニズムです。これにより、ヒープ割り当てとスタック管理の間の循環的な依存関係が解消されます。runtime·MHeap_AllocStack
とruntime·MHeap_FreeStack
: これらの関数は、スタック専用のメモリ割り当てと解放を担当します。これらはMSpanStack
状態のMSpan
を操作し、mstats.stacks_inuse
(使用中のスタックメモリ量)を更新します。これにより、スタックメモリがGCヒープとは独立して管理されることが保証されます。runtime·MHeap_SplitSpan
の削除: 以前は、スタックの縮小時にMSpan
を分割してヒープに戻すロジックが存在しましたが、スタックがヒープから分離され、スタックの縮小がcopystack
によるコピーベースのメカニズムに一本化されたため、この関数は不要になりました。
src/pkg/runtime/stack.c
の変更
- グローバルスタックプール (
stackpool
):stackpool
は、異なるサイズの空きスタックセグメントを管理するグローバルなプールです。これは、各Pのローカルキャッシュが枯渇した場合に、スタックセグメントを供給する役割を担います。stackpoolmu
というロックで保護されており、複数のPからのアクセスを同期します。 poolalloc
とpoolfree
: これらの関数は、グローバルスタックプールからスタックセグメントを割り当てたり、プールに返却したりする低レベルな操作を行います。特にpoolalloc
では、プールに空きがない場合にruntime·MHeap_AllocStack
を呼び出して新しいMSpan
を割り当て、それをスタックセグメントに分割してプールに追加するロジックが含まれています。stackcacherefill
とstackcacherelease
: これらの関数は、MCacheごとのスタックキャッシュとグローバルスタックプール間のスタックセグメントの移動を管理します。stackcacherefill
は、ローカルキャッシュが不足した場合にグローバルプールからスタックセグメントを取得し、stackcacherelease
は、ローカルキャッシュが過剰になった場合にスタックセグメントをグローバルプールに返却します。これにより、スタックセグメントの再利用が促進され、メモリ割り当てのオーバーヘッドが削減されます。runtime·stackalloc
とruntime·stackfree
の再実装: これらの関数は、Goルーチンのスタックを割り当てたり解放したりする主要なインターフェースです。新しい実装では、まずMCacheごとのローカルキャッシュを試み、不足している場合はグローバルスタックプールから取得します。それでも不足する場合は、runtime·MHeap_AllocStack
を呼び出して新しいMSpan
を割り当てます。これにより、スタックの割り当てと解放が、ヒープのGCとは独立した、より効率的なパスで行われるようになりました。
これらの変更は、Goランタイムのメモリ管理において、スタックとヒープの役割を明確に分離し、それぞれの特性に合わせた最適化を可能にするための重要なステップです。これにより、ランタイムの複雑性が軽減され、将来的なパフォーマンス改善や機能拡張の余地が広がりました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/7c13860cd08352e785002cb97bd3baafd370e8bc
- Go CL (Code Review) ページ: https://golang.org/cl/104200047
参考にした情報源リンク
- GoのIssueトラッカー (Go issues #7468, #7424): 検索を試みましたが、具体的な情報は見つかりませんでした。古いIssueであるか、内部的なトラッキングによるものと考えられます。
- Goのメモリ管理に関する一般的なドキュメントやブログ記事 (Go runtime memory management, Go garbage collection, Go stack allocation)
- Goのソースコード (特に
src/pkg/runtime
ディレクトリ) - Goの設計に関する議論や提案 (Go design documents)