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

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

このコミットは、Goランタイムにおけるスタックサイズの丸め処理に関する変更です。特に、スタックサイズを2のべき乗に丸めることで、Windows/386およびPlan9/386環境でのビルド問題を修正し、Issue #7487を解決することを目的としています。

コミット

commit 84570aa9a18fa46dba1402004a54cedc7cf5e043
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Fri Mar 7 15:11:16 2014 -0500

    runtime: round stack size to power of 2.
    Fixes build on windows/386 and plan9/386.
    Fixes #7487.
    
    LGTM=mattn.jp, dvyukov, rsc
    R=golang-codereviews, mattn.jp, dvyukov, 0intro, rsc
    CC=golang-codereviews
    https://golang.org/cl/72360043

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

https://github.com/golang/go/commit/84570aa9a18fa46dba1402004a54cedc7cf5e043

元コミット内容

Goランタイムにおいて、スタックサイズを2のべき乗に丸めるように変更します。これにより、Windows/386およびPlan9/386環境でのビルドが修正され、Issue #7487が解決されます。

変更の背景

このコミットの主な背景は、特定のアーキテクチャ(特にWindows/386とPlan9/386)において、Goランタイムがスタックを割り当てる際に発生していたビルド問題と、それに起因するIssue #7487の解決です。

Goのランタイムは、ゴルーチンごとにスタックを割り当てます。効率的なメモリ管理とパフォーマンスのために、スタックの割り当てには特定のアライメント要件やサイズ制約が存在することが一般的です。以前のGoランタイムでは、スタックサイズが常に2のべき乗に丸められていなかったため、一部のシステム(特に32ビット環境)でスタックの境界条件に関する問題が発生していました。

具体的には、WindowsやPlan 9のようなOSでは、シグナルハンドリングなどのOS固有の目的のために、通常のガード領域の下にスタックごとに特定のシステム領域(StackSystem)を必要とします。このStackSystemのサイズと、Goランタイムが割り当てるスタックの基本サイズ(StackMin)の合計が、特定の条件下で2のべき乗にならない場合がありました。

Goのスタック管理は、スタックの拡張(stack splitting)や縮小を効率的に行うために、スタックが特定のサイズ(通常は2のべき乗)にアラインされていることを前提としている場合があります。この前提が崩れると、スタックの割り当てや管理ロジックが正しく機能せず、ビルドエラーやランタイムエラーにつながる可能性がありました。

Issue #7487は、まさにこの問題、すなわちスタックサイズが2のべき乗にアラインされていないことによって引き起こされるビルド失敗やクラッシュを報告していました。このコミットは、スタックサイズを明示的に2のべき乗に丸めることで、この根本原因を解決し、これらのプラットフォームでの安定性と互換性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念と一般的なコンピュータサイエンスの知識が必要です。

  1. ゴルーチン (Goroutine): Go言語における軽量な実行スレッドです。OSのスレッドよりもはるかに少ないメモリで作成でき、数百万のゴルーチンを同時に実行することが可能です。各ゴルーチンは独自のスタックを持ちます。

  2. スタック (Stack): プログラムの実行中に、関数呼び出しの引数、ローカル変数、リターンアドレスなどを格納するために使用されるメモリ領域です。Goのゴルーチンスタックは、必要に応じて動的にサイズが変更されます(stack splitting/copying)。

  3. スタックの動的拡張 (Stack Splitting/Copying): Goランタイムの重要な機能の一つで、ゴルーチンのスタックが不足しそうになったときに、より大きなスタックを割り当てて内容をコピーし、新しいスタックに切り替えるメカニズムです。これにより、初期スタックサイズを小さく保ちつつ、必要に応じてスタックを拡張できます。

  4. 2のべき乗 (Power of 2): 2、4、8、16、32、... のように、2を整数回掛け合わせた数です。コンピュータシステムでは、メモリのアライメント、バッファサイズ、ハッシュテーブルのサイズなど、多くの場所で2のべき乗が効率的な処理のために利用されます。2のべき乗にアラインされたメモリ領域は、アドレス計算やビット演算が高速に行えるため、パフォーマンス上の利点があります。

  5. メモリのアライメント (Memory Alignment): データがメモリ上で特定の境界(例えば、4バイト境界、8バイト境界など)に配置されることを指します。CPUは、アラインされたデータにアクセスする方が高速であるため、コンパイラやランタイムはデータの配置を調整します。スタックも同様に、特定の境界にアラインされていることが期待される場合があります。

  6. StackSystem: GoランタイムがOS固有の目的(シグナルハンドリングなど)のために、通常のスタックガード領域の下に確保する追加のスタック領域です。特にWindowsやPlan 9のようなOSで必要とされます。

  7. StackMin: ゴルーチンに割り当てられる最小のスタックサイズです。

  8. runtime·round2 関数: このコミットで導入または変更される、数値を最も近い(またはそれ以上の)2のべき乗に丸めるユーティリティ関数です。

  9. proc.c, runtime.c, runtime.h, stack.c, stack.h: これらはGoランタイムのC言語で書かれたソースファイルです。

    • proc.c: プロセス(GoではP、M、Gなどのスケジューリングエンティティ)管理に関連するコード。ゴルーチンの割り当て(runtime·malg)もここに含まれます。
    • runtime.c: ランタイムのコア機能、初期化、エラーチェックなど。
    • runtime.h: ランタイムの共通ヘッダファイルで、関数プロトタイプや定数定義が含まれます。
    • stack.c: スタックの割り当て、拡張、コピーなど、スタック管理のロジック。
    • stack.h: スタック関連の定数や構造体の定義。

