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

[インデックス 17738] ファイルの概要

このコミットは、Goランタイムにおけるスタックセグメントのデフォルトサイズを4KBから8KBに変更するものです。この変更は、Go 1ベンチマークの様々なテストにおいて、amd64および386システムの両方で顕著なパフォーマンス向上をもたらしました。特に、GobEncodeベンチマークでは64%もの大幅な改善が見られます。これは、スタックの再割り当て(スタックの拡張)が頻繁に発生するシナリオにおいて、そのオーバーヘッドを削減することを目的としています。

コミット

  • コミットハッシュ: 408238e20bb794d91199c892c68a0989fc924d65
  • Author: Russ Cox rsc@golang.org
  • Date: Thu Oct 3 09:19:10 2013 -0400

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/408238e20bb794d91199c892c68a0989fc924d65

元コミット内容

runtime: change default stack segment size to 8 kB

Changing from 4 kB to 8 kB brings significant improvement
on a variety of the Go 1 benchmarks, on both amd64
and 386 systems.

Significant runtime reductions:

          amd64  386
GoParse    -14%  -1%
GobDecode  -12% -20%
GobEncode  -64%  -1%
JSONDecode  -9%  -4%
JSONEncode -15%  -5%
Template   -17% -14%

In the longer term, khr's new stacks will avoid needing to
make this decision at all, but for Go 1.2 this is a reasonable
stopgap that makes performance significantly better.

Demand paging should mean that if the second 4 kB is not
used, it will not be brought into memory, so the change
should not adversely affect resident set size.
The same argument could justify bumping as high as 64 kB
on 64-bit machines, but there are diminishing returns
after 8 kB, and using 8 kB limits the possible unintended
memory overheads we are not aware of.

Benchmark graphs at
http://swtch.com/~rsc/gostackamd64.html
http://swtch.com/~rsc/gostack386.html

Full data at
http://swtch.com/~rsc/gostack.zip

R=golang-dev, khr, dave, bradfitz, dvyukov
CC=golang-dev
https://golang.org/cl/14317043

変更の背景

Go言語のランタイムは、ゴルーチン(goroutine)と呼ばれる軽量な並行処理単位を使用しており、各ゴルーチンは独自のスタックを持っています。Go 1.2のリリースに向けて、ランタイムのパフォーマンス最適化が図られていました。

このコミットの主な背景は、Goプログラムの実行時におけるスタックの拡張(stack growth)に伴うオーバーヘッドの削減です。Goのスタックは、必要に応じて動的にサイズが変更される「セグメントスタック」という方式を採用していました。これは、スタックの使用量が少ないゴルーチンではメモリを節約し、スタックの使用量が増えるにつれて自動的に拡張されるという利点があります。しかし、スタックの拡張は、新しいスタックセグメントの割り当て、古いセグメントからの内容のコピー、そしてポインタの更新といった処理を伴い、これがパフォーマンスのボトルネックとなることがありました。

特に、関数呼び出しが深くネストされるような処理や、再帰的な処理が多いプログラムでは、スタックの拡張が頻繁に発生し、その都度パフォーマンスコストが発生します。コミットメッセージに記載されているベンチマーク結果(GoParse, GobDecode, GobEncode, JSONDecode, JSONEncode, Templateなど)は、これらの処理がスタックの頻繁な拡張によって影響を受けていたことを示唆しています。

このコミットは、スタックセグメントの最小サイズを4KBから8KBに増やすことで、スタック拡張の頻度を減らし、結果として全体的な実行時間を短縮することを目的としています。これは、将来的に導入される予定の新しいスタック管理メカニズム(khr氏による「新しいスタック」)が完全に実装されるまでの「一時的な解決策(stopgap)」として位置づけられています。

また、メモリ使用量への影響も考慮されています。コミットメッセージでは「デマンドページング(Demand paging)」の概念に触れ、スタックセグメントが大きくなっても、実際に使用されない部分は物理メモリにロードされないため、常駐セットサイズ(Resident Set Size: RSS)に悪影響を与えないと説明されています。これにより、パフォーマンス向上とメモリ効率のバランスが図られています。

前提知識の解説

