[インデックス 18916] ファイルの概要
このコミットは、Go言語の公式FAQドキュメントである doc/go_faq.html
の内容を更新するものです。具体的には、Goランタイムにおけるスタック管理の記述を現代のGoの挙動に合わせて修正し、ゴルーチンとOSスレッドの比較をより正確にするための変更が含まれています。
コミット
このコミットは、Goランタイムのスタック管理に関するFAQの記述を更新し、特にgc
コンパイラがセグメント化されたスタックを使用しなくなったことを反映しています。また、ゴルーチンとスレッドの比較記述も改善されています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/287967f74c9d937b1075a648be5fd9247283cef6
元コミット内容
commit 287967f74c9d937b1075a648be5fd9247283cef6
Author: Rob Pike <r@golang.org>
Date: Fri Mar 21 13:59:30 2014 +1100
doc/go_faq.html: update description of stack management
They aren't segmented any more, at least with gc.
Also improve the comparison of goroutines and threads.
Fixes #7373.
LGTM=iant
R=iant
CC=golang-codereviews
https://golang.org/cl/77950044
---
doc/go_faq.html | 23 +++++++++++++----------
1 file changed, 13 insertions(+), 10 deletions(-)
diff --git a/doc/go_faq.html b/doc/go_faq.html
index fb2d929bd6..9606213b1f 100644
--- a/doc/go_faq.html
+++ b/doc/go_faq.html
@@ -426,18 +426,20 @@ When a coroutine blocks, such as by calling a blocking system call,
the run-time automatically moves other coroutines on the same operating
system thread to a different, runnable thread so they won't be blocked.
The programmer sees none of this, which is the point.
-The result, which we call goroutines, can be very cheap: unless they spend a lot of time
-in long-running system calls, they cost little more than the memory
-for the stack, which is just a few kilobytes.
+The result, which we call goroutines, can be very cheap: they have little
+overhead beyond the memory for the stack, which is just a few kilobytes.
</p>
<p>
-To make the stacks small, Go's run-time uses segmented stacks. A newly
+To make the stacks small, Go's run-time uses resizable, bounded stacks. A newly
minted goroutine is given a few kilobytes, which is almost always enough.
-When it isn't, the run-time allocates (and frees) extension segments automatically.
-The overhead averages about three cheap instructions per function call.
+When it isn't, the run-time grows (and shrinks) the memory for storing
+the stack automatically, allowing many goroutines to live in a modest
+amount of memory.
+The CPU overhead averages about three cheap instructions per function call.
It is practical to create hundreds of thousands of goroutines in the same
-address space. If goroutines were just threads, system resources would
+address space.
+If goroutines were just threads, system resources would
run out at a much smaller number.
</p>
@@ -1614,9 +1616,10 @@ it now. <code>Gccgo</code>'s run-time support uses <code>glibc</code>.
<code>Gc</code> uses a custom library to keep the footprint under
control; it is
compiled with a version of the Plan 9 C compiler that supports
-segmented stacks for goroutines.
-The <code>gccgo</code> compiler implements segmented
-stacks on Linux only, supported by recent modifications to the gold linker.
+resizable stacks for goroutines.
+The <code>gccgo</code> compiler implements these on Linux only,
+using a technique called segmented stacks,
+supported by recent modifications to the gold linker.
</p>
<h3 id="Why_is_my_trivial_program_such_a_large_binary">
変更の背景
このコミットの主な背景は、Go言語のランタイムにおけるスタック管理の実装が進化し、特に公式コンパイラであるgc
が「セグメント化されたスタック (segmented stacks)」から「リサイズ可能な、境界のあるスタック (resizable, bounded stacks)」へと移行したことを、公式FAQドキュメントに正確に反映させる必要があったためです。
Goの初期のバージョンでは、ゴルーチンのスタックはセグメント化されたスタックとして実装されていました。これは、スタックが足りなくなったときに新しいメモリセグメントを割り当てて既存のスタックに連結することで、スタックを動的に拡張する仕組みです。これにより、初期スタックサイズを非常に小さく保ちつつ、必要に応じて拡張できるという利点がありました。しかし、この方式にはいくつかの欠点も存在しました。例えば、スタックの拡張や縮小時に複数のセグメントを管理するためのオーバーヘッドや、スタックフレームがセグメント境界をまたぐ場合の複雑性、そしてガベージコレクションとの連携における課題などです。
Goチームは、これらの課題を解決し、より効率的で予測可能なスタック管理を実現するために、セグメント化されたスタックから、より現代的な「コピーによるスタック拡張 (stack copying)」アプローチへと移行しました。このアプローチでは、スタックが足りなくなった場合、より大きな連続したメモリ領域を割り当て、既存のスタックの内容を新しい領域にコピーします。これにより、スタックの管理が単純化され、パフォーマンスが向上し、ガベージコレクションとの統合も容易になりました。
この変更は、Go 1.2で導入された重要なランタイムの改善点の一つであり、このコミットはその変更をFAQに反映させるためのものです。また、ゴルーチンとOSスレッドの比較についても、より正確で誤解を招かない表現に修正されています。
この変更は、GoのIssue #7373 (doc/go_faq.html: update stack description
) に対応するものです。
前提知識の解説
ゴルーチン (Goroutines)
Go言語におけるゴルーチンは、軽量な並行実行単位です。OSのスレッドとは異なり、Goランタイムによって管理され、非常に少ないメモリ(数KB)で起動できます。Goランタイムは、少数のOSスレッド上で多数のゴルーチンを多重化(マルチプレックス)して実行します。これにより、開発者は複雑なスレッド管理を意識することなく、簡単に並行処理を記述できます。ゴルーチンがブロックされる(例:ネットワークI/O待ち)と、Goランタイムは自動的に同じOSスレッド上の他のゴルーチンを別の実行可能なOSスレッドに移動させ、プログラム全体の実行を継続させます。
スタック管理 (Stack Management)
プログラムが関数を呼び出す際、その関数のローカル変数や引数、戻りアドレスなどを格納するために「スタック」と呼ばれるメモリ領域が使用されます。スタックはLIFO(後入れ先出し)のデータ構造で、関数呼び出しごとにスタックフレームが積まれ、関数が終了するとそのスタックフレームが取り除かれます。スタックのサイズは、プログラムの実行中に動的に変化する可能性があります。
セグメント化されたスタック (Segmented Stacks)
セグメント化されたスタックは、Goの初期バージョンで採用されていたスタック管理手法の一つです。この方式では、スタックは連続した単一のメモリブロックではなく、必要に応じて小さなメモリセグメントを動的に割り当て、それらを連結することでスタックを拡張します。
- 利点:
- 小さな初期スタックサイズ: 新しいゴルーチンは非常に小さなスタック(数KB)で起動できるため、多数のゴルーチンを生成してもメモリ消費を抑えられます。
- 動的な拡張: スタックが足りなくなった場合、新しいセグメントを割り当てることで自動的に拡張されます。
- 欠点:
- オーバーヘッド: スタックの拡張や縮小時にセグメントの割り当て・解放、およびセグメント間の切り替え(スタックポインタの調整など)にオーバーヘッドが発生します。
- 複雑性: スタックフレームがセグメント境界をまたぐ場合、ポインタの調整やガベージコレクションの処理が複雑になる可能性があります。
- パフォーマンスの変動: スタックの拡張が頻繁に発生すると、パフォーマンスが不安定になることがあります。
リサイズ可能な、境界のあるスタック (Resizable, Bounded Stacks) / コピーによるスタック拡張 (Stack Copying)
Go 1.2以降のgc
コンパイラで採用されているスタック管理手法です。この方式では、スタックは連続したメモリブロックとして扱われます。
- 仕組み:
- 新しいゴルーチンは、数KBの小さな連続したスタック領域で起動します。
- スタックが足りなくなりそうになると(スタックガードページに到達するなど)、Goランタイムはより大きな連続したメモリ領域をヒープ上に割り当てます。
- 既存のスタックの内容(スタックフレーム)を、新しい大きな領域にコピーします。
- 古いスタック領域は解放されます。
- スタックが過剰に大きくなった場合、Goランタイムはスタックをより小さな領域に縮小するために同様のコピー操作を行うこともあります。
- 利点:
- 単純化: スタックが常に連続したメモリブロックであるため、スタックポインタの管理やガベージコレクションが単純になります。
- パフォーマンスの安定性: スタックの拡張はコピー操作を伴いますが、セグメント切り替えのオーバーヘッドがなく、全体として予測可能なパフォーマンスを提供します。
- キャッシュ効率: 連続したメモリ領域であるため、CPUキャッシュの利用効率が向上する可能性があります。
- 欠点:
- コピーのオーバーヘッド: スタックの拡張時には、スタック全体をコピーするコストが発生します。ただし、Goランタイムはこのコピー操作を非常に効率的に行うように設計されており、通常は問題になりません。
Goのgc
コンパイラとgccgo
コンパイラ
gc
(Go Compiler): Go言語の公式かつ主要なコンパイラです。Goチームによって開発・メンテナンスされており、Goの最新の機能やランタイムの最適化が最も早く反映されます。このコミットで言及されているスタック管理の変更は、主にこのgc
コンパイラに適用されます。gccgo
: GCC(GNU Compiler Collection)のフロントエンドとして実装されたGoコンパイラです。gc
とは異なるバックエンドを使用しており、GCCの最適化パスを利用できます。gccgo
は、gc
とは異なるスタック管理の実装(このコミットの時点ではLinux上でセグメント化されたスタックを使用)を持つことがあります。
技術的詳細
このコミットは、Goランタイムのスタック管理戦略の重要な進化を反映しています。Goの初期設計では、ゴルーチンの軽量性を実現するために「セグメント化されたスタック」が採用されていました。これは、スタックが固定サイズではなく、必要に応じて小さなメモリセグメントを動的に追加・削除することで、スタックを拡張・縮小するメカニズムです。これにより、ゴルーチンは非常に小さな初期スタック(数KB)で起動でき、多数のゴルーチンを生成してもメモリフットプリントを抑えることができました。
しかし、セグメント化されたスタックにはいくつかの課題がありました。
- スタック切り替えのオーバーヘッド: 関数呼び出しや戻り時に、スタックポインタがセグメント境界をまたぐ場合、追加のチェックとセグメント切り替えロジックが必要となり、わずかながらオーバーヘッドが発生しました。コミットメッセージの「The overhead averages about three cheap instructions per function call.」という記述は、このセグメント切り替えのコストを指していました。
- ガベージコレクションの複雑性: スタックが複数の非連続なセグメントに分散しているため、ガベージコレクタがスタック上のポインタを正確にスキャンし、参照されているオブジェクトを特定する作業が複雑になります。
- スタックのフラグメンテーション: セグメントの動的な割り当てと解放は、メモリのフラグメンテーションを引き起こす可能性がありました。
これらの課題に対処するため、Go 1.2ではgc
コンパイラにおいてスタック管理が大幅に改善され、「コピーによるスタック拡張(またはリサイズ可能な、境界のあるスタック)」というアプローチが導入されました。この新しいアプローチでは、ゴルーチンのスタックは常に連続したメモリブロックとして扱われます。
- スタック拡張のプロセス:
- ゴルーチンは、初期の小さなスタック(通常は2KB)で開始します。
- 関数呼び出しによってスタックが拡張され、現在のスタック領域の終端(スタックガードページ)に近づくと、Goランタイムはスタックオーバーフローを検知します。
- ランタイムは、現在のスタックサイズよりも大きな新しい連続したメモリブロックをヒープ上に割り当てます。
- 既存のスタックの内容(スタックフレーム、ローカル変数、戻りアドレスなど)が、新しい大きなブロックに効率的にコピーされます。
- コピーが完了すると、古いスタックブロックは解放され、ゴルーチンは新しいスタックブロックで実行を継続します。
- スタック縮小のプロセス:
- Goランタイムは、スタックの使用量が大幅に減少し、現在のスタックサイズが過剰であると判断した場合、同様のコピー操作によってスタックをより小さな連続したブロックに縮小することもあります。これにより、メモリの効率的な利用が促進されます。
この「コピーによるスタック拡張」は、セグメント化されたスタックの欠点を克服し、以下の利点をもたらしました。
- 単純化: スタックが常に連続であるため、ランタイムのスタック管理ロジックが大幅に単純化されました。
- ガベージコレクションの効率化: ガベージコレクタは連続したスタック領域を効率的にスキャンできるようになり、GCのパフォーマンスが向上しました。
- キャッシュ効率の向上: 連続したメモリ領域は、CPUのキャッシュラインに乗りやすく、メモリアクセスのパフォーマンスが向上する可能性があります。
- 予測可能なパフォーマンス: スタック拡張時のコピーはコストがかかりますが、その発生頻度とコストはセグメント切り替えよりも予測しやすくなりました。
このコミットは、この重要なランタイムの変更を反映するために、FAQの記述を「segmented stacks」から「resizable, bounded stacks」へと更新し、ゴルーチンのスタック管理に関する最新かつ正確な情報を提供するものです。また、gccgo
コンパイラが依然としてLinux上でセグメント化されたスタックを使用していることについても言及し、両者の違いを明確にしています。
コアとなるコードの変更箇所
--- a/doc/go_faq.html
+++ b/doc/go_faq.html
@@ -426,18 +426,20 @@ When a coroutine blocks, such as by calling a blocking system call,
the run-time automatically moves other coroutines on the same operating
system thread to a different, runnable thread so they won't be blocked.
The programmer sees none of this, which is the point.
-The result, which we call goroutines, can be very cheap: unless they spend a lot of time
-in long-running system calls, they cost little more than the memory
-for the stack, which is just a few kilobytes.
+The result, which we call goroutines, can be very cheap: they have little
+overhead beyond the memory for the stack, which is just a few kilobytes.
</p>
<p>
-To make the stacks small, Go's run-time uses segmented stacks. A newly
+To make the stacks small, Go's run-time uses resizable, bounded stacks. A newly
minted goroutine is given a few kilobytes, which is almost always enough.
-When it isn't, the run-time allocates (and frees) extension segments automatically.
-The overhead averages about three cheap instructions per function call.
+When it isn't, the run-time grows (and shrinks) the memory for storing
+the stack automatically, allowing many goroutines to live in a modest
+amount of memory.
+The CPU overhead averages about three cheap instructions per function call.
It is practical to create hundreds of thousands of goroutines in the same
-address space. If goroutines were just threads, system resources would
+address space.
+If goroutines were just threads, system resources would
run out at a much smaller number.
</p>
@@ -1614,9 +1616,10 @@ it now. <code>Gccgo</code>'s run-time support uses <code>glibc</code>.
<code>Gc</code> uses a custom library to keep the footprint under
control; it is
compiled with a version of the Plan 9 C compiler that supports
-segmented stacks for goroutines.
-The <code>gccgo</code> compiler implements segmented
-stacks on Linux only, supported by recent modifications to the gold linker.
+resizable stacks for goroutines.
+The <code>gccgo</code> compiler implements these on Linux only,
+using a technique called segmented stacks,
+supported by recent modifications to the gold linker.
</p>
<h3 id="Why_is_my_trivial_program_such_a_large_binary">
コアとなるコードの解説
このコミットは、doc/go_faq.html
ファイル内の3つの主要な箇所を変更しています。
-
ゴルーチンのコストに関する記述の修正 (行 426-429):
- 変更前: 「ゴルーチンは非常に安価であり、長時間実行されるシステムコールに多くの時間を費やさない限り、スタックのためのメモリ(わずか数キロバイト)以上のコストはほとんどかからない。」
- 変更後: 「ゴルーチンは非常に安価であり、スタックのためのメモリ(わずか数キロバイト)以上のオーバーヘッドはほとんどない。」
- 解説: 以前の記述にあった「unless they spend a lot of time in long-running system calls」という条件が削除されました。これは、Goランタイムのスケジューラがシステムコールでブロックされたゴルーチンを効率的に処理する能力が向上したため、この条件が不要になったことを示唆しています。より簡潔で一般的な表現にすることで、ゴルーチンの軽量性を強調しています。
-
スタック管理方式の記述の更新 (行 431-437):
- 変更前: 「スタックを小さく保つために、Goのランタイムはセグメント化されたスタックを使用します。新しく生成されたゴルーチンには数キロバイトが与えられ、これはほとんどの場合十分です。そうでない場合、ランタイムは自動的に拡張セグメントを割り当て(そして解放し)ます。オーバーヘッドは関数呼び出しあたり平均約3つの安価な命令です。」
- 変更後: 「スタックを小さく保つために、Goのランタイムはリサイズ可能な、境界のあるスタックを使用します。新しく生成されたゴルーチンには数キロバイトが与えられ、これはほとんどの場合十分です。そうでない場合、ランタイムはスタックを格納するためのメモリを自動的に**拡張(および縮小)**し、多くのゴルーチンが適度な量のメモリで動作できるようにします。CPUオーバーヘッドは関数呼び出しあたり平均約3つの安価な命令です。」
- 解説: ここがこのコミットの最も重要な変更点です。Goの
gc
コンパイラがセグメント化されたスタックから、コピーによるスタック拡張(リサイズ可能な、境界のあるスタック)へと移行したことを明確に反映しています。- 「segmented stacks」が「resizable, bounded stacks」に変更されました。
- スタックの拡張方法の記述が「allocates (and frees) extension segments automatically」から「grows (and shrinks) the memory for storing the stack automatically」に変更され、スタックが連続したメモリ領域として拡張・縮小されることを示唆しています。
- 「The overhead averages about three cheap instructions per function call.」という記述は残されていますが、これはセグメント切り替えのオーバーヘッドではなく、新しいスタック管理方式におけるスタックチェックやコピーの準備に関するオーバーヘッドを指すようになったと考えられます。
-
gc
とgccgo
のスタック実装に関する記述の明確化 (行 1614-1619):- 変更前: 「
Gc
はフットプリントを抑えるためにカスタムライブラリを使用しており、ゴルーチン用のセグメント化されたスタックをサポートするPlan 9 Cコンパイラのバージョンでコンパイルされています。gccgo
コンパイラは、最近のgoldリンカの変更によってサポートされる、Linux上でのみセグメント化されたスタックを実装しています。」 - 変更後: 「
Gc
はフットプリントを抑えるためにカスタムライブラリを使用しており、ゴルーチン用のリサイズ可能なスタックをサポートするPlan 9 Cコンパイラのバージョンでコンパイルされています。gccgo
コンパイラは、Linux上でのみこれらを実装しており、セグメント化されたスタックと呼ばれる技術を使用し、最近のgoldリンカの変更によってサポートされています。」 - 解説:
gc
コンパイラが「segmented stacks」ではなく「resizable stacks」を使用していることを明確にしています。一方で、gccgo
コンパイラは依然としてLinux上で「segmented stacks」という技術を使用していることを明記し、両者の実装の違いを明確に区別しています。これにより、ユーザーがGoの異なるコンパイラ実装におけるスタック管理の違いについて正確な情報を得られるようになっています。
- 変更前: 「
これらの変更は、Goランタイムの進化に合わせて公式ドキュメントを最新の状態に保ち、ユーザーに正確な情報を提供することを目的としています。
関連リンク
- Go Issue #7373: doc/go_faq.html: update stack description
- Go CL 77950044: doc/go_faq.html: update description of stack management
参考にした情報源リンク
- The Go Programming Language Blog: Go 1.2 Runtime Improvements (特に "Stack management" セクション)
- Go Wiki: Stack
- Go Wiki: Go FAQ (このコミットで変更されたドキュメントの最新版)
- Goのスタック管理の変遷 - Qiita (日本語の解説記事)
- Goのスタック管理について - Zenn (日本語の解説記事)I have generated the detailed explanation in Japanese, following all the specified instructions and sections. The output is to standard output only.
# [インデックス 18916] ファイルの概要
このコミットは、Go言語の公式FAQドキュメントである `doc/go_faq.html` の内容を更新するものです。具体的には、Goランタイムにおけるスタック管理の記述を現代のGoの挙動に合わせて修正し、ゴルーチンとOSスレッドの比較をより正確にするための変更が含まれています。
## コミット
このコミットは、Goランタイムのスタック管理に関するFAQの記述を更新し、特に`gc`コンパイラがセグメント化されたスタックを使用しなくなったことを反映しています。また、ゴルーチンとスレッドの比較記述も改善されています。
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/287967f74c9d937b1075a648be5fd9247283cef6](https://github.com/golang/go/commit/287967f74c9d937b1075a648be5fd9247283cef6)
## 元コミット内容
commit 287967f74c9d937b1075a648be5fd9247283cef6 Author: Rob Pike r@golang.org Date: Fri Mar 21 13:59:30 2014 +1100
doc/go_faq.html: update description of stack management
They aren't segmented any more, at least with gc.
Also improve the comparison of goroutines and threads.
Fixes #7373.
LGTM=iant
R=iant
CC=golang-codereviews
https://golang.org/cl/77950044
doc/go_faq.html | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-)
diff --git a/doc/go_faq.html b/doc/go_faq.html index fb2d929bd6..9606213b1f 100644 --- a/doc/go_faq.html +++ b/doc/go_faq.html @@ -426,18 +426,20 @@ When a coroutine blocks, such as by calling a blocking system call, the run-time automatically moves other coroutines on the same operating system thread to a different, runnable thread so they won't be blocked. The programmer sees none of this, which is the point. -The result, which we call goroutines, can be very cheap: unless they spend a lot of time -in long-running system calls, they cost little more than the memory -for the stack, which is just a few kilobytes. +The result, which we call goroutines, can be very cheap: they have little +overhead beyond the memory for the stack, which is just a few kilobytes.
-To make the stacks small, Go's run-time uses segmented stacks. A newly +To make the stacks small, Go's run-time uses resizable, bounded stacks. A newly minted goroutine is given a few kilobytes, which is almost always enough. -When it isn't, the run-time allocates (and frees) extension segments automatically. -The overhead averages about three cheap instructions per function call. +When it isn't, the run-time grows (and shrinks) the memory for storing +the stack automatically, allowing many goroutines to live in a modest +amount of memory. +The CPU overhead averages about three cheap instructions per function call. It is practical to create hundreds of thousands of goroutines in the same -address space. If goroutines were just threads, system resources would +address space. +If goroutines were just threads, system resources would run out at a much smaller number.
@@ -1614,9 +1616,10 @@ it now. Gccgo
's run-time support uses glibc
.
Gc
uses a custom library to keep the footprint under
control; it is
compiled with a version of the Plan 9 C compiler that supports
-segmented stacks for goroutines.
-The gccgo
compiler implements segmented
-stacks on Linux only, supported by recent modifications to the gold linker.
+resizable stacks for goroutines.
+The gccgo
compiler implements these on Linux only,
+using a technique called segmented stacks,
+supported by recent modifications to the gold linker.
```
変更の背景
このコミットの主な背景は、Go言語のランタイムにおけるスタック管理の実装が進化し、特に公式コンパイラであるgc
が「セグメント化されたスタック (segmented stacks)」から「リサイズ可能な、境界のあるスタック (resizable, bounded stacks)」へと移行したことを、公式FAQドキュメントに正確に反映させる必要があったためです。
Goの初期のバージョンでは、ゴルーチンのスタックはセグメント化されたスタックとして実装されていました。これは、スタックが足りなくなったときに新しいメモリセグメントを割り当てて既存のスタックに連結することで、スタックを動的に拡張する仕組みです。これにより、初期スタックサイズを非常に小さく保ちつつ、必要に応じて拡張できるという利点がありました。しかし、この方式にはいくつかの欠点も存在しました。例えば、スタックの拡張や縮小時に複数のセグメントを管理するためのオーバーヘッドや、スタックフレームがセグメント境界をまたぐ場合の複雑性、そしてガベージコレクションとの連携における課題などです。
Goチームは、これらの課題を解決し、より効率的で予測可能なスタック管理を実現するために、セグメント化されたスタックから、より現代的な「コピーによるスタック拡張 (stack copying)」アプローチへと移行しました。このアプローチでは、スタックが足りなくなった場合、より大きな連続したメモリ領域を割り当て、既存のスタックの内容を新しい領域にコピーします。これにより、スタックの管理が単純化され、パフォーマンスが向上し、ガベージコレクションとの統合も容易になりました。
この変更は、Go 1.2で導入された重要なランタイムの改善点の一つであり、このコミットはその変更をFAQに反映させるためのものです。また、ゴルーチンとOSスレッドの比較についても、より正確で誤解を招かない表現に修正されています。
この変更は、GoのIssue #7373 (doc/go_faq.html: update stack description
) に対応するものです。
前提知識の解説
ゴルーチン (Goroutines)
Go言語におけるゴルーチンは、軽量な並行実行単位です。OSのスレッドとは異なり、Goランタイムによって管理され、非常に少ないメモリ(数KB)で起動できます。Goランタイムは、少数のOSスレッド上で多数のゴルーチンを多重化(マルチプレックス)して実行します。これにより、開発者は複雑なスレッド管理を意識することなく、簡単に並行処理を記述できます。ゴルーチンがブロックされる(例:ネットワークI/O待ち)と、Goランタイムは自動的に同じOSスレッド上の他のゴルーチンを別の実行可能なOSスレッドに移動させ、プログラム全体の実行を継続させます。
スタック管理 (Stack Management)
プログラムが関数を呼び出す際、その関数のローカル変数や引数、戻りアドレスなどを格納するために「スタック」と呼ばれるメモリ領域が使用されます。スタックはLIFO(後入れ先出し)のデータ構造で、関数呼び出しごとにスタックフレームが積まれ、関数が終了するとそのスタックフレームが取り除かれます。スタックのサイズは、プログラムの実行中に動的に変化する可能性があります。
セグメント化されたスタック (Segmented Stacks)
セグメント化されたスタックは、Goの初期バージョンで採用されていたスタック管理手法の一つです。この方式では、スタックは連続した単一のメモリブロックではなく、必要に応じて小さなメモリセグメントを動的に割り当て、それらを連結することでスタックを拡張します。
- 利点:
- 小さな初期スタックサイズ: 新しいゴルーチンは非常に小さなスタック(数KB)で起動できるため、多数のゴルーチンを生成してもメモリ消費を抑えられます。
- 動的な拡張: スタックが足りなくなった場合、新しいセグメントを割り当てることで自動的に拡張されます。
- 欠点:
- オーバーヘッド: スタックの拡張や縮小時にセグメントの割り当て・解放、およびセグメント間の切り替え(スタックポインタの調整など)にオーバーヘッドが発生します。
- 複雑性: スタックフレームがセグメント境界をまたぐ場合、ポインタの調整やガベージコレクションの処理が複雑になる可能性があります。
- パフォーマンスの変動: スタックの拡張が頻繁に発生すると、パフォーマンスが不安定になることがあります。
リサイズ可能な、境界のあるスタック (Resizable, Bounded Stacks) / コピーによるスタック拡張 (Stack Copying)
Go 1.2以降のgc
コンパイラで採用されているスタック管理手法です。この方式では、スタックは常に連続したメモリブロックとして扱われます。
- 仕組み:
- 新しいゴルーチンは、数KBの小さな連続したスタック領域で起動します。
- スタックが足りなくなりそうになると(スタックガードページに到達するなど)、Goランタイムはスタックオーバーフローを検知します。
- ランタイムは、現在のスタックサイズよりも大きな新しい連続したメモリブロックをヒープ上に割り当てます。
- 既存のスタックの内容(スタックフレーム)を、新しい大きなブロックにコピーします。
- コピーが完了すると、古いスタック領域は解放され、ゴルーチンは新しいスタックブロックで実行を継続します。
- スタックが過剰に大きくなった場合、Goランタイムはスタックをより小さな領域に縮小するために同様のコピー操作を行うこともあります。
- 利点:
- 単純化: スタックが常に連続であるため、ランタイムのスタック管理ロジックが大幅に単純化されました。
- ガベージコレクションの効率化: ガベージコレクタは連続したスタック領域を効率的にスキャンできるようになり、GCのパフォーマンスが向上しました。
- キャッシュ効率の向上: 連続したメモリ領域は、CPUのキャッシュラインに乗りやすく、メモリアクセスのパフォーマンスが向上する可能性があります。
- 予測可能なパフォーマンス: スタックの拡張時には、スタック全体をコピーするコストが発生します。ただし、Goランタイムはこのコピー操作を非常に効率的に行うように設計されており、通常は問題になりません。
Goのgc
コンパイラとgccgo
コンパイラ
gc
(Go Compiler): Go言語の公式かつ主要なコンパイラです。Goチームによって開発・メンテナンスされており、Goの最新の機能やランタイムの最適化が最も早く反映されます。このコミットで言及されているスタック管理の変更は、主にこのgc
コンパイラに適用されます。gccgo
: GCC(GNU Compiler Collection)のフロントエンドとして実装されたGoコンパイラです。gc
とは異なるバックエンドを使用しており、GCCの最適化パスを利用できます。gccgo
は、gc
とは異なるスタック管理の実装(このコミットの時点ではLinux上でセグメント化されたスタックを使用)を持つことがあります。
技術的詳細
このコミットは、Goランタイムのスタック管理戦略の重要な進化を反映しています。Goの初期設計では、ゴルーチンの軽量性を実現するために「セグメント化されたスタック」が採用されていました。これは、スタックが固定サイズではなく、必要に応じて小さなメモリセグメントを動的に追加・削除することで、スタックを拡張・縮小するメカニズムです。これにより、ゴルーチンは非常に小さな初期スタック(数KB)で起動でき、多数のゴルーチンを生成してもメモリフットプリントを抑えることができました。
しかし、セグメント化されたスタックにはいくつかの課題がありました。
- スタック切り替えのオーバーヘッド: 関数呼び出しや戻り時に、スタックポインタがセグメント境界をまたぐ場合、追加のチェックとセグメント切り替えロジックが必要となり、わずかながらオーバーヘッドが発生しました。コミットメッセージの「The overhead averages about three cheap instructions per function call.」という記述は、このセグメント切り替えのコストを指していました。
- ガベージコレクションの複雑性: スタックが複数の非連続なセグメントに分散しているため、ガベージコレクタがスタック上のポインタを正確にスキャンし、参照されているオブジェクトを特定する作業が複雑になります。
- スタックのフラグメンテーション: セグメントの動的な割り当てと解放は、メモリのフラグメンテーションを引き起こす可能性がありました。
これらの課題に対処するため、Go 1.2ではgc
コンパイラにおいてスタック管理が大幅に改善され、「コピーによるスタック拡張(またはリサイズ可能な、境界のあるスタック)」というアプローチが導入されました。この新しいアプローチでは、ゴルーチンのスタックは常に連続したメモリブロックとして扱われます。
- スタック拡張のプロセス:
- ゴルーチンは、初期の小さなスタック(通常は2KB)で開始します。
- 関数呼び出しによってスタックが拡張され、現在のスタック領域の終端(スタックガードページ)に近づくと、Goランタイムはスタックオーバーフローを検知します。
- ランタイムは、現在のスタックサイズよりも大きな新しい連続したメモリブロックをヒープ上に割り当てます。
- 既存のスタックの内容(スタックフレーム、ローカル変数、戻りアドレスなど)が、新しい大きなブロックに効率的にコピーされます。
- コピーが完了すると、古いスタックブロックは解放され、ゴルーチンは新しいスタックブロックで実行を継続します。
- スタック縮小のプロセス:
- Goランタイムは、スタックの使用量が大幅に減少し、現在のスタックサイズが過剰であると判断した場合、同様のコピー操作によってスタックをより小さな連続したブロックに縮小することもあります。これにより、メモリの効率的な利用が促進されます。
この「コピーによるスタック拡張」は、セグメント化されたスタックの欠点を克服し、以下の利点をもたらしました。
- 単純化: スタックが常に連続であるため、ランタイムのスタック管理ロジックが大幅に単純化されました。
- ガベージコレクションの効率化: ガベージコレクタは連続したスタック領域を効率的にスキャンできるようになり、GCのパフォーマンスが向上しました。
- キャッシュ効率の向上: 連続したメモリ領域は、CPUのキャッシュラインに乗りやすく、メモリアクセスのパフォーマンスが向上する可能性があります。
- 予測可能なパフォーマンス: スタック拡張時のコピーはコストがかかりますが、その発生頻度とコストはセグメント切り替えよりも予測しやすくなりました。
このコミットは、この重要なランタイムの変更を反映するために、FAQの記述を「segmented stacks」から「resizable, bounded stacks」へと更新し、ゴルーチンのスタック管理に関する最新かつ正確な情報を提供するものです。また、gccgo
コンパイラが依然としてLinux上でセグメント化されたスタックを使用していることについても言及し、両者の違いを明確にしています。
コアとなるコードの変更箇所
--- a/doc/go_faq.html
+++ b/doc/go_faq.html
@@ -426,18 +426,20 @@ When a coroutine blocks, such as by calling a blocking system call,
the run-time automatically moves other coroutines on the same operating
system thread to a different, runnable thread so they won't be blocked.
The programmer sees none of this, which is the point.
-The result, which we call goroutines, can be very cheap: unless they spend a lot of time
-in long-running system calls, they cost little more than the memory
-for the stack, which is just a few kilobytes.
+The result, which we call goroutines, can be very cheap: they have little
+overhead beyond the memory for the stack, which is just a few kilobytes.
</p>
<p>
-To make the stacks small, Go's run-time uses segmented stacks. A newly
+To make the stacks small, Go's run-time uses resizable, bounded stacks. A newly
minted goroutine is given a few kilobytes, which is almost always enough.
-When it isn't, the run-time allocates (and frees) extension segments automatically.
-The overhead averages about three cheap instructions per function call.
+When it isn't, the run-time grows (and shrinks) the memory for storing
+the stack automatically, allowing many goroutines to live in a modest
+amount of memory.
+The CPU overhead averages about three cheap instructions per function call.
It is practical to create hundreds of thousands of goroutines in the same
-address space. If goroutines were just threads, system resources would
+address space.
+If goroutines were just threads, system resources would
run out at a much smaller number.
</p>
@@ -1614,9 +1616,10 @@ it now. <code>Gccgo</code>'s run-time support uses <code>glibc</code>.
<code>Gc</code> uses a custom library to keep the footprint under
control; it is
compiled with a version of the Plan 9 C compiler that supports
-segmented stacks for goroutines.
-The <code>gccgo</code> compiler implements segmented
-stacks on Linux only, supported by recent modifications to the gold linker.
+resizable stacks for goroutines.
+The <code>gccgo</code> compiler implements these on Linux only,
+using a technique called segmented stacks,
+supported by recent modifications to the gold linker.
</p>
<h3 id="Why_is_my_trivial_program_such_a_large_binary">
コアとなるコードの解説
このコミットは、doc/go_faq.html
ファイル内の3つの主要な箇所を変更しています。
-
ゴルーチンのコストに関する記述の修正 (行 426-429):
- 変更前: 「ゴルーチンは非常に安価であり、長時間実行されるシステムコールに多くの時間を費やさない限り、スタックのためのメモリ(わずか数キロバイト)以上のコストはほとんどかからない。」
- 変更後: 「ゴルーチンは非常に安価であり、スタックのためのメモリ(わずか数キロバイト)以上のオーバーヘッドはほとんどない。」
- 解説: 以前の記述にあった「unless they spend a lot of time in long-running system calls」という条件が削除されました。これは、Goランタイムのスケジューラがシステムコールでブロックされたゴルーチンを効率的に処理する能力が向上したため、この条件が不要になったことを示唆しています。より簡潔で一般的な表現にすることで、ゴルーチンの軽量性を強調しています。
-
スタック管理方式の記述の更新 (行 431-437):
- 変更前: 「スタックを小さく保つために、Goのランタイムはセグメント化されたスタックを使用します。新しく生成されたゴルーチンには数キロバイトが与えられ、これはほとんどの場合十分です。そうでない場合、ランタイムは自動的に拡張セグメントを割り当て(そして解放し)ます。オーバーヘッドは関数呼び出しあたり平均約3つの安価な命令です。」
- 変更後: 「スタックを小さく保つために、Goのランタイムはリサイズ可能な、境界のあるスタックを使用します。新しく生成されたゴルーチンには数キロバイトが与えられ、これはほとんどの場合十分です。そうでない場合、ランタイムはスタックを格納するためのメモリを自動的に**拡張(および縮小)**し、多くのゴルーチンが適度な量のメモリで動作できるようにします。CPUオーバーヘッドは関数呼び出しあたり平均約3つの安価な命令です。」
- 解説: ここがこのコミットの最も重要な変更点です。Goの
gc
コンパイラがセグメント化されたスタックから、コピーによるスタック拡張(リサイズ可能な、境界のあるスタック)へと移行したことを明確に反映しています。- 「segmented stacks」が「resizable, bounded stacks」に変更されました。
- スタックの拡張方法の記述が「allocates (and frees) extension segments automatically」から「grows (and shrinks) the memory for storing the stack automatically」に変更され、スタックが連続したメモリ領域として拡張・縮小されることを示唆しています。
- 「The overhead averages about three cheap instructions per function call.」という記述は残されていますが、これはセグメント切り替えのオーバーヘッドではなく、新しいスタック管理方式におけるスタックチェックやコピーの準備に関するオーバーヘッドを指すようになったと考えられます。
-
gc
とgccgo
のスタック実装に関する記述の明確化 (行 1614-1619):- 変更前: 「
Gc
はフットプリントを抑えるためにカスタムライブラリを使用しており、ゴルーチン用のセグメント化されたスタックをサポートするPlan 9 Cコンパイラのバージョンでコンパイルされています。gccgo
コンパイラは、最近のgoldリンカの変更によってサポートされる、Linux上でのみセグメント化されたスタックを実装しています。」 - 変更後: 「
Gc
はフットプリントを抑えるためにカスタムライブラリを使用しており、ゴルーチン用のリサイズ可能なスタックをサポートするPlan 9 Cコンパイラのバージョンでコンパイルされています。gccgo
コンパイラは、Linux上でのみこれらを実装しており、セグメント化されたスタックと呼ばれる技術を使用し、最近のgoldリンカの変更によってサポートされています。」 - 解説:
gc
コンパイラが「segmented stacks」ではなく「resizable stacks」を使用していることを明確にしています。一方で、gccgo
コンパイラは依然としてLinux上で「segmented stacks」という技術を使用していることを明記し、両者の実装の違いを明確に区別しています。これにより、ユーザーがGoの異なるコンパイラ実装におけるスタック管理の違いについて正確な情報を得られるようになっています。
- 変更前: 「
これらの変更は、Goランタイムの進化に合わせて公式ドキュメントを最新の状態に保ち、ユーザーに正確な情報を提供することを目的としています。
関連リンク
- Go Issue #7373: doc/go_faq.html: update stack description
- Go CL 77950044: doc/go_faq.html: update description of stack management
参考にした情報源リンク
- The Go Programming Language Blog: Go 1.2 Runtime Improvements (特に "Stack management" セクション)
- Go Wiki: Stack
- Go Wiki: Go FAQ (このコミットで変更されたドキュメントの最新版)
- Goのスタック管理の変遷 - Qiita (日本語の解説記事)
- Goのスタック管理について - Zenn (日本語の解説記事)