技術的詳細

このコミットの核心は、Goランタイムがゴルーチンにスタックを割り当てる際に、そのサイズを常に2のべき乗に丸めるように変更することです。これにより、特に32ビットアーキテクチャ(Windows/386, Plan9/386)におけるスタックアライメントの問題を解決します。

変更の具体的な技術的詳細は以下の通りです。

  1. runtime·round2 関数の導入/変更:

    • src/pkg/runtime/stack.c にあった static int32 round2(int32 x) 関数が、int32 runtime·round2(int32 x) としてグローバルに公開されるように変更されました。これにより、ランタイムの他の部分からこの関数を呼び出してスタックサイズを丸めることが可能になります。
    • この関数は、与えられた整数 x を、x 以上で最小の2のべき乗に丸めます。例えば、round2(5)8 を返し、round2(10)16 を返します。
  2. スタック割り当て時の丸め処理の適用:

    • src/pkg/runtime/proc.cruntime·malg 関数(新しいゴルーチンを割り当てる関数)内で、スタックサイズを計算する際に runtime·round2 が適用されるようになりました。
      // 変更前:
      // stk = runtime·stackalloc(newg, StackSystem + stacksize);
      // newg->stacksize = StackSystem + stacksize;
      // newg->stackbase = (uintptr)stk + StackSystem + stacksize - sizeof(Stktop);
      
      // 変更後:
      stacksize = runtime·round2(StackSystem + stacksize); // ここで丸める
      stk = runtime·stackalloc(newg, stacksize);
      newg->stacksize = stacksize;
      newg->stackbase = (uintptr)stk + stacksize - sizeof(Stktop);
      
      これにより、runtime·stackalloc に渡されるスタックサイズ、およびゴルーチン構造体 newgstacksizestackbase の計算が、常に2のべき乗に丸められた値に基づいて行われるようになります。
  3. runtime·newstack での丸め処理の適用:

    • src/pkg/runtime/stack.cruntime·newstack 関数(スタック拡張時に新しいスタックを割り当てる関数)でも、同様に runtime·round2 が適用されるようになりました。
      // 変更前:
      // framesize = round2(framesize);
      
      // 変更後:
      framesize = runtime·round2(framesize);
      
      これにより、スタック拡張時にも割り当てられるスタックサイズが2のべき乗に丸められます。
  4. FixedStack の計算ロジックの変更と検証:

    • src/pkg/runtime/stack.h において、StackSystem の定義がOSごとに調整され、特にWindowsとPlan 9では具体的なバイト数が指定されました。
    • 最も重要な変更は、FixedStack の計算方法です。
      // 変更前:
      // FixedStack = StackMin + StackSystem,
      
      // 変更後:
      StackSystemRounded = StackSystem + (-StackSystem & (StackMin-1));
      FixedStack = StackMin + StackSystemRounded;
      
      ここで StackSystemRounded は、StackSystemStackMin の倍数に丸めるための計算です。StackMin は通常4096バイト(4KB)であり、これは2のべき乗です。この計算により、FixedStack が常に2のべき乗になるように保証されます。
    • src/pkg/runtime/runtime.cruntime·check 関数に、FixedStack が2のべき乗であることを検証するアサーションが追加されました。
      if(FixedStack != runtime·round2(FixedStack))
          runtime·throw("FixedStack is not power-of-2");
      
      これにより、ビルド時にFixedStackが2のべき乗でない場合にエラーがスローされ、問題が早期に検出されるようになります。