このコミットを理解するためには、以下の概念について理解しておく必要があります。

  1. Goランタイム (Go Runtime): Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ガベージコレクション、スケジューラ(ゴルーチンの管理と実行)、メモリ管理、スタック管理などが含まれます。Goプログラムは、OSのプロセスやスレッドに直接マッピングされるのではなく、Goランタイムが軽量なゴルーチンをOSスレッド上で多重化して実行します。

  2. ゴルーチン (Goroutine): Go言語における並行処理の基本単位です。OSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行することが可能です。各ゴルーチンは、非常に小さな初期スタックサイズ(通常は数KB)で開始し、必要に応じてスタックを動的に拡張します。

  3. スタック (Stack): プログラムの実行中に、関数呼び出しの引数、ローカル変数、戻りアドレスなどを一時的に格納するために使用されるメモリ領域です。関数が呼び出されるたびにスタックフレームがプッシュされ、関数から戻るたびにポップされます。

  4. セグメントスタック (Segmented Stacks): Go 1.x系で採用されていたスタック管理方式の一つです。ゴルーチンのスタックは、固定サイズのセグメント(チャンク)に分割され、必要に応じて新しいセグメントが割り当てられ、既存のセグメントに連結されます。これにより、スタックの最大サイズを事前に決定する必要がなく、メモリを効率的に利用できます。しかし、セグメントの拡張時には、古いセグメントの内容を新しいセグメントにコピーするオーバーヘッドが発生します。

  5. スタック拡張 (Stack Growth): ゴルーチンが実行中に現在のスタックセグメントの容量を超えてメモリを必要とする場合、ランタイムはより大きなスタックセグメントを割り当て、既存のスタックの内容を新しいセグメントにコピーします。このプロセスがスタック拡張です。この操作は、プログラムの実行を一時的に停止させるため、頻繁に発生するとパフォーマンスに悪影響を与えます。

  6. デマンドページング (Demand Paging): オペレーティングシステムが提供するメモリ管理技術の一つです。プログラムが仮想メモリ空間を要求しても、その全ての領域がすぐに物理メモリに割り当てられるわけではありません。実際にそのメモリ領域にアクセスがあったときに初めて、OSが対応する物理メモリページを割り当て、ディスクからデータをロードします。これにより、プログラムが要求する仮想メモリ空間が大きくても、実際に使用される物理メモリは最小限に抑えられます。このコミットでは、スタックセグメントのサイズを大きくしても、実際に使用されない部分は物理メモリにロードされないため、常駐セットサイズ(RSS)への影響は小さいと説明されています。

  7. 常駐セットサイズ (Resident Set Size: RSS): プロセスが現在物理メモリに保持しているメモリの量を示す指標です。デマンドページングの概念と関連して、スタックセグメントのサイズを大きくしても、RSSが大幅に増加しないことが期待されます。

技術的詳細

このコミットは、Goランタイムのスタック管理に関連する2つの定数、StackExtraStackMinの値を変更しています。これらの定数は、src/pkg/runtime/stack.hファイルで定義されています。

  • StackExtra: これは、スタック拡張が発生した際に、現在のフレームが必要とするサイズに加えて追加で割り当てられるスタックの量を示します。

    • 変更前: 1024 バイト (1 KB)
    • 変更後: 2048 バイト (2 KB) この値が増加することで、一度のスタック拡張でより多くの余裕を持たせることができ、その後の連続した関数呼び出しで再びスタック拡張が発生する可能性を低減します。
  • StackMin: これは、スタックセグメントに割り当てられる最小サイズを示します。スタック拡張が必要なフレームのサイズとStackExtraの合計がこの値よりも小さい場合でも、スタックはこのStackMinのサイズで割り当てられます。

    • 変更前: 4096 バイト (4 KB)
    • 変更後: 8192 バイト (8 KB) この変更が最も重要です。スタックセグメントの最小サイズが倍になることで、多くのゴルーチンが初期段階でより大きなスタックを持つことになります。これにより、スタック拡張が必要となる閾値が高くなり、結果としてスタック拡張の頻度が大幅に減少します。

これらの変更は、特にスタックの使用量が比較的少ないが、頻繁にスタック拡張が発生するようなワークロードにおいて効果を発揮します。例えば、再帰的な関数呼び出しや、多くの小さな関数が連続して呼び出されるようなシナリオです。スタック拡張のコストは、メモリの割り当て、データのコピー、そしてガベージコレクタへの通知など、複数の要因によって発生します。これらのコストを削減することで、全体的な実行パフォーマンスが向上します。

コミットメッセージに記載されているベンチマーク結果は、この変更が実際にパフォーマンスに与えるポジティブな影響を明確に示しています。特にGobEncodeのようなデータシリアライゼーション処理では、深い関数呼び出しや一時的なデータ構造の生成が頻繁に行われるため、スタック拡張のオーバーヘッドが顕著に現れやすかったと考えられます。

