[インデックス 19640] ファイルの概要
このコミットは、Goランタイムにおけるスタックアロケータの変更を元に戻すものです。具体的には、以前のコミット(CL 104200047 / 318b04f28372)によって導入された、スタックアロケータをmallocgc
(ヒープアロケータ)から分離する試みを巻き戻しています。この分離は、malloc
をGo言語で実装するための前提条件として計画されていましたが、Windows環境およびGoのレース検出器に問題を引き起こしたため、このコミットで元に戻されました。
コミット
commit 3cf83c182af504bcffb82f3fc78a0c8b0ffb3aaa
Author: Keith Randall <khr@golang.org>
Date: Mon Jun 30 19:48:08 2014 -0700
undo CL 104200047 / 318b04f28372
Breaks windows and race detector.
TBR=rsc
««« original CL description
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
»»»
TBR=rsc
CC=golang-codereviews
https://golang.org/cl/101570044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3cf83c182af504bcffb82f3fc78a0c8b0ffb3aaa
元コミット内容
このコミットが元に戻した元の変更(CL 104200047 / 318b04f28372)の目的は以下の通りでした。
- スタックアロケータの分離:
malloc
(メモリ確保関数)をGo言語で実装するために、スタックアロケータを既存のヒープアロケータであるmallocgc
から分離すること。これは、malloc
実行中にスタックが不足した場合に、新しいスタックを確保できなくなる問題を回避するためでした。 FlagNoGC
オブジェクトからのスタックの除外: スタックは、GCヒープ内で最後に残ったFlagNoGC
(ガベージコレクションの対象外)オブジェクトでした。スタックをFlagNoGC
から除外することで、割り当て済み/ブロック境界ビット間の区別をなくし、メモリ管理をより統一的にすることを目指していました。- 関連するIssueの修正: Issue #7468 および #7424 の修正を意図していました。
変更の背景
元のコミット(CL 104200047)は、Goランタイムのメモリ管理を近代化し、将来的にmalloc
をGo言語で実装するための重要なステップでした。しかし、この変更は意図しない副作用をもたらしました。コミットメッセージに明記されている通り、「Windowsとレース検出器を壊した」ため、安定性を優先してこの変更を元に戻す必要が生じました。
具体的には、スタックアロケータをmallocgc
から分離し、スタックをGC対象に含めようとしたことで、以下の問題が発生したと考えられます。
- Windows環境での問題: Goのgoroutineスタックは動的に伸縮しますが、WindowsのOSスレッドスタックは固定サイズです。この間の相互作用、特にC言語ライブラリやWindows APIとの連携において、スタック管理の変更が予期せぬ動作やクラッシュを引き起こした可能性があります。Goの動的スタックとWindowsの固定スタックの間の不整合が、新しいスタックアロケータの設計と衝突したと考えられます。
- レース検出器の問題: Goのレース検出器は、メモリへのアクセスを詳細に計測し、データ競合を検出します。スタックアロケータの変更が、この計測メカニズムと競合したり、スタック上のメモリアクセスの追跡方法に影響を与えたりしたことで、レース検出器が正しく機能しなくなった可能性があります。特に、スタックの成長や新しいスタックの割り当て時に、ライトバリアが正しく適用されない問題(元のCLの目的でもあった「スタックバリアの修正」)が、レース検出器の誤動作につながった可能性も考えられます。
これらの問題は、Goランタイムの安定性とデバッグの容易さに深刻な影響を与えるため、元の変更を一時的に巻き戻し、より堅牢な解決策を模索する判断が下されました。
前提知識の解説
このコミットの変更内容を理解するためには、Goランタイムのメモリ管理、特にスタックとヒープの概念、ガベージコレクション(GC)、そしてスタックの動的な性質に関する知識が不可欠です。
-
Goのメモリ管理(スタックとヒープ)
- スタック (Stack): 関数呼び出しのフレーム、ローカル変数、関数引数、戻りアドレスなどを格納するために使用されるメモリ領域です。Goでは、各goroutineが独自のスタックを持っています。スタックの割り当てと解放はコンパイラによって自動的に行われ、非常に高速です。スタックはLIFO(Last-In, First-Out)の原則に従って動作し、関数が呼び出されるとスタックフレームがプッシュされ、関数から戻るとポップされます。Goのスタックは動的に伸縮する特徴があり、必要に応じて自動的に拡大・縮小します。
- ヒープ (Heap): プログラムの実行中に動的にメモリを割り当てるために使用されるメモリ領域です。コンパイル時にサイズが不明なデータや、関数のスコープを超えて存続する必要があるデータ(例: スライス、マップ、チャネル、構造体のポインタなど)はヒープに割り当てられます。ヒープメモリはGoランタイムのガベージコレクタ(GC)によって管理され、不要になったメモリは自動的に解放されます。スタックに比べて割り当てと解放のオーバーヘッドが大きいです。
- エスケープ解析 (Escape Analysis): Goコンパイラは、変数がスタックに割り当てられるべきか、ヒープに「エスケープ」して割り当てられるべきかを決定するためにエスケープ解析を行います。変数の寿命が関数呼び出しを超えて延長される場合や、そのアドレスが他の場所で使用される場合、その変数はヒープに割り当てられます。
-
Goのガベージコレクション (GC)
- Goは、並行マーク&スイープ方式のガベージコレクタを採用しています。これにより、アプリケーションの実行を長時間停止させることなく(Stop-The-World: STW時間を最小限に抑えながら)、不要なヒープメモリを効率的に回収します。
- ハイブリッドライトバリア (Hybrid Write Barrier): GoのGCは、Yuasaの削除バリアとDijkstraの挿入バリアを組み合わせたハイブリッドライトバリアを使用しています。これは、GCが並行して動作している間にポインタの変更を正確に追跡し、ライブオブジェクトが誤って回収されるのを防ぐための重要なメカニズムです。
FlagNoGC
: Goランタイム内部で、ガベージコレクションの対象外としてマークされる特定のオブジェクトやメモリ領域が存在します。これらは通常、GCのサイクルとは独立して管理されるか、GCのロジックに含めるのが複雑な低レベルのランタイム構造体です。元のCLでは、スタックがこのFlagNoGC
オブジェクトの最後の残りのカテゴリであると述べられていました。
-
Goランタイムのメモリ管理構造体
MHeap
: Goランタイム全体のヒープを管理する構造体です。メモリの割り当て、解放、およびガベージコレクションの主要な調整を行います。MSpan
: 連続したページ(メモリブロック)のまとまりを表す構造体です。ヒープはMSpan
の集合として管理されます。MSpan
は、その状態(例:MSpanInUse
,MSpanFree
)によって、現在使用中か、空き状態か、あるいは特定の目的(元のCLではスタック用)に割り当てられているかを示します。MCache
: 各P(プロセッサ、Goスケジューラにおける論理CPU)に紐付けられたローカルなメモリキャッシュです。小さなオブジェクトの割り当てを高速化するために使用されます。MCentral
: 特定のサイズクラスのMSpan
を管理する中央リストです。MCache
がメモリを使い果たした場合、MCentral
からMSpan
を取得します。
-
Windows環境でのスタック管理の課題
- Goのgoroutineは軽量であり、そのスタックは動的に伸縮します。しかし、GoコードがCgoを介してCコードを呼び出す場合や、直接Windows APIと対話する場合、Goランタイムは固定サイズのOSスレッドスタックに切り替える必要があります。Goの動的なスタック管理とOSの固定スタック管理の間の不一致は、特にWindows環境でスタックオーバーフローやクラッシュの原因となることが歴史的にありました。
-
Goレース検出器
- Goレース検出器は、Goプログラムにおけるデータ競合(複数のgoroutineが同じメモリ位置に同時にアクセスし、少なくとも1つのアクセスが書き込みであり、適切な同期が行われていない場合に発生するバグ)を特定するための強力なツールです。
-race
フラグを付けてビルドすることで有効になります。レース検出器は、メモリアクセスを計測し、追加のデータ構造を維持することで動作するため、プログラムの実行時間とメモリ使用量に大きなオーバーヘッドをもたらします。スタック上のメモリアクセスも監視対象となります。
- Goレース検出器は、Goプログラムにおけるデータ競合(複数のgoroutineが同じメモリ位置に同時にアクセスし、少なくとも1つのアクセスが書き込みであり、適切な同期が行われていない場合に発生するバグ)を特定するための強力なツールです。
技術的詳細
このコミットは、元のCL 104200047によって導入されたスタックアロケータの分離と、それに伴うメモリ管理の変更を完全に元に戻すものです。元のCLは、スタックをGCヒープの一部として扱い、FlagNoGC
オブジェクトから除外することで、メモリ管理の統一性を高めようとしました。しかし、このアプローチはWindows環境でのスタックの挙動とレース検出器の動作に悪影響を与えました。
この「undo」コミットの技術的なポイントは以下の通りです。
- スタックアロケータの独立性の回復: 元のCLは、スタックを
mallocgc
から分離し、独自のスタックアロケータを導入しようとしました。しかし、このundoコミットは、スタックの割り当てを再びヒープの一般的な割り当てメカニズム(ただし、GC対象外の特別な扱い)に戻し、スタック専用のキャッシュメカニズム(stackcache
)を復活させました。これにより、スタックの割り当てと解放が、ヒープのGCサイクルとはある程度独立して行われるようになります。 MSpanStack
状態の削除の巻き戻し: 元のCLでは、MSpan
の状態からMSpanStack
が削除され、スタックもMSpanInUse
として扱われるように変更されていました。このundoコミットでは、MSpanStack
状態の概念自体が削除され、スタック用のMSpan
もMSpanInUse
として扱われるようになります。これは、スタックがGCヒープの通常のオブジェクトとして扱われるという元のCLの意図とは逆の方向性ですが、安定性を優先した結果です。ただし、mheap.c
の変更を見ると、MSpanStack
という状態は完全に削除され、MSpanInUse
が割り当てられたスパンの唯一の状態となっています。これは、スタックがヒープから割り当てられるものの、その管理方法が特殊であることを示唆しています。Stktop
構造体へのmalloced
フラグの再導入: スタックがヒープから割り当てられたものか、それとも特別なスタックアロケータから割り当てられたものかを区別するために、Stktop
構造体にmalloced
ブーリアンフィールドが再導入されました。これは、スタックの解放時に適切な処理(ヒープからの解放か、スタックキャッシュへの返却か)を行うために必要です。- スタックキャッシュの復活:
runtime.h
とstack.c
の変更から、各M(OSスレッド)にローカルなスタックキャッシュ(stackcache
)が復活していることがわかります。これは、頻繁なスタックの割り当てと解放のパフォーマンスを向上させるためのものです。グローバルなスタックキャッシュ(stackcachemu
とstackcache
)も存在し、Mのローカルキャッシュが不足した場合に利用されます。 - GCとの相互作用の簡素化: 元のCLが試みたスタックとGCのより密接な統合は、ライトバリアの複雑さを増し、Windowsやレース検出器で問題を引き起こしました。このundoコミットは、スタックのメモリ管理をGCの主要なパスからある程度切り離すことで、これらの問題を回避しています。スタックは依然としてGCヒープの一部から割り当てられる可能性がありますが、そのライフサイクル管理はより独立したメカニズムによって行われます。
このコミットは、機能追加や最適化よりも、Goランタイムの堅牢性と安定性を維持することを優先した典型的な例と言えます。複雑な低レベルのメモリ管理において、理論的にクリーンな設計が常に実用的な安定性をもたらすとは限らないことを示しています。
コアとなるコードの変更箇所
このコミットは、主にGoランタイムのメモリ管理とスタック管理に関連する複数のファイルにわたる変更を元に戻しています。
src/pkg/runtime/malloc.h
:StackCacheSize
,NumStackOrders
といったスタックキャッシュ関連の定数定義が削除されました。StackFreeList
構造体の定義が削除されました。MCache
構造体からstackcache
フィールドが削除されました。MSpan
の状態定義からMSpanStack
が削除され、MSpanInUse
が割り当て済みスパンの唯一の状態となりました。runtime·MHeap_AllocStack
およびruntime·MHeap_FreeStack
関数のプロトタイプが削除されました。
src/pkg/runtime/mcache.c
:runtime·stackcache_clear(c)
の呼び出しが削除されました。これは、MCacheのスタックキャッシュをクリアする関数でした。
src/pkg/runtime/mcentral.c
:MSpan
がリストにないことを確認するアサーションが削除されました。
src/pkg/runtime/mem.go
:MemStats
構造体内のStackInuse
フィールドのコメントが「bytes used by stack allocator」から「bootstrap stacks」に戻されました。
src/pkg/runtime/mgc0.c
:- GCのマークフェーズにおける
MSpanStack
状態のチェックが削除されました。 runtime·stackcache_clear(c)
の呼び出しが削除されました。runtime·updatememstats
関数におけるmstats.stacks_inuse
の計算ロジックが変更され、mp->stackinuse*FixedStack
の合計が使用されるようになりました。runtime·ReadMemStats
関数から、スタック関連の統計をヒープ統計から分離するロジックが削除されました。
- GCのマークフェーズにおける
src/pkg/runtime/mheap.c
:MHeap_AllocSpanLocked
関数がMHeap_AllocLocked
に統合され、スタック専用の割り当てロジックが削除されました。runtime·MHeap_AllocStack
関数が削除されました。mheap_alloc
関数がruntime·MHeap_Alloc
に統合され、GスタックとMスタックでの呼び出しを区別するロジックが削除されました。MHeap_FreeSpanLocked
関数がMHeap_FreeLocked
に統合され、MSpanStack
状態のチェックが削除されました。runtime·MHeap_FreeStack
関数が削除されました。MSpan
の結合ロジックにおいて、MSpanStack
状態のチェックが削除されました。scavenge
関数およびruntime·MHeap_Scavenger
関数から、mcall
を使用したGスタックからMスタックへの切り替えロジックが削除され、直接Mスタックで実行されるようになりました。runtime·MHeap_SplitSpan
関数が追加されましたが、これは元のCLとは直接関係なく、このコミットで導入された新しい機能です。
src/pkg/runtime/proc.c
:runtime·schedinit
関数からruntime·stackinit()
の呼び出しが削除されました。gfput
関数で、スタックがmalloced
かどうかをチェックするロジックが復活しました。
src/pkg/runtime/runtime.h
:StackCacheSize
,StackCacheBatch
といったスタックキャッシュ関連の定数が再導入されました。M
構造体にstackinuse
,stackcachepos
,stackcachecnt
,stackcache
といったスタックキャッシュ関連のフィールドが再導入されました。M
構造体からscalararg
とptrarg
フィールドが削除されました。Stktop
構造体にmalloced
フィールドが再導入されました。runtime·stackinit
関数のプロトタイプが削除されました。
src/pkg/runtime/stack.c
:- スタックプール(
stackpool
)と関連するロック(stackpoolmu
)の定義が削除されました。 runtime·stackinit
関数が削除されました。poolalloc
、poolfree
といったスタックプールからの割り当て・解放関数が削除されました。stackcacherefill
、stackcacherelease
といったMCacheのスタックキャッシュを操作する関数が、よりシンプルなグローバルキャッシュのロジックに置き換えられました。runtime·stackcache_clear
関数が削除されました。runtime·stackalloc
関数とruntime·stackfree
関数が大幅に変更され、スタックキャッシュとmalloced
フラグを使用する以前のロジックに戻されました。
- スタックプール(
src/pkg/runtime/stack_test.go
:- 元のCLで追加された
TestStackCache
テスト関数が削除されました。
- 元のCLで追加された
コアとなるコードの解説
このコミットの核心は、Goランタイムのスタック管理を、元のCLが導入しようとした「ヒープとの統合」から、以前の「独立したキャッシュベースのアロケータ」に戻すことです。
-
スタックキャッシュの復活と役割分担:
- 元のCLは、スタックをヒープの一般的な
MSpanInUse
として扱い、mallocgc
と統合しようとしました。しかし、このコミットでは、runtime.h
とstack.c
でStackCacheSize
やStackCacheBatch
といった定数、そしてM
構造体内のstackcache
関連フィールドが復活しています。これは、各M(OSスレッド)が小さなスタックセグメントのローカルキャッシュを持つことを意味します。 stack.c
のruntime·stackalloc
とruntime·stackfree
関数は、このローカルキャッシュとグローバルなスタックキャッシュ(stackcache
とstackcachemu
)を利用するように変更されました。これにより、頻繁に割り当て・解放される小さなスタックは、高速なキャッシュから供給され、ヒープアロケータのオーバーヘッドを回避します。FixedStack
サイズのスタックはキャッシュから供給され、それより大きなスタックはmallocgc
から割り当てられるという、以前のハイブリッドなアプローチに戻りました。
- 元のCLは、スタックをヒープの一般的な
-
MSpanStack
状態の削除とmalloced
フラグの重要性:- 元のCLは
MSpanStack
状態を削除し、スタックもMSpanInUse
として扱おうとしました。このundoコミットでもMSpanStack
状態は削除されたままですが、Stktop
構造体にmalloced
ブーリアンフィールドが再導入されました。 - この
malloced
フラグは、スタックがFixedStack
サイズでスタックキャッシュから割り当てられたものか(malloced
がfalse
)、それともmallocgc
(ヒープ)から割り当てられたものか(malloced
がtrue
)を区別するために使用されます。 runtime·stackfree
関数では、このmalloced
フラグに基づいて、スタックをスタックキャッシュに戻すか、runtime·free
(ヒープ解放)を呼び出すかを決定します。これにより、スタックの割り当て元に応じた適切な解放処理が保証されます。
- 元のCLは
-
mheap.c
とmgc0.c
の変更:mheap.c
では、スタック専用の割り当て・解放関数(MHeap_AllocStack
,MHeap_FreeStack
)が削除され、一般的なMHeap_Alloc
とMHeap_Free
が使用されるようになりました。これは、スタックがヒープから割り当てられるという事実を反映していますが、その管理はstack.c
のロジックによって特殊化されます。mgc0.c
では、GCの統計更新やマークフェーズからスタックキャッシュ関連のロジックが削除され、GCがスタックの内部管理に直接関与しない以前の状態に戻りました。mstats.stacks_inuse
の計算も、Mごとのstackinuse
フィールド(これはキャッシュ内のスタック数を追跡)に基づいて行われるようになりました。
-
テストの巻き戻し:
stack_test.go
からTestStackCache
が削除されたことは、元のCLが導入したスタックキャッシュの新しいテストが不要になったことを示しています。これは、その機能が完全に元に戻されたことの明確な証拠です。
総じて、このコミットは、Goランタイムのスタック管理を、よりシンプルで実績のあるメカニズムに戻すことで、Windows環境での互換性問題とレース検出器の誤動作を解決しました。これは、パフォーマンスの最適化や設計の統一性よりも、安定性と堅牢性を優先した結果であり、Goランタイム開発における実用主義的なアプローチを反映しています。
関連リンク
- 元コミット (CL 104200047):
- https://golang.org/cl/104200047
- GitHub上でのコミットハッシュ:
318b04f28372
(このコミットが元に戻した変更)
参考にした情報源リンク
- Goのメモリ管理(スタックとヒープ)に関する記事:
- Goのガベージコレクションに関する情報:
- Goのレース検出器に関する情報:
- GoランタイムのWindowsスタックに関する問題:
- https://github.com/golang/go/issues/18138 (関連する可能性のあるIssueの一例)
- Goの
FlagNoGC
に関する議論(一般的なGC無効化の文脈で言及されることが多い): - GoのCL 104200047に関する情報:
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEHPC54Lp50PivNUpszw7VH1duClDTT_ECSUdBRO96OckSiQrE4j4pzzRge8gOto1qMc6CnMUaBmhLMiwE31W12paV0NhgagYzMcgtoq_3En5sZQ_-I-DBzqDDKvN7JLSVFwsEHFh-IjRXKLEVbgG5Lw4IQ1Oyy2LTHu7c=
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEdklemNN8CtBHeTE-y3JLVo0ItZ-rbrNpwGHerWljEbOC8LmTobmvDW6tgLCAAPJXVkaEBKeHO63DSYTYkE6nUIcATbYr7aeXEvpiHZRnyj-rMuRzxq54DLDsP59Qm2rEfiMjuf9TY2TSLVkrO-QdGMd5-8UiNrNqiSlRjiMtGOTvc5Ibwd0yop6TqGDBU3vhL0RIFUuI4-iiBjj2WDGF3EMiRAcP-HV-nEA==
- https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQF8uqbS3JmYuDXCwh5xZYpdHUUvU5IEKy0sqcHCYtQL5_TKQtJQ5XGrXglzx6e2rk3C-fMv8sKe01bYBnBUB8NpPnJXk7aWCFtYQIgwsDmcQ3VDZUKUwxmyNnyjUp0P3s4iivNX