これらの変更により、Goランタイムはスタックの割り当てと管理において、より厳密な2のべき乗アライメントを強制するようになります。これは、特に特定のOSやアーキテクチャでスタックの境界条件が厳しく、アライメントが正しくないと問題が発生する環境において、安定性と互換性を大幅に向上させます。

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

このコミットにおけるコアとなるコードの変更箇所は以下のファイルにまたがっています。

  1. src/pkg/runtime/proc.c:

    • runtime·malg 関数内で、新しいゴルーチンのスタックサイズを計算する際に runtime·round2 を呼び出すように変更。
    • newg->stacksizenewg->stackbase の計算で、丸められたスタックサイズを使用するように変更。
  2. src/pkg/runtime/runtime.c:

    • runtime·check 関数に、FixedStack が2のべき乗であることを検証するアサーションを追加。
    • stack.h のインクルードを追加。
  3. src/pkg/runtime/runtime.h:

    • runtime·round2 関数のプロトタイプ宣言を追加し、この関数が外部から利用可能であることを示す。
  4. src/pkg/runtime/stack.c:

    • round2 関数を runtime·round2 にリネームし、static キーワードを削除してグローバルに公開。
    • copystack 関数内で runtime·round2 を呼び出す箇所を修正。
    • runtime·newstack 関数内で、スタック拡張時のフレームサイズを runtime·round2 で丸めるように変更。
  5. src/pkg/runtime/stack.h:

    • StackSystem の定義をOS(Windows, Plan 9)ごとに具体的に指定。
    • FixedStack の計算ロジックを、StackSystemRounded を導入して変更し、常に2のべき乗になるように保証。

コアとなるコードの解説

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