また、この変更はGo 1.2のリリースに向けた「一時的な解決策」とされています。これは、当時開発中であった「新しいスタック」(Contiguous Stacks)への移行が完了するまでの間、既存のセグメントスタックモデルのパフォーマンスを改善するための措置でした。新しいスタックモデルでは、スタックが連続したメモリ領域として扱われるため、セグメントスタックのような頻繁な拡張とコピーのオーバーヘッドが根本的に解消されます。

メモリ使用量に関しては、デマンドページングの特性により、スタックセグメントのサイズが大きくなっても、実際に使用されないページは物理メモリにロードされないため、常駐セットサイズ(RSS)への影響は限定的であると説明されています。しかし、コミットメッセージでは、8KBを超えるサイズ(例えば64KB)への増加は、未知のメモリオーバーヘッドを引き起こす可能性があり、8KBが「収穫逓減(diminishing returns)」の点から見て最適なバランスであると判断されています。これは、パフォーマンス向上とメモリフットプリントの増加リスクのトレードオフを考慮した結果です。

コアとなるコードの変更箇所

変更は src/pkg/runtime/stack.h ファイルで行われています。

--- a/src/pkg/runtime/stack.h
+++ b/src/pkg/runtime/stack.h
@@ -71,12 +71,12 @@ enum {
 
 	// The amount of extra stack to allocate beyond the size
 	// needed for the single frame that triggered the split.
-	StackExtra = 1024,
+	StackExtra = 2048,
 
 	// 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,
 	FixedStack = StackMin + StackSystem,
 
 	// Functions that need frames bigger than this use an extra

コアとなるコードの解説

上記のコードスニペットは、Goランタイムのスタック管理に関連する重要な定数の定義を示しています。

  • StackExtra: この定数は、スタックが拡張される際に、現在の関数呼び出しに必要なスタックサイズに加えて、追加で確保されるメモリの量を定義しています。

    • 変更前は 1024 (1KB) でした。これは、スタック拡張が発生した際に、現在のフレームが要求するサイズに加えて1KBの余裕を持たせることを意味します。
    • 変更後は 2048 (2KB) になりました。これにより、一度のスタック拡張でより多くの余裕が確保されるため、その後の数回の関数呼び出しで再びスタック拡張が必要になる可能性が低減されます。これは、スタック拡張の頻度を減らすための直接的な手段です。
  • StackMin: この定数は、ゴルーチンに割り当てられるスタックセグメントの最小サイズを定義しています。

    • 変更前は 4096 (4KB) でした。これは、Go 1.x系のセグメントスタックにおいて、各ゴルーチンが最初に割り当てられるスタックの最小サイズが4KBであったことを意味します。
    • 変更後は 8192 (8KB) になりました。この変更がこのコミットの最も重要な部分です。スタックの最小サイズが倍になることで、多くのゴルーチンが初期段階でより大きなスタックを持つことになります。これにより、スタック拡張が必要となる閾値が高くなり、結果としてスタック拡張の発生頻度が大幅に減少します。特に、スタック使用量が4KBから8KBの範囲に収まるような関数呼び出しパターンでは、以前はスタック拡張が発生していたものが、この変更により拡張が不要になるため、パフォーマンスが向上します。
  • FixedStack: この定数は、StackMinStackSystemの合計として定義されています。StackSystemはシステムが予約するスタック領域(例えばシグナルハンドラ用など)を指すと考えられます。FixedStackは、特定の固定スタックサイズを必要とする関数や、スタック拡張が不要な場合に利用されるスタックのサイズに関連している可能性があります。StackMinの変更に伴い、FixedStackの値も自動的に増加します。

これらの定数の変更は、Goランタイムのスタック管理戦略における重要な調整です。スタック拡張のコストは無視できないため、その頻度を減らすことは全体的なパフォーマンス向上に直結します。特に、Go 1.2の時点ではセグメントスタックが採用されていたため、この調整は当時のGoプログラムの実行効率を向上させる上で非常に効果的な手段でした。

関連リンク

参考にした情報源リンク

  • Go言語のスタック管理に関する公式ドキュメントやブログ記事 (当時のGo 1.2周辺の情報を参照)
  • Goのセグメントスタックと連続スタックに関する技術解説記事
  • オペレーティングシステムのデマンドページングに関する一般的な情報
  • Goのガベージコレクションとランタイムの仕組みに関する一般的な情報
  • Goのベンチマークツールと結果の解釈に関する情報