[インデックス 19402] ファイルの概要
このコミットは、Goランタイムにおけるデフォルトのスタックサイズを4KBから8KBに再度変更するものです。Go 1.2でスタックサイズが8KBに増加された後、Go 1.3で導入された連続スタックモデルとスタックコピーの仕組みにより4KBに戻されましたが、特定の条件下(特にreflect
パッケージの使用時)でスタックコピーが完全に機能しないケースがあり、深刻なパフォーマンス低下が発生したため、一時的に8KBに戻す決定がなされました。
コミット
commit 6aee29648fce3af20507787035ae22d06d75d39b
Author: Russ Cox <rsc@golang.org>
Date: Tue May 20 00:30:46 2014 -0400
runtime: switch default stack size back to 8kB
The move from 4kB to 8kB in Go 1.2 was to eliminate many stack split hot spots.
The move back to 4kB was predicated on copying stacks eliminating
the potential for hot spots.
Unfortunately, the fact that stacks do not copy 100% of the time means
that hot spots can still happen under the right conditions, and the slowdown
is worse now than it was in Go 1.2. There is a real program in issue 8030 that
sees about a 30x slowdown: it has a reflect call near the top of the stack
which inhibits any stack copying on that segment.
Go back to 8kB until stack copying can be used 100% of the time.
Fixes #8030.
LGTM=khr, dave, iant
R=iant, khr, r, bradfitz, dave
CC=golang-codereviews
https://golang.org/cl/92540043
---
doc/go1.3.html | 7 -------
src/pkg/runtime/stack.h | 2 +-\n 2 files changed, 1 insertion(+), 8 deletions(-)
diff --git a/doc/go1.3.html b/doc/go1.3.html
index fa9e3f7784..5404f4ec66 100644
--- a/doc/go1.3.html
+++ b/doc/go1.3.html
@@ -118,13 +118,6 @@ Details including performance numbers are in this
<a href="http://golang.org/s/contigstacks">design document</a>.
</p>
-<h3 id="stack_size">Stack size</h3>
-
-<p>
-Go 1.2 increased the minimum stack size to 8 kilobytes; with the new stack model, it has been
-put back to 4 kilobytes.
-</p>
-
<h3 id="garbage_collector">Changes to the garbage collector</h3>
<p>
diff --git a/src/pkg/runtime/stack.h b/src/pkg/runtime/stack.h
index a3a5d83a64..18ab30b69b 100644
--- a/src/pkg/runtime/stack.h
+++ b/src/pkg/runtime/stack.h
@@ -76,7 +76,7 @@ enum {
// The minimum stack segment size to allocate.
// If the amount needed for the splitting frame + StackExtra
// is less than this number, the stack will have this size instead.
-\tStackMin = 4096,\n+\tStackMin = 8192,\n \tStackSystemRounded = StackSystem + (-StackSystem & (StackMin-1)),
\tFixedStack = StackMin + StackSystemRounded,
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6aee29648fce3af20507787035ae22d06d75d39b
元コミット内容
このコミットは、Goランタイムのデフォルトスタックサイズを8KBに戻すものです。Go 1.2でスタック分割のホットスポットを解消するために4KBから8KBに増やされましたが、スタックコピーによってホットスポットが解消されるという前提で4KBに戻されました。しかし、スタックコピーが常に100%行われるわけではないため、特定の条件下でホットスポットが再発し、Go 1.2よりもパフォーマンスが著しく低下する問題が発生しました。特に、issue 8030で報告された実際のプログラムでは、スタックの最上位付近にあるreflect
呼び出しがスタックコピーを阻害し、約30倍の速度低下が見られました。この問題に対処するため、スタックコピーが100%信頼できるようになるまで、一時的にスタックサイズを8KBに戻すことが決定されました。
変更の背景
Goのスタック管理は、パフォーマンスとメモリ効率のバランスを取る上で重要な要素です。このコミットの背景には、Goランタイムにおけるスタックサイズの変遷と、新しいスタックモデルの導入に伴う課題があります。
-
Go 1.2でのスタックサイズ増加 (4KB -> 8KB): Go 1.2以前のGoランタイムでは、ゴルーチンのスタックはデフォルトで4KBの最小サイズで開始されました。しかし、関数呼び出しが頻繁に行われ、スタックが頻繁に拡張・縮小を繰り返すようなワークロードでは、「スタック分割のホットスポット (stack split hot spots)」と呼ばれるパフォーマンス上の問題が発生していました。これは、スタックが小さすぎると、頻繁に新しいスタックセグメントを割り当てたり、既存のセグメントを解放したりする必要が生じ、これがオーバーヘッドとなっていたためです。この問題を緩和するため、Go 1.2ではデフォルトの最小スタックサイズが8KBに増加されました。これにより、多くのアプリケーションでスタック分割の頻度が減少し、パフォーマンスが改善されました。
-
Go 1.3での連続スタックモデルとスタックコピーの導入、そしてスタックサイズ削減 (8KB -> 4KB): Go 1.3では、Goランタイムのスタック管理に大きな変更が加えられ、「連続スタック (contiguous stacks)」モデルが導入されました。以前の「セグメントスタック (segmented stacks)」モデルでは、スタックは複数の不連続なメモリセグメントで構成されていましたが、連続スタックモデルでは、スタックは単一の連続したメモリブロックとして扱われます。スタックが現在の割り当てサイズを超えて成長する必要がある場合、ランタイムはより大きな連続したメモリブロックを割り当て、古いスタックの内容を新しいブロックにコピーします(スタックコピー)。この新しいモデルは、セグメントスタックで問題となっていた「ホットスプリット問題」を根本的に解決し、スタックの成長と縮小のオーバーヘッドを削減することを目的としていました。この改善により、Goチームはデフォルトの最小スタックサイズを再び4KBに戻すことが可能であると判断しました。これは、メモリ使用量を削減し、より多くのゴルーチンを効率的に実行できるようにするためです。
-
スタックコピーの不完全性とパフォーマンス回帰: しかし、Go 1.3の連続スタックモデルとスタックコピーの導入は、まだ初期段階であり、特定のコーナーケースで完全に機能しないことが判明しました。このコミットのメッセージが示すように、「スタックが常に100%コピーされるわけではない」という問題がありました。特に、
reflect
パッケージを使用した関数呼び出しがスタックの最上位付近にある場合、スタックコピーが阻害されることが確認されました。これは、reflect
がランタイムの内部構造に深く関与し、スタックフレームのポインタ情報などを正確に追跡する必要があるため、スタックコピーのメカニズムと競合する可能性があったためと考えられます。この問題により、スタックコピーが期待通りに行われず、結果としてスタックの頻繁な再割り当てや移動が発生し、Go 1.2で8KBに増やした時よりもさらに悪いパフォーマンス低下(issue 8030では30倍の速度低下)を引き起こしました。 -
一時的な8KBへの回帰: この深刻なパフォーマンス回帰に対処するため、Goチームは、スタックコピーのメカニズムが完全に堅牢になり、すべてのケースで100%信頼できるようになるまで、デフォルトの最小スタックサイズを一時的に8KBに戻すことを決定しました。これは、安定性とパフォーマンスを優先するための実用的な判断でした。
このコミットは、Goランタイムの進化の過程で、理論的な最適化と実際の運用における課題との間でバランスを取る必要性を示しています。
前提知識の解説
このコミットを理解するためには、Go言語のランタイムにおけるスタック管理、特にゴルーチンのスタック、スタックの成長と縮小、そしてGo 1.3で導入された連続スタックモデルとスタックコピーの概念について理解しておく必要があります。
-
ゴルーチンのスタック (Goroutine Stacks): Go言語のゴルーチンは、軽量な並行処理の単位であり、それぞれが独立した実行スタックを持っています。このスタックは、関数呼び出しの引数、ローカル変数、戻りアドレスなどを格納するために使用されます。Goのスタックは、必要に応じて動的に成長・縮小する特性を持っています。
-
スタックの成長と縮小: Goの関数が呼び出されると、その関数のスタックフレームがスタックにプッシュされます。スタックが現在の割り当てサイズを超えてしまう場合、ランタイムはスタックを拡張する必要があります。逆に、関数から戻るとスタックフレームがポップされ、スタックの使用量が減ると、ランタイムはスタックを縮小してメモリを解放することがあります。
-
セグメントスタック (Segmented Stacks) - Go 1.3以前: Go 1.3より前のバージョンでは、「セグメントスタック」というモデルが採用されていました。このモデルでは、ゴルーチンのスタックは複数の小さなメモリセグメントに分割され、必要に応じて新しいセグメントが割り当てられ、既存のセグメントにリンクされていました。
- 利点: 初期スタックサイズを非常に小さく(例えば4KB)設定できるため、多数のゴルーチンを起動してもメモリ消費を抑えることができました。
- 欠点: スタックがセグメント境界を頻繁にまたいで成長・縮小する場合、「ホットスプリット問題 (hot split problem)」と呼ばれるパフォーマンス上の問題が発生しました。これは、頻繁なセグメントの割り当てと解放、そしてセグメント間のポインタの更新がオーバーヘッドとなっていたためです。
-
連続スタック (Contiguous Stacks) とスタックコピー (Stack Copying) - Go 1.3以降: Go 1.3で導入された新しいスタックモデルは「連続スタック」です。このモデルでは、ゴルーチンのスタックは単一の連続したメモリブロックとして扱われます。
- スタックコピーのメカニズム: スタックが現在の割り当てサイズを超えて成長する必要がある場合、ランタイムは以下の手順を実行します。
- 現在のスタックサイズの約2倍の、より大きな連続したメモリブロックをヒープ上に割り当てます。
- 古いスタックの内容(関数呼び出しの引数、ローカル変数、戻りアドレス、そして重要なのはポインタ)を、新しい大きなブロックにすべてコピーします。
- コピーが完了した後、古いスタックメモリは解放されます。
- スタック上のポインタは、新しいメモリ位置を指すように更新されます。
- 利点: ホットスプリット問題が解消され、スタックの成長・縮小がより効率的になります。また、スタック上のポインタの扱いが単純化されます。
- 課題: スタック全体をコピーする操作は、スタックサイズが大きい場合や頻繁に発生する場合にオーバーヘッドとなる可能性があります。また、スタック上のポインタを正確に追跡し、コピー後に適切に更新することが不可欠です。
- スタックコピーのメカニズム: スタックが現在の割り当てサイズを超えて成長する必要がある場合、ランタイムは以下の手順を実行します。
-
reflect
パッケージとスタックコピーの阻害:reflect
パッケージは、Goプログラムが実行時に自身の型情報を検査したり、値の操作を行ったりするための機能を提供します。reflect
は、コンパイル時には不明な型や構造を扱うため、ランタイムの内部構造、特にスタックフレームやポインタの扱いに深く関与します。 このコミットで言及されている問題は、reflect
呼び出しがスタックの最上位付近にある場合、スタックコピーのプロセスを阻害する可能性があるというものです。これは、reflect
がスタック上の特定のポインタやデータ構造に依存しているため、スタックがコピーされてメモリ上の位置が変わると、その依存関係が壊れてしまう、あるいはランタイムが安全にコピーできないと判断するような状況が発生したためと考えられます。結果として、スタックコピーが回避され、代わりにセグメントスタック時代のような非効率なスタック拡張メカニズムがフォールバックとして使用され、パフォーマンスが著しく低下しました。
これらの背景知識を理解することで、このコミットがGoランタイムの安定性とパフォーマンスを確保するための重要な修正であることがわかります。
技術的詳細
このコミットの技術的詳細は、Goランタイムのスタック管理におけるStackMin
定数の役割と、スタックコピーのメカニズムが特定の条件下で期待通りに機能しないという問題に集約されます。
-
StackMin
定数:src/pkg/runtime/stack.h
ファイルにあるStackMin
定数は、Goゴルーチンに割り当てられる最小スタックサイズを定義しています。- Go 1.2では、この値が4096バイト(4KB)から8192バイト(8KB)に増加されました。これは、セグメントスタックモデルにおける頻繁なスタック分割によるパフォーマンス低下(ホットスポット)を緩和するためです。
- Go 1.3の連続スタックモデル導入後、スタックコピーによってホットスポットが解消されるという前提で、この値は再び4096バイト(4KB)に戻されました。これは、メモリ使用量を削減し、より多くのゴルーチンを効率的に実行できるようにするためです。
- このコミットでは、
StackMin
が再び8192バイト(8KB)に戻されています。これは、スタックコピーが常に100%機能するわけではないという現実的な問題に直面したためです。
-
スタックコピーの不完全性: Go 1.3で導入された連続スタックモデルは、スタックコピーによってスタックの成長を効率的に処理することを意図していました。しかし、このコミットが指摘するように、「スタックが常に100%コピーされるわけではない」という問題がありました。
reflect
呼び出しによる阻害: コミットメッセージは、特に「スタックの最上位付近にあるreflect
呼び出しが、そのセグメントでのスタックコピーを阻害する」と明記しています。reflect
パッケージは、実行時に型情報を操作するため、スタック上のポインタやデータ構造に非常に敏感です。スタックコピーは、スタック上のすべてのポインタを新しいメモリ位置に正確に更新する必要がありますが、reflect
が関与する複雑なスタックフレームでは、このポインタ更新のロジックが完全に機能しない、あるいはランタイムが安全にコピーできないと判断するようなエッジケースが存在したと考えられます。- パフォーマンス回帰: スタックコピーが阻害されると、ランタイムはスタックを拡張するために、よりコストのかかる代替手段(例えば、セグメントスタック時代のような、新しいセグメントの割り当てとリンク、あるいはより頻繁なGCトリガーなど)に頼らざるを得なくなります。これにより、スタックの成長が非効率になり、結果として深刻なパフォーマンス低下を引き起こしました。issue 8030で報告された30倍の速度低下は、この問題の深刻さを示しています。
-
8KBへの回帰の理由: このコミットは、スタックコピーのメカニズムが完全に堅牢になるまでの「一時的な措置」として、デフォルトスタックサイズを8KBに戻しています。
- ホットスポットの緩和: 8KBのスタックサイズは、4KBの場合よりもスタックがオーバーフローする頻度を減らします。これにより、スタックコピーが阻害されるケースであっても、スタックの拡張操作自体の発生頻度を減らし、結果としてパフォーマンス低下の影響を緩和することができます。
- 安定性の確保: 完全にデバッグされ、堅牢なスタックコピーメカニズムが実装されるまでの間、既知のパフォーマンス問題を回避し、Goプログラムの安定した動作を保証するための措置です。
この変更は、Goランタイム開発における「理論的な最適化」と「実世界の複雑性」の間のトレードオフを示しています。新しいスタックモデルは長期的なパフォーマンス向上を目指すものでしたが、その初期実装にはまだ課題があり、ユーザー体験を損なわないために一時的な後退が必要とされたのです。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、以下の2つのファイルに集中しています。
-
src/pkg/runtime/stack.h
: Goランタイムのスタック関連の定数が定義されているヘッダーファイルです。--- a/src/pkg/runtime/stack.h +++ b/src/pkg/runtime/stack.h @@ -76,7 +76,7 @@ enum { // The minimum stack segment size to allocate. // If the amount needed for the splitting frame + StackExtra // is less than this number, the stack will have this size instead. - StackMin = 4096, + StackMin = 8192, StackSystemRounded = StackSystem + (-StackSystem & (StackMin-1)), FixedStack = StackMin + StackSystemRounded,
この変更は、
StackMin
という定数の値を4096
(4KB)から8192
(8KB)に変更しています。これは、ゴルーチンに割り当てられる最小スタックサイズを直接的に制御するものです。 -
doc/go1.3.html
: Go 1.3のリリースノートまたはドキュメントの一部です。--- a/doc/go1.3.html +++ b/doc/go1.3.html @@ -118,13 +118,6 @@ Details including performance numbers are in this <a href="http://golang.org/s/contigstacks">design document</a>. </p> -<h3 id="stack_size">Stack size</h3> - -<p> -Go 1.2 increased the minimum stack size to 8 kilobytes; with the new stack model, it has been -put back to 4 kilobytes. -</p> - <h3 id="garbage_collector">Changes to the garbage collector</h3> <p>
この変更は、Go 1.3のドキュメントから「Stack size」に関するセクションを削除しています。このセクションでは、Go 1.2でスタックサイズが8KBに増加し、新しいスタックモデル(連続スタック)で4KBに戻されたことが記述されていました。今回のコミットで再び8KBに戻されるため、この記述が現状と合わなくなるため削除されました。
コアとなるコードの解説
src/pkg/runtime/stack.h
におけるStackMin
の変更は、Goランタイムのスタック管理において非常に直接的かつ重要な意味を持ちます。
StackMin
の役割:StackMin
は、新しいゴルーチンが起動される際に最初に割り当てられるスタックの最小サイズを決定します。また、スタックが成長する必要がある場合でも、この最小サイズを下回ることはありません。- 4KBから8KBへの変更の意図:
- パフォーマンスの安定化: 前述の通り、Go 1.3で導入された連続スタックモデルとスタックコピーは、特定の条件下(特に
reflect
の使用時)で完全に機能せず、スタックコピーが阻害される問題がありました。これにより、スタックの成長が非効率になり、深刻なパフォーマンス低下が発生していました。 - スタックオーバーフローの頻度低減: 最小スタックサイズを8KBに増やすことで、スタックが頻繁に拡張操作を必要とする可能性が低くなります。これにより、スタックコピーが阻害されるような状況に陥る頻度自体が減少し、結果としてパフォーマンス低下の影響を緩和することができます。
- 一時的な解決策: この変更は、スタックコピーのメカニズムが完全にデバッグされ、すべてのエッジケースで堅牢に機能するようになるまでの「一時的な回避策」として導入されました。Goチームは、スタックコピーが100%信頼できるようになれば、再び4KBに戻すことを視野に入れていたと考えられます。
- パフォーマンスの安定化: 前述の通り、Go 1.3で導入された連続スタックモデルとスタックコピーは、特定の条件下(特に
doc/go1.3.html
からのスタックサイズに関する記述の削除は、ドキュメントと実際のランタイムの挙動との整合性を保つためのものです。Go 1.3のリリース時点では4KBに戻す予定でしたが、このコミットによって8KBに戻されたため、古い記述は誤解を招く可能性があったため削除されました。
このコミットは、Goランタイムのパフォーマンスと安定性を確保するために、理論的な設計と実運用上の課題の間でバランスを取る必要性を示しています。
関連リンク
- Go CL (Code Review) リンク: https://golang.org/cl/92540043
- Go Issue 8030: このコミットが修正した問題のトラッキング。Goの公式Issue Trackerで詳細を確認できます。
参考にした情報源リンク
- Go 1.2 Release Notes (Stack size): Go 1.2でのスタックサイズ変更に関する公式情報。
- Go 1.3 Design Document (Contiguous Stacks): Go 1.3で導入された連続スタックモデルに関する設計ドキュメント。
- https://golang.org/s/contigstacks (コミットメッセージにも記載されているリンク)
- Go 1.3 Release Notes (Runtime): Go 1.3でのランタイム変更に関する公式情報。
- Goのreflectパッケージのパフォーマンスに関する議論:
reflect
がパフォーマンスに与える影響に関する一般的な情報源。 - Goのスタック管理に関するブログ記事や解説: Goのスタックモデルの変遷や技術的詳細を解説している記事。