[インデックス 18900] ファイルの概要
このコミットは、Goランタイムにおけるスタック拡張(stack growth)のテストを追加し、同時にテストコードの可読性と管理性を向上させるためのリファクタリングを行っています。具体的には、自動生成された大量のテストコードを stack_test.go から stack_gen_test.go という新しいファイルに分離しています。
コミット
commit 8a908efd7462b4476d95a92498aef13c531cc8af
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Mar 19 17:22:56 2014 +0400
runtime: add stack growth tests
Also move generated code into a separate file,
because it's difficult to work with the file otherwise.
LGTM=khr
R=golang-codereviews, khr
CC=golang-codereviews
https://golang.org/cl/76080044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8a908efd7462b4476d95a92498aef13c531cc8af
元コミット内容
Goランタイムにスタック拡張のテストを追加し、生成されたコードを別のファイルに移動しました。これは、元のファイルでの作業を困難にしていたためです。
変更の背景
Go言語のランタイムは、ゴルーチン(goroutine)のスタックを動的に管理します。これは、プログラムの実行中に必要に応じてスタックサイズを自動的に拡張(grow)したり、縮小(shrink)したりする機能です。この動的なスタック管理は、固定サイズのスタックを持つ他の多くの言語と比較して、メモリ効率と並行処理の柔軟性において大きな利点をもたらします。
しかし、この動的なスタック拡張のメカニズムは非常に複雑であり、正確に機能することを保証するための堅牢なテストが不可欠です。特に、様々なスタックフレームサイズや呼び出し深度において、スタックが適切に拡張されることを検証する必要があります。
このコミット以前の stack_test.go ファイルには、スタック拡張のテストが含まれていましたが、そのテストコードの大部分は自動生成されたものでした。具体的には、異なるサイズのスタックフレームを持つ多数の関数(stack4, stack8, ..., stack5000 など)が手動で記述されており、これらが splitTests というスライスにまとめられていました。このような大量の繰り返しコードが単一のファイルに存在すると、ファイルの可読性が著しく低下し、手動でのメンテナンスが非常に困難になります。また、テストの追加や変更を行う際にも、誤って手動で生成された部分を編集してしまうリスクがありました。
このコミットの背景には、以下の2つの主要な動機があります。
- スタック拡張テストの強化: Goランタイムの重要な機能であるスタック拡張の信頼性をさらに高めるため、より包括的で多様なシナリオをカバーするテストを追加する必要がありました。
- テストコードの管理性向上: 自動生成された大量のテストコードが
stack_test.goに混在していることで生じるメンテナンス上の課題を解決し、開発者がより効率的にテストコードを扱えるようにすること。
これらの課題に対処するため、新しいスタック拡張テストを追加しつつ、既存の自動生成コードを別のファイルに分離するという方針が取られました。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびランタイムに関する前提知識が必要です。
1. Goルーチンとスタック
- Goルーチン (Goroutine): Go言語における軽量な並行処理の単位です。OSのスレッドよりもはるかに軽量で、数百万個のGoルーチンを同時に実行することも可能です。
- スタック (Stack): 関数呼び出しの際に、ローカル変数、関数引数、戻りアドレスなどが格納されるメモリ領域です。Goルーチンはそれぞれ独自のスタックを持っています。
2. Goの動的スタック管理 (Contiguous Stacks)
Goのランタイムは、Goルーチンのスタックを動的に管理します。これは「Contiguous Stacks(連続スタック)」または「Split Stacks(分割スタック)」と呼ばれることもあります。
- 初期スタックサイズ: Goルーチンが最初に作成される際、非常に小さなスタック(通常は数KB)が割り当てられます。
- スタックガード (Stack Guard): 各Goルーチンのスタックの末尾には「スタックガードページ」と呼ばれる特別な領域が設定されています。関数が呼び出され、スタックの使用量が増えてこのガードページに近づくと、ランタイムはスタックオーバーフローを検知します。
- スタック拡張 (Stack Growth): スタックガードページに到達すると、ランタイムは自動的にGoルーチンのスタックを拡張します。新しい、より大きなメモリ領域が割り当てられ、既存のスタックの内容が新しい領域にコピーされます。その後、実行は新しいスタック領域で継続されます。このプロセスは透過的に行われ、開発者が意識する必要はありません。
- スタック縮小 (Stack Shrinkage): 関数から戻るなどしてスタックの使用量が減り、スタックが過剰に大きいと判断された場合、ランタイムはスタックを縮小してメモリを解放することもあります。
この動的なスタック管理の利点は以下の通りです。
- メモリ効率: 必要な分だけスタックを割り当てるため、多数のGoルーチンを起動してもメモリ消費を抑えられます。固定サイズの大きなスタックを割り当てる必要がありません。
- プログラミングの容易さ: 開発者はスタックサイズを事前に見積もる必要がなく、再帰呼び出しなどスタックを深く使う処理も安心して記述できます。
3. runtime パッケージとテスト
runtimeパッケージ: Go言語のランタイムシステムを構成する低レベルな機能を提供します。ガベージコレクション、スケジューラ、メモリ管理、スタック管理などが含まれます。- テストコード: Goのテストは
testingパッケージを使用して記述されます。_test.goで終わるファイルにテスト関数を記述し、go testコマンドで実行します。 runtime_testパッケージ:runtimeパッケージの内部テストは、慣例としてruntime_testというパッケージ名で記述されます。これにより、テストコードがruntimeパッケージの内部実装にアクセスできるようになります。
4. go generate
go generate は、Goのソースコードから別のGoソースコードやその他のファイルを生成するためのツールです。特定のコメント行 (//go:generate ...) をソースファイル内に記述することで、そのコメントに続くコマンドを実行できます。このコミットでは、stack_gen_test.go の生成に go generate が使われていることが示唆されています。
技術的詳細
このコミットの技術的な核心は、Goランタイムのスタック拡張メカニズムのテスト方法の改善と、それに伴うテストコードの構造化です。
スタック拡張テストの原理
Goのスタック拡張は、関数が呼び出される際に、その関数が必要とするスタックフレームのサイズが現在のスタックの残量(スタックガードとの距離)と比較されることでトリガーされます。もし必要なスタックフレームがスタックガードを超えてしまう場合、ランタイムはスタックを拡張する処理(morestack ルーチンなど)を実行します。
TestStackSplit 関数は、このスタック拡張の挙動を検証するために設計されています。このテストは、様々なサイズのローカル変数(スタックフレームを消費する)を持つ関数を連続して呼び出すことで、意図的にスタック拡張を誘発します。
各テスト関数(例: stack4, stack8, ..., stack5000)は、特定のバイト数のローカル配列 buf を宣言し、その配列を使用 (use(buf[:])) することで、コンパイラがその配列を最適化で削除しないようにします。そして、Stackguard() 関数を呼び出して、現在のスタックポインタ(SP)とスタックガードのアドレスを取得します。テストは、sp が guard よりも大きい(つまり、スタックがガードページを超えていない)ことを検証します。もし sp が guard よりも小さい場合、それはスタック拡張が適切に行われなかったことを意味し、テストは失敗します。
生成コードの分離
元の stack_test.go ファイルには、stack4 から stack5000 までの、4バイト刻みでスタックフレームサイズが異なる関数が約1250個も手動で記述されていました。これらの関数は、以下のようなパターンで生成されていました。
func stackN()(uintptr, uintptr) { var buf [N]byte; use(buf[:]); return Stackguard() }
このような大量の定型的なコードが1つのファイルに存在すると、以下のような問題が発生します。
- 可読性の低下: ファイルが非常に長くなり、全体像を把握するのが困難になります。
- メンテナンスの困難さ: テストロジックの変更や新しいテストケースの追加が、大量の繰り返しコードの中に埋もれてしまい、作業が煩雑になります。
- レビューの負担: コードレビューの際に、本質的な変更と自動生成部分の差分を区別するのが難しくなります。
このコミットでは、これらの問題を解決するために、これらの自動生成された関数群を src/pkg/runtime/stack_gen_test.go という新しいファイルに移動しました。
stack_gen_test.go の先頭には、これらの関数を生成するための sed コマンドのヒントがコメントとして残されています。
// Edit .+1,$ | seq 4 4 5000 | sed 's/.*/func stack&()(uintptr, uintptr) { var buf [&]byte; use(buf[:]); return Stackguard() }/'
これは、go generate などのツールを使ってこれらの関数を自動生成できることを示唆しています。これにより、stack_test.go はテストの主要なロジックと、生成された関数への参照(splitTests スライス)のみを含むようになり、大幅に簡潔になりました。
StackGuard と StackLimit 定数
stack_test.go には、StackGuard と StackLimit という定数が定義されています。
const (
StackGuard = 256
StackLimit = 128
)
これらはGoランタイムのスタック管理における重要な閾値です。
StackGuard: スタックガードページのサイズ、またはスタックガードチェックが行われるオフセットを示します。この値は、スタックがどれだけ残っているかを判断するための基準となります。StackLimit: スタックの最小サイズ、またはスタック拡張が必要になる限界値を示します。
これらの定数は、スタック拡張テストの検証ロジックにおいて、スタックポインタが安全な範囲内にあることを確認するために使用されます。
use 関数
use 関数は、テスト関数内で宣言されたローカル変数(buf)がコンパイラによって最適化で削除されないようにするために使用されます。
var Used byte
func use(buf []byte) {
Used = buf[0]
}
buf[0] にアクセスし、その値をグローバル変数 Used に代入することで、コンパイラは buf が実際に使用されていると判断し、スタックフレームが確保されることを保証します。これにより、テストが意図した通りにスタック消費をシミュレートできるようになります。
コアとなるコードの変更箇所
このコミットにおける主要な変更は、以下の2つのファイルに集中しています。
-
src/pkg/runtime/stack_gen_test.go(新規ファイル)- このファイルが新しく作成されました。
splitTestsというfunc() (uintptr, uintptr)型の関数スライスが定義され、stack4からstack5000までの約1250個のテスト関数がこのスライスに格納されています。- これらの
stackN関数は、それぞれNバイトのローカル配列bufを宣言し、use(buf[:])でその配列が最適化されないようにし、Stackguard()を呼び出してスタックポインタとスタックガードの値を返します。
-
src/pkg/runtime/stack_test.go(変更)splitTestsスライスと、それに含まれる約1250個のstackN関数の定義がこのファイルから削除されました。StackGuardとStackLimit定数の定義が、ファイルの先頭付近に移動されました。use関数の定義はそのまま残されています。TestStackSplit関数は、splitTestsスライスをイテレートして各テスト関数を実行するロジックを保持しています。このスライスは、stack_gen_test.goで定義されたものがインポートされることで利用されます。
変更の概要は以下の通りです。
src/pkg/runtime/stack_gen_test.go: 1473行追加src/pkg/runtime/stack_test.go: 1623行追加, 1480行削除 (実質的には、生成コードの移動による大幅な削減)
コアとなるコードの解説
src/pkg/runtime/stack_gen_test.go
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package runtime_test
import (
. "runtime"
)
var splitTests = []func() (uintptr, uintptr){
// Edit .+1,/^}/-1|seq 4 4 5000 | sed 's/.*/\tstack&,/' | fmt
stack4, stack8, stack12, stack16, stack20, stack24, stack28,
// ... (中略) ...
stack4996, stack5000,
}
// Edit .+1,$ | seq 4 4 5000 | sed 's/.*/func stack&()(uintptr, uintptr) { var buf [&]byte; use(buf[:]); return Stackguard() }/'
func stack4() (uintptr, uintptr) { var buf [4]byte; use(buf[:]); return Stackguard() }
func stack8() (uintptr, uintptr) { var buf [8]byte; use(buf[:]); return Stackguard() }
func stack12() (uintptr, uintptr) { var buf [12]byte; use(buf[:]); return Stackguard() }
// ... (中略) ...
func stack4996() (uintptr, uintptr) { var buf [4996]byte; use(buf[:]); return Stackguard() }
func stack5000() (uintptr, uintptr) { var buf [5000]byte; use(buf[:]); return Stackguard() }
このファイルは、Goのスタック拡張テストのために自動生成された大量の関数を格納しています。
package runtime_test:runtimeパッケージのテストであることを示します。import . "runtime":runtimeパッケージの要素を修飾子なしで直接使用できるようにします(例:Stackguard())。var splitTests = []func() (uintptr, uintptr){ ... }:splitTestsは、スタックサイズをテストするための関数を保持するスライスです。各関数はuintptr型の2つの値を返します。1つは現在のスタックポインタ、もう1つはスタックガードのアドレスです。stackN()関数群:stack4からstack5000まで、4バイト刻みで定義された関数です。var buf [N]byte:Nバイトのローカル配列を宣言し、スタック上にNバイトを割り当てます。use(buf[:]):bufがコンパイラによって最適化で削除されないようにします。return Stackguard():runtimeパッケージのStackguard()関数を呼び出し、現在のスタックポインタとスタックガードのアドレスを返します。これにより、テストはスタックが適切に拡張されたかどうかを検証できます。
ファイルの先頭と stackN 関数群の定義の直前にあるコメントは、これらの関数がどのように生成されたかを示すヒントです。seq 4 4 5000 は4から5000まで4刻みの数列を生成し、sed コマンドでその数列を使って関数定義の文字列を生成していることを示唆しています。
src/pkg/runtime/stack_test.go
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package runtime_test
import (
. "runtime"
"sync" // 新規追加
"testing"
"time"
"unsafe"
)
// See stack.h.
const (
StackGuard = 256
StackLimit = 128
)
func TestStackSplit(t *testing.T) {
for _, f := range splitTests { // splitTests は stack_gen_test.go からインポートされる
sp, guard := f()
if sp < guard {
t.Fatalf("after %s: sp=%#x < limit=%#x", funcname(f), sp, guard)
}
}
}
var Used byte
func use(buf []byte) {
Used = buf[0]
}
// ... (その他のテスト関数) ...
このファイルは、スタック拡張テストのメインロジックと、その他の関連テストを保持しています。
package runtime_testとimport . "runtime"はstack_gen_test.goと同様です。import "sync": このコミットで追加されたsyncパッケージのインポートは、おそらく他のテスト関数(このコミットの差分には含まれていないが、元のファイルに存在したか、後続のコミットで追加されるもの)で使用されるためです。const (StackGuard = 256; StackLimit = 128): スタックガードとスタック制限の定数定義です。これらはGoランタイムのスタック管理の内部的な詳細を反映しています。func TestStackSplit(t *testing.T):for _, f := range splitTests:stack_gen_test.goで定義されたsplitTestsスライスをイテレートし、各テスト関数fを実行します。sp, guard := f(): 各stackN関数が返すスタックポインタspとスタックガードguardの値を取得します。if sp < guard: この条件は、スタックポインタがスタックガードのアドレスよりも小さい場合に真となります。これは、スタックがガードページを超えてしまい、スタック拡張が適切に行われなかったことを意味します。t.Fatalf(...): テストが失敗した場合にエラーメッセージを出力し、テストを終了します。funcname(f)は、実行中のテスト関数の名前を取得するためのヘルパー関数です。
この変更により、stack_test.go はテストのフレームワークと検証ロジックに集中し、大量の生成コードは stack_gen_test.go に分離されたため、両ファイルの可読性と保守性が大幅に向上しました。
関連リンク
- Go言語のスタック管理に関する公式ドキュメントやブログ記事:
- The Go Programming Language Specification - Goroutines: https://go.dev/ref/spec#Goroutines
- Go's work-stealing scheduler: https://go.dev/blog/go15runtime (Go 1.5以降のスケジューラに関する記事ですが、スタック管理の文脈で関連します)
- Goのランタイムソースコード:
src/runtime/stack.go: Goランタイムのスタック管理に関する主要な実装ファイル。src/runtime/stack.h: スタック関連の定数や構造体が定義されているヘッダーファイル(C言語部分)。
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード (golang/go GitHubリポジトリ)
- Go言語のランタイムに関する技術ブログや解説記事 (一般的な知識として)