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

[インデックス 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言語自身の表現力の向上は常に重要なテーマでした。このコミットの背景には、主に以下の二つの大きな動機があります。

  1. mallocのGo言語への移行: Goランタイムの初期段階では、パフォーマンスが要求される低レベルなメモリ割り当て処理(malloc)はC言語で実装されていました。しかし、Go言語の成熟とランタイムの複雑化に伴い、ランタイムの大部分をGo言語で記述することで、コードの可読性、保守性、そして開発効率を向上させるという目標がありました。mallocをGo言語に移行する上で、スタックの割り当てがボトルネックとなる可能性がありました。具体的には、mallocが新しいメモリを割り当てる際に、自身の実行に必要なスタックが不足した場合、そのスタックを確保するために再度mallocを呼び出すというデッドロックのような状況が発生する可能性がありました。この循環的な依存関係を解消するためには、スタックの割り当てをmallocから完全に独立させる必要がありました。

  2. ガベージコレクションの簡素化: 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では、makenewで作成されるオブジェクト、および関数の呼び出しによってエスケープ解析の結果ヒープに割り当てられる変数がこの領域に配置されます。ヒープ上のメモリはGCによって管理されます。

  • スタック (Stack): 関数の呼び出しやローカル変数の保存に使用されるメモリ領域です。Goでは、各ゴルーチンが独自のスタックを持っています。スタックは通常、ヒープとは異なり、LIFO(Last-In, First-Out)の原則に従って自動的に割り当て・解放されます。Goのスタックは可変長であり、必要に応じて自動的に拡張・縮小されます。

  • mheap: Goランタイムのグローバルなヒープアロケータです。OSから大きなメモリブロックを確保し、それをMSpanと呼ばれるページ単位のチャンクに分割して管理します。

  • mcache: 各P(プロセッサ、Goスケジューラの論理的なCPU)に紐付けられたローカルなメモリキャッシュです。小さなオブジェクトの割り当てを高速化するために使用されます。mcachemheapからMSpanを受け取り、それをさらに小さなオブジェクトに分割してゴルーチンに提供します。

  • MSpan: mheapが管理するメモリの基本的な単位です。連続したページ(通常4KB)のブロックを表します。MSpanは、GC対象のオブジェクトを格納するMSpanInUse、空き領域を表すMSpanFreeなどの状態を持ちます。

  • FlagNoGC: ガベージコレクションの対象外であることを示すフラグです。このフラグが設定されたオブジェクトは、GCによってスキャンされたり、解放されたりすることはありません。スタックは歴史的にこのフラグが設定されていました。

  • GoとCの相互運用: Goランタイムは、Go言語とC言語(およびアセンブリ言語)のコードが混在しています。GoコードからCコードを呼び出す場合や、その逆の場合には、特定の呼び出し規約やスタック管理の考慮が必要です。特に、Goのスケジューラが管理するGoスタックと、Cコードが使用するシステムスタックの間には違いがあります。

これらの概念を理解することで、スタックアロケータの分離がGoランタイムの全体的なアーキテクチャとパフォーマンスにどのように影響するかを深く理解することができます。

技術的詳細

このコミットの技術的な詳細は、主に以下の点に集約されます。

  1. スタック専用のメモリ割り当てパスの導入:

    • runtime/malloc.hに、スタックキャッシュのサイズ(StackCacheSize = 32*1024)と、キャッシュされるスタックサイズのオーダー数(NumStackOrders = 3)が定義されました。これは、異なるサイズのスタックセグメントを効率的に管理するためのものです。
    • MSpan構造体に、スタック専用の割り当てを示す新しい状態MSpanStackが追加されました。これにより、MSpanがGCヒープ用なのかスタック用なのかを明確に区別できるようになります。
    • runtime/mheap.cに、スタック専用のMSpanを割り当てるruntime·MHeap_AllocStack関数と、解放するruntime·MHeap_FreeStack関数が追加されました。これらの関数は、従来のruntime·MHeap_Allocruntime·MHeap_Freeとは異なり、スタックの特性(GC対象外であることなど)を考慮した処理を行います。
    • runtime·MHeap_Alloc関数自体も変更され、Goルーチンのスタック(Gスタック)上でヒープロックを伴う操作を行わないように、mcallを使ってMスタック(OSスレッドのスタック)に切り替えてmheap_allocを呼び出すようになりました。これは、ヒープ割り当て中にスタック拡張が必要になった場合にデッドロックを避けるための重要な変更です。
  2. スタックキャッシュの再設計:

    • runtime/stack.cにおいて、従来のグローバルなスタックキャッシュ(stackcache)が廃止され、より洗練されたスタックプールとMCacheごとのスタックキャッシュが導入されました。
    • グローバルスタックプール: stackpoolというMSpanの配列が導入され、異なるサイズの空きスタックセグメントを管理します。stackpoolmuというロックで保護されます。poolallocpoolfree関数がこのプールからの割り当てと解放を処理します。
    • MCacheごとのスタックキャッシュ: MCache構造体(runtime/malloc.hで定義)にstackcache[NumStackOrders]というStackFreeListの配列が追加されました。これにより、各P(プロセッサ)が自身のローカルなスタックキャッシュを持つことができるようになり、ロックの競合を減らし、スタック割り当てのパフォーマンスを向上させます。
    • stackcacherefillstackcacherelease関数が、グローバルスタックプールとMCacheごとのスタックキャッシュ間でスタックセグメントを移動させるロジックに変更されました。
    • runtime·stackinit関数が追加され、スタックプールの初期化を行います。
  3. メモリ統計の更新:

    • runtime/mem.goMemStats構造体において、StackInuseStackSysのコメントが更新され、スタックがヒープとは独立して管理されることを反映しています。
    • runtime/mgc0.cruntime·updatememstats関数が変更され、スタックの利用状況がヒープの統計から分離して計算されるようになりました。また、runtime·ReadMemStatsでは、ユーザー向けにスタックの数値がヒープの数値から分離して報告されるようになりました。
  4. スタック関連関数の変更:

    • runtime/proc.cgfput関数(ゴルーチンをフリーリストに戻す関数)が、スタックサイズが固定サイズでない場合にruntime·stackfreeを呼び出すように変更されました。
    • runtime/runtime.hから、古いスタックキャッシュ関連のフィールド(stackinuse, stackcachepos, stackcachecnt, stackcache)がM構造体から削除されました。また、Stktop構造体からmallocedフィールドが削除されました。
    • runtime/stack.cruntime·stackallocruntime·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_AllocStackMHeap_FreeStackが追加。
  • src/pkg/runtime/mheap.c:

    • runtime·MHeap_Alloc関数の変更: g == g->m->g0(Mスタック上)でない場合にmcallを使ってMスタックに切り替えてmheap_allocを呼び出すロジックの追加。
    • mheap_allocmheap_freeという内部ヘルパー関数の導入。
    • runtime·MHeap_AllocStack関数の新規追加: スタック専用のMSpanを割り当てる。
    • runtime·MHeap_FreeStack関数の新規追加: スタック専用のMSpanを解放する。
    • MHeap_AllocSpanLocked関数の変更: MSpanInUseMSpanStackの状態を区別。
    • MHeap_FreeSpanLocked関数の変更: MSpanStack状態のMSpanの解放を処理。
    • runtime·MHeap_SplitSpan関数の削除: スタックの縮小ロジックが変更されたため。
  • src/pkg/runtime/stack.c:

    • stackpoolstackpoolmu(グローバルスタックプール)の導入。
    • runtime·stackinit関数の新規追加: スタックプールの初期化。
    • poolallocpoolfree関数の新規追加: グローバルスタックプールからのスタックセグメントの割り当てと解放。
    • stackcacherefillstackcacherelease関数の大幅な変更: MCacheごとのスタックキャッシュとグローバルスタックプール間の連携ロジック。
    • runtime·stackcache_clear関数の新規追加: MCacheのスタックキャッシュをクリアする。
    • runtime·stackallocruntime·stackfree関数の全面的書き換え: 新しいスタックキャッシュとMHeap_AllocStack/MHeap_FreeStackを利用。
    • copystackshrinkstack関数の変更: スタックのコピーと縮小ロジックの簡素化。
  • src/pkg/runtime/runtime.h:

    • M構造体からの古いスタックキャッシュ関連フィールドの削除。
    • Stktop構造体からのmallocedフィールドの削除。
    • M構造体へのscalarargptrargフィールドの追加(mcallで使用)。
  • src/pkg/runtime/mgc0.c:

    • flushallmcachesruntime·updatememstatsにおけるスタック統計の更新ロジックの変更。
    • runtime·ReadMemStatsにおけるスタック統計の分離。

コアとなるコードの解説

src/pkg/runtime/malloc.hの変更

  • StackCacheSizeNumStackOrders: これらはスタックキャッシュの設計における重要なパラメータです。StackCacheSizeは各Pのスタックキャッシュが保持できるスタックセグメントの合計サイズ(32KB)を示し、NumStackOrdersはキャッシュされるスタックセグメントのサイズクラスの数(3種類)を示します。これにより、Goランタイムは異なるサイズのスタック要求に効率的に対応できます。
  • StackFreeListMCachestackcache: StackFreeListは空きスタックセグメントのリンクリストとその合計サイズを保持します。MCacheにこのstackcache配列が追加されたことで、各Pが自身のローカルなスタックセグメントのキャッシュを持つことができ、スタックの割り当てと解放におけるグローバルロックの競合を大幅に削減します。
  • MSpanStack: MSpanの状態にMSpanStackが追加されたことで、メモリヒープが管理するMSpanが、GC対象のオブジェクトを格納するMSpanInUseと、スタックセグメントを格納するMSpanStackに明確に区別されるようになりました。これにより、GCはMSpanStackのメモリをスキャンする必要がなくなり、GCの効率が向上します。

src/pkg/runtime/mheap.cの変更

  • runtime·MHeap_Allocmcall利用: この変更は、Goランタイムの設計思想における重要な進歩を示しています。runtime·MHeap_Allocはヒープメモリを割り当てるための主要な関数ですが、この関数がGoルーチンのスタック(Gスタック)上で実行中に、スタックの拡張が必要になった場合、そのスタック拡張自体がメモリ割り当てを必要とするため、デッドロックが発生する可能性がありました。これを避けるため、ヒープロックを伴う重要な処理(mheap_alloc)は、g->m->g0(OSスレッドのスタック、Mスタック)に切り替えて実行されるようになりました。runtime·mcallは、現在のGスタックからMスタックに切り替えて指定された関数を実行し、その後Gスタックに戻るためのメカニズムです。これにより、ヒープ割り当てとスタック管理の間の循環的な依存関係が解消されます。
  • runtime·MHeap_AllocStackruntime·MHeap_FreeStack: これらの関数は、スタック専用のメモリ割り当てと解放を担当します。これらはMSpanStack状態のMSpanを操作し、mstats.stacks_inuse(使用中のスタックメモリ量)を更新します。これにより、スタックメモリがGCヒープとは独立して管理されることが保証されます。
  • runtime·MHeap_SplitSpanの削除: 以前は、スタックの縮小時にMSpanを分割してヒープに戻すロジックが存在しましたが、スタックがヒープから分離され、スタックの縮小がcopystackによるコピーベースのメカニズムに一本化されたため、この関数は不要になりました。

src/pkg/runtime/stack.cの変更

  • グローバルスタックプール (stackpool): stackpoolは、異なるサイズの空きスタックセグメントを管理するグローバルなプールです。これは、各Pのローカルキャッシュが枯渇した場合に、スタックセグメントを供給する役割を担います。stackpoolmuというロックで保護されており、複数のPからのアクセスを同期します。
  • poolallocpoolfree: これらの関数は、グローバルスタックプールからスタックセグメントを割り当てたり、プールに返却したりする低レベルな操作を行います。特にpoolallocでは、プールに空きがない場合にruntime·MHeap_AllocStackを呼び出して新しいMSpanを割り当て、それをスタックセグメントに分割してプールに追加するロジックが含まれています。
  • stackcacherefillstackcacherelease: これらの関数は、MCacheごとのスタックキャッシュとグローバルスタックプール間のスタックセグメントの移動を管理します。stackcacherefillは、ローカルキャッシュが不足した場合にグローバルプールからスタックセグメントを取得し、stackcachereleaseは、ローカルキャッシュが過剰になった場合にスタックセグメントをグローバルプールに返却します。これにより、スタックセグメントの再利用が促進され、メモリ割り当てのオーバーヘッドが削減されます。
  • runtime·stackallocruntime·stackfreeの再実装: これらの関数は、Goルーチンのスタックを割り当てたり解放したりする主要なインターフェースです。新しい実装では、まずMCacheごとのローカルキャッシュを試み、不足している場合はグローバルスタックプールから取得します。それでも不足する場合は、runtime·MHeap_AllocStackを呼び出して新しいMSpanを割り当てます。これにより、スタックの割り当てと解放が、ヒープのGCとは独立した、より効率的なパスで行われるようになりました。

これらの変更は、Goランタイムのメモリ管理において、スタックとヒープの役割を明確に分離し、それぞれの特性に合わせた最適化を可能にするための重要なステップです。これにより、ランタイムの複雑性が軽減され、将来的なパフォーマンス改善や機能拡張の余地が広がりました。

関連リンク

参考にした情報源リンク

  • 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言語自身の表現力の向上は常に重要なテーマでした。このコミットの背景には、主に以下の二つの大きな動機があります。

  1. mallocのGo言語への移行: Goランタイムの初期段階では、パフォーマンスが要求される低レベルなメモリ割り当て処理(malloc)はC言語で実装されていました。しかし、Go言語の成熟とランタイムの複雑化に伴い、ランタイムの大部分をGo言語で記述することで、コードの可読性、保守性、そして開発効率を向上させるという目標がありました。mallocをGo言語に移行する上で、スタックの割り当てがボトルネックとなる可能性がありました。具体的には、mallocが新しいメモリを割り当てる際に、自身の実行に必要なスタックが不足した場合、そのスタックを確保するために再度mallocを呼び出すというデッドロックのような状況が発生する可能性がありました。この循環的な依存関係を解消するためには、スタックの割り当てをmallocから完全に独立させる必要がありました。

  2. ガベージコレクションの簡素化: 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では、makenewで作成されるオブジェクト、および関数の呼び出しによってエスケープ解析の結果ヒープに割り当てられる変数がこの領域に配置されます。ヒープ上のメモリはGCによって管理されます。

  • スタック (Stack): 関数の呼び出しやローカル変数の保存に使用されるメモリ領域です。Goでは、各ゴルーチンが独自のスタックを持っています。スタックは通常、ヒープとは異なり、LIFO(Last-In, First-Out)の原則に従って自動的に割り当て・解放されます。Goのスタックは可変長であり、必要に応じて自動的に拡張・縮小されます。

  • mheap: Goランタイムのグローバルなヒープアロケータです。OSから大きなメモリブロックを確保し、それをMSpanと呼ばれるページ単位のチャンクに分割して管理します。

  • mcache: 各P(プロセッサ、Goスケジューラの論理的なCPU)に紐付けられたローカルなメモリキャッシュです。小さなオブジェクトの割り当てを高速化するために使用されます。mcachemheapからMSpanを受け取り、それをさらに小さなオブジェクトに分割してゴルーチンに提供します。

  • MSpan: mheapが管理するメモリの基本的な単位です。連続したページ(通常4KB)のブロックを表します。MSpanは、GC対象のオブジェクトを格納するMSpanInUse、空き領域を表すMSpanFreeなどの状態を持ちます。

  • FlagNoGC: ガベージコレクションの対象外であることを示すフラグです。このフラグが設定されたオブジェクトは、GCによってスキャンされたり、解放されたりすることはありません。スタックは歴史的にこのフラグが設定されていました。

  • GoとCの相互運用: Goランタイムは、Go言語とC言語(およびアセンブリ言語)のコードが混在しています。GoコードからCコードを呼び出す場合や、その逆の場合には、特定の呼び出し規約やスタック管理の考慮が必要です。特に、Goのスケジューラが管理するGoスタックと、Cコードが使用するシステムスタックの間には違いがあります。

これらの概念を理解することで、スタックアロケータの分離がGoランタイムの全体的なアーキテクチャとパフォーマンスにどのように影響するかを深く理解することができます。

技術的詳細

このコミットの技術的な詳細は、主に以下の点に集約されます。

  1. スタック専用のメモリ割り当てパスの導入:

    • runtime/malloc.hに、スタックキャッシュのサイズ(StackCacheSize = 32*1024)と、キャッシュされるスタックサイズのオーダー数(NumStackOrders = 3)が定義されました。これは、異なるサイズのスタックセグメントを効率的に管理するためのものです。
    • MSpan構造体に、スタック専用の割り当てを示す新しい状態MSpanStackが追加されました。これにより、MSpanがGCヒープ用なのかスタック用なのかを明確に区別できるようになります。
    • runtime/mheap.cに、スタック専用のMSpanを割り当てるruntime·MHeap_AllocStack関数と、解放するruntime·MHeap_FreeStack関数が追加されました。これらの関数は、従来のruntime·MHeap_Allocruntime·MHeap_Freeとは異なり、スタックの特性(GC対象外であることなど)を考慮した処理を行います。
    • runtime·MHeap_Alloc関数自体も変更され、Goルーチンのスタック(Gスタック)上でヒープロックを伴う操作を行わないように、mcallを使ってMスタック(OSスレッドのスタック)に切り替えてmheap_allocを呼び出すようになりました。これは、ヒープ割り当て中にスタック拡張が必要になった場合にデッドロックを避けるための重要な変更です。
  2. スタックキャッシュの再設計:

    • runtime/stack.cにおいて、従来のグローバルなスタックキャッシュ(stackcache)が廃止され、より洗練されたスタックプールとMCacheごとのスタックキャッシュが導入されました。
    • グローバルスタックプール: stackpoolというMSpanの配列が導入され、異なるサイズの空きスタックセグメントを管理します。stackpoolmuというロックで保護されます。poolallocpoolfree関数がこのプールからの割り当てと解放を処理します。
    • MCacheごとのスタックキャッシュ: MCache構造体(runtime/malloc.hで定義)にstackcache[NumStackOrders]というStackFreeListの配列が追加されました。これにより、各P(プロセッサ)が自身のローカルなスタックキャッシュを持つことができるようになり、ロックの競合を減らし、スタック割り当てのパフォーマンスを向上させます。
    • stackcacherefillstackcacherelease関数が、グローバルスタックプールとMCacheごとのスタックキャッシュ間でスタックセグメントを移動させるロジックに変更されました。
    • runtime·stackinit関数が追加され、スタックプールの初期化を行います。
  3. メモリ統計の更新:

    • runtime/mem.goMemStats構造体において、StackInuseStackSysのコメントが更新され、スタックがヒープとは独立して管理されることを反映しています。
    • runtime/mgc0.cruntime·updatememstats関数が変更され、スタックの利用状況がヒープの統計から分離して計算されるようになりました。また、runtime·ReadMemStatsでは、ユーザー向けにスタックの数値がヒープの数値から分離して報告されるようになりました。
  4. スタック関連関数の変更:

    • runtime/proc.cgfput関数(ゴルーチンをフリーリストに戻す関数)が、スタックサイズが固定サイズでない場合にruntime·stackfreeを呼び出すように変更されました。
    • runtime/runtime.hから、古いスタックキャッシュ関連のフィールド(stackinuse, stackcachepos, stackcachecnt, stackcache)がM構造体から削除されました。また、Stktop構造体からmallocedフィールドが削除されました。
    • runtime/stack.cruntime·stackallocruntime·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_AllocStackMHeap_FreeStackが追加。
  • src/pkg/runtime/mheap.c:

    • runtime·MHeap_Alloc関数の変更: g == g->m->g0(Mスタック上)でない場合にmcallを使ってMスタックに切り替えてmheap_allocを呼び出すロジックの追加。
    • mheap_allocmheap_freeという内部ヘルパー関数の導入。
    • runtime·MHeap_AllocStack関数の新規追加: スタック専用のMSpanを割り当てる。
    • runtime·MHeap_FreeStack関数の新規追加: スタック専用のMSpanを解放する。
    • MHeap_AllocSpanLocked関数の変更: MSpanInUseMSpanStackの状態を区別。
    • MHeap_FreeSpanLocked関数の変更: MSpanStack状態のMSpanの解放を処理。
    • runtime·MHeap_SplitSpan関数の削除: スタックの縮小ロジックが変更されたため。
  • src/pkg/runtime/stack.c:

    • stackpoolstackpoolmu(グローバルスタックプール)の導入。
    • runtime·stackinit関数の新規追加: スタックプールの初期化。
    • poolallocpoolfree関数の新規追加: グローバルスタックプールからのスタックセグメントの割り当てと解放。
    • stackcacherefillstackcacherelease関数の大幅な変更: MCacheごとのスタックキャッシュとグローバルスタックプール間の連携ロジック。
    • runtime·stackcache_clear関数の新規追加: MCacheのスタックキャッシュをクリアする。
    • runtime·stackallocruntime·stackfree関数の全面的書き換え: 新しいスタックキャッシュとMHeap_AllocStack/MHeap_FreeStackを利用。
    • copystackshrinkstack関数の変更: スタックのコピーと縮小ロジックの簡素化。
  • src/pkg/runtime/runtime.h:

    • M構造体からの古いスタックキャッシュ関連フィールドの削除。
    • Stktop構造体からのmallocedフィールドの削除。
    • M構造体へのscalarargptrargフィールドの追加(mcallで使用)。
  • src/pkg/runtime/mgc0.c:

    • flushallmcachesruntime·updatememstatsにおけるスタック統計の更新ロジックの変更。
    • runtime·ReadMemStatsにおけるスタック統計の分離。

コアとなるコードの解説

src/pkg/runtime/malloc.hの変更

  • StackCacheSizeNumStackOrders: これらはスタックキャッシュの設計における重要なパラメータです。StackCacheSizeは各Pのスタックキャッシュが保持できるスタックセグメントの合計サイズ(32KB)を示し、NumStackOrdersはキャッシュされるスタックセグメントのサイズクラスの数(3種類)を示します。これにより、Goランタイムは異なるサイズのスタック要求に効率的に対応できます。
  • StackFreeListMCachestackcache: StackFreeListは空きスタックセグメントのリンクリストとその合計サイズを保持します。MCacheにこのstackcache配列が追加されたことで、各Pが自身のローカルなスタックセグメントのキャッシュを持つことができ、スタックの割り当てと解放におけるグローバルロックの競合を大幅に削減します。
  • MSpanStack: MSpanの状態にMSpanStackが追加されたことで、メモリヒープが管理するMSpanが、GC対象のオブジェクトを格納するMSpanInUseと、スタックセグメントを格納するMSpanStackに明確に区別されるようになりました。これにより、GCはMSpanStackのメモリをスキャンする必要がなくなり、GCの効率が向上します。

src/pkg/runtime/mheap.cの変更

  • runtime·MHeap_Allocmcall利用: この変更は、Goランタイムの設計思想における重要な進歩を示しています。runtime·MHeap_Allocはヒープメモリを割り当てるための主要な関数ですが、この関数がGoルーチンのスタック(Gスタック)上で実行中に、スタックの拡張が必要になった場合、そのスタック拡張自体がメモリ割り当てを必要とするため、デッドロックが発生する可能性がありました。これを避けるため、ヒープロックを伴う重要な処理(mheap_alloc)は、g->m->g0(OSスレッドのスタック、Mスタック)に切り替えて実行されるようになりました。runtime·mcallは、現在のGスタックからMスタックに切り替えて指定された関数を実行し、その後Gスタックに戻るためのメカニズムです。これにより、ヒープ割り当てとスタック管理の間の循環的な依存関係が解消されます。
  • runtime·MHeap_AllocStackruntime·MHeap_FreeStack: これらの関数は、スタック専用のメモリ割り当てと解放を担当します。これらはMSpanStack状態のMSpanを操作し、mstats.stacks_inuse(使用中のスタックメモリ量)を更新します。これにより、スタックメモリがGCヒープとは独立して管理されることが保証されます。
  • runtime·MHeap_SplitSpanの削除: 以前は、スタックの縮小時にMSpanを分割してヒープに戻すロジックが存在しましたが、スタックがヒープから分離され、スタックの縮小がcopystackによるコピーベースのメカニズムに一本化されたため、この関数は不要になりました。

src/pkg/runtime/stack.cの変更

  • グローバルスタックプール (stackpool): stackpoolは、異なるサイズの空きスタックセグメントを管理するグローバルなプールです。これは、各Pのローカルキャッシュが枯渇した場合に、スタックセグメントを供給する役割を担います。stackpoolmuというロックで保護されており、複数のPからのアクセスを同期します。
  • poolallocpoolfree: これらの関数は、グローバルスタックプールからスタックセグメントを割り当てたり、プールに返却したりする低レベルな操作を行います。特にpoolallocでは、プールに空きがない場合にruntime·MHeap_AllocStackを呼び出して新しいMSpanを割り当て、それをスタックセグメントに分割してプールに追加するロジックが含まれています。
  • stackcacherefillstackcacherelease: これらの関数は、MCacheごとのスタックキャッシュとグローバルスタックプール間のスタックセグメントの移動を管理します。stackcacherefillは、ローカルキャッシュが不足した場合にグローバルプールからスタックセグメントを取得し、stackcachereleaseは、ローカルキャッシュが過剰になった場合にスタックセグメントをグローバルプールに返却します。これにより、スタックセグメントの再利用が促進され、メモリ割り当てのオーバーヘッドが削減されます。
  • runtime·stackallocruntime·stackfreeの再実装: これらの関数は、Goルーチンのスタックを割り当てたり解放したりする主要なインターフェースです。新しい実装では、まずMCacheごとのローカルキャッシュを試み、不足している場合はグローバルスタックプールから取得します。それでも不足する場合は、runtime·MHeap_AllocStackを呼び出して新しいMSpanを割り当てます。これにより、スタックの割り当てと解放が、ヒープのGCとは独立した、より効率的なパスで行われるようになりました。

これらの変更は、Goランタイムのメモリ管理において、スタックとヒープの役割を明確に分離し、それぞれの特性に合わせた最適化を可能にするための重要なステップです。これにより、ランタイムの複雑性が軽減され、将来的なパフォーマンス改善や機能拡張の余地が広がりました。

関連リンク

参考にした情報源リンク

  • 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)