@@ -1751,12 +1751,13 @@ runtime·malg(int32 stacksize)
 
 	newg = runtime·malloc(sizeof(G));
 	if(stacksize >= 0) {
+\t\tstacksize = runtime·round2(StackSystem + stacksize); // ここでスタックサイズを2のべき乗に丸める
 \t\tif(g == m->g0) {
 \t\t\t// running on scheduler stack already.
-\t\t\tstk = runtime·stackalloc(newg, StackSystem + stacksize);
+\t\t\tstk = runtime·stackalloc(newg, stacksize); // 丸められたサイズを使用
 \t\t} else {\
 \t\t\t// have to call stackalloc on scheduler stack.
-\t\t\tnewg->stacksize = StackSystem + stacksize;
+\t\t\tnewg->stacksize = stacksize; // 丸められたサイズを使用
 \t\t\tg->param = newg;\n \t\t\truntime·mcall(mstackalloc);\n \t\t\tstk = g->param;\n@@ -1765,7 +1766,7 @@ runtime·malg(int32 stacksize)
 \t\tnewg->stack0 = (uintptr)stk;\n \t\tnewg->stackguard = (uintptr)stk + StackGuard;\n \t\tnewg->stackguard0 = newg->stackguard;\n-\t\tnewg->stackbase = (uintptr)stk + StackSystem + stacksize - sizeof(Stktop);\n+\t\tnewg->stackbase = (uintptr)stk + stacksize - sizeof(Stktop);\n \t}\n \treturn newg;\n }\n```
`runtime·malg` は新しいゴルーチンを割り当てる際に呼び出されます。ここで、要求された `stacksize` に `StackSystem` を加えた合計が、まず `runtime·round2` 関数によって最も近い2のべき乗に丸められます。その後、この丸められた `stacksize` が `runtime·stackalloc` に渡され、実際にスタックメモリが割り当てられます。また、ゴルーチン構造体 `newg` の `stacksize` と `stackbase` も、この丸められた値に基づいて設定されます。これにより、ゴルーチンのスタックが常に2のべき乗のサイズで確保されることが保証されます。

### `src/pkg/runtime/runtime.c` の変更

```c
@@ -256,6 +257,9 @@ runtime·check(void)
 	\truntime·throw(\"float32nan3\");
 
 	TestAtomic64();
+\n+\tif(FixedStack != runtime·round2(FixedStack))\n+\t\truntime·throw(\"FixedStack is not power-of-2\");\n }
 
 uint32

runtime·check はランタイムの初期化時に実行される検証関数です。ここに追加された行は、FixedStack(最小スタックサイズとシステムスタックサイズの合計)が実際に2のべき乗であるかどうかを検証します。もし2のべき乗でなければ、ランタイムはエラーをスローし、プログラムの実行を停止します。これは、スタック関連の重要な定数が正しく設定されていることを保証するための安全チェックです。

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

@@ -555,8 +555,8 @@ copystack(G *gp, uintptr nframes, uintptr newsize)
 }
 
 // round x up to a power of 2.
-static int32
-round2(int32 x)\n+int32
+runtime·round2(int32 x) // 関数名を変更し、staticを削除
 {\n \tint32 s;\n \n@@ -683,7 +683,7 @@ runtime·newstack(void)
 \tif(framesize < StackMin)\n \t\tframesize = StackMin;\n \tframesize += StackSystem;\n-\tframesize = round2(framesize);\n+\tframesize = runtime·round2(framesize);\n \tstk = runtime·stackalloc(gp, framesize);\n \tif(gp->stacksize > runtime·maxstacksize) {\n \t\truntime·printf(\"runtime: goroutine stack exceeds %D-byte limit\\n\", (uint64)runtime·maxstacksize);\n```
`round2` 関数が `runtime·round2` にリネームされ、`static` キーワードが削除されました。これにより、この関数はランタイムの他の部分から呼び出し可能になります。また、`runtime·newstack` 関数(スタック拡張時に新しいスタックを割り当てる)でも、割り当てる `framesize` が `runtime·round2` によって2のべき乗に丸められるようになりました。これは、スタックの動的拡張時にもアライメント要件が満たされることを保証します。

### `src/pkg/runtime/stack.h` の変更

```c
@@ -57,15 +57,13 @@ enum {
 	// to each stack below the usual guard area for OS-specific
 	// purposes like signal handling. Used on Windows and on
 	// Plan 9 because they do not use a separate stack.
-\t// The new stack code requires stacks to be a power of two,
-\t// and the default start size is 4k, so make StackSystem also 4k
-\t// to keep the sum a power of two. StackSystem used to be
-\t// 512*sizeof(uintptr) on Windows and 512 bytes on Plan 9.
 #ifdef GOOS_windows
-\tStackSystem = 4096,\n+\tStackSystem = 512 * sizeof(uintptr),\n #else
 #ifdef GOOS_plan9
-\tStackSystem = 4096,\n+\t// The size of the note handler frame varies among architectures,\n+\t// but 512 bytes should be enough for every implementation.\n+\tStackSystem = 512,\n #else
 \tStackSystem = 0,\n #endif\t// Plan 9
@@ -79,7 +77,8 @@ enum {
 	// If the amount needed for the splitting frame + StackExtra
 	// is less than this number, the stack will have this size instead.
 	StackMin = 4096,\n-\tFixedStack = StackMin + StackSystem,\n+\tStackSystemRounded = StackSystem + (-StackSystem & (StackMin-1)),\n+\tFixedStack = StackMin + StackSystemRounded,\n \n \t// Functions that need frames bigger than this use an extra\n \t// instruction to do the stack split check, to avoid overflow\n```
`StackSystem` の定義が、WindowsとPlan 9で具体的なバイト数(それぞれ `512 * sizeof(uintptr)` と `512`)に設定されました。これは、これらのOSでのシステムスタックの要件をより正確に反映するためです。
最も重要な変更は `FixedStack` の計算です。`StackSystemRounded` という新しい中間定数が導入され、`StackSystem` を `StackMin` の倍数に丸めるためのビット演算が使用されています。これにより、`FixedStack` が常に `StackMin` の倍数、かつ2のべき乗になることが保証されます。この変更は、スタックの最小サイズとシステムスタック領域の合計が、特定のアーキテクチャで期待されるアライメント要件を満たすようにするために不可欠です。

## 関連リンク

*   Go Issue #7487: [https://github.com/golang/go/issues/7487](https://github.com/golang/go/issues/7487)
*   Go CL 72360043: [https://golang.org/cl/72360043](https://golang.org/cl/72360043)

## 参考にした情報源リンク

*   Go言語の公式ドキュメント (Go runtime, goroutines, stack management)
*   Go言語のソースコード (特に `src/runtime` ディレクトリ)
*   Issue #7487 の議論スレッド
*   Goのコミット履歴と関連するコードレビュー
*   コンピュータアーキテクチャにおけるメモリのアライメントに関する一般的な情報
*   2のべき乗への丸め処理に関するビット演算のテクニック