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

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

このコミットは、Go言語のテストスイートに含まれるtest/chan/select5.goテストのコンパイル時間を短縮することを目的としています。機能的な変更は一切なく、テストの実行速度ではなく、テストコード自体のコンパイル効率を改善することで、全体のテスト実行時間を削減しています。

コミット

このコミットは、test/chan/select5.goテストにおいて、生成される関数の構造を変更することで、コンパイル時間を大幅に短縮しました。具体的には、テストケースごとに個別のinit関数を生成するように修正し、コンパイラ(当時の6g)が処理しやすいように最適化を施しています。これにより、テスト全体の実行時間が短縮され、特にリソースが限られた環境(例: Raspberry Pi)での効果が顕著でした。

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

https://github.com/golang/go/commit/63393faedf65b5b3719965cd4dead9a634be352f

元コミット内容

commit 63393faedf65b5b3719965cd4dead9a634be352f
Author: Josh Bleecher Snyder <josharian@gmail.com>
Date:   Tue Jun 17 09:07:18 2014 -0700

    test: speed up chan/select5
    
    No functional changes.
    
    Generating shorter functions improves compilation time. On my laptop, this test's running time goes from 5.5s to 1.5s; the wall clock time to run all tests goes down 1s. On Raspberry Pi, this CL cuts 50s off the wall clock time to run all tests.
    
    Fixes #7503.
    
    LGTM=bradfitz
    R=golang-codereviews, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/72590045

変更の背景

この変更の背景には、Go言語のテストスイート全体の実行時間の最適化という課題がありました。特にtest/chan/select5.goのような、多数のテストケースを動的に生成するタイプのテストは、生成されるコードが非常に大きくなる傾向がありました。当時のGoコンパイラ(6g)は、非常に大きな関数を最適化する際に多くの時間を要するという特性がありました。

コミットメッセージによると、このテストはラップトップで5.5秒から1.5秒に短縮され、全体のテスト実行時間も1秒短縮されました。さらに注目すべきは、Raspberry Piのような低スペックな環境では、この変更だけで全体のテスト実行時間が50秒も短縮されたという点です。これは、コンパイル時間の最適化が、特にリソースが限られた環境において、開発サイクルやCI/CDパイプラインの効率に大きな影響を与えることを示しています。

この変更は、GoのIssue #7503を修正するものであり、コンパイル時間の改善がコミュニティからの要望であったことが伺えます。

前提知識の解説

Go言語のinit()関数

Go言語において、init()関数は特別な関数です。各パッケージは複数のinit()関数を持つことができ、これらはパッケージがインポートされた際、またはプログラムの実行が開始される前に、自動的に(そして定義された順序で)実行されます。init()関数は、プログラムの起動時に必要な初期化処理(例: グローバル変数の設定、データベース接続の確立、設定ファイルの読み込みなど)を行うために使用されます。

Goコンパイラの最適化

Goコンパイラ(当時は6g、現在はgo tool compile)は、ソースコードを機械語に変換する際に様々な最適化を行います。これには、デッドコードの削除、インライン化、レジスタ割り当ての最適化などが含まれます。一般的に、コンパイラは小さな関数の方が最適化しやすく、大きな関数は解析と最適化に時間がかかる傾向があります。これは、関数のサイズが大きくなるほど、コンパイラが考慮すべきパスや状態が増加するためです。

selectステートメントとチャネル

Go言語のselectステートメントは、複数のチャネル操作(送受信)を同時に待機し、準備ができた最初の操作を実行するための強力な並行処理プリミティブです。selectは、デッドロックを回避し、複数の並行処理のフローを調整するために不可欠な要素です。test/chan/select5.goは、このselectステートメントとチャネルの様々な組み合わせをテストするためのものです。

go testコマンド

go testは、Go言語の標準的なテストツールです。ソースコード内のテスト関数(TestXxxBenchmarkXxxExampleXxxなど)を自動的に発見し、実行します。テストの実行時間やカバレッジレポートの生成など、Goプロジェクトの品質保証において中心的な役割を果たします。

6gコンパイラ

6gは、Go言語の初期のコンパイラの一つで、主にx86-64アーキテクチャをターゲットとしていました。Go 1.5以降、コンパイラはGo自身で書かれるようになり、go tool compileというコマンドに統合されましたが、このコミットが作成された2014年当時は6gが広く使われていました。このコンパイラの特性が、今回の変更の動機の一つとなっています。

技術的詳細

このコミットの技術的な核心は、「大きな関数を小さな関数に分割することで、コンパイラの最適化効率を向上させる」という点にあります。

元のtest/chan/select5.goでは、テストケースを動的に生成する際に、すべてのテストロジックを単一の大きなinit()関数内にまとめていました。これは、do関数内でfmt.Fprintln(out, func init() {)fmt.Fprintln(out, }がループの外側にあったためです。結果として、生成されるGoコードは、非常に巨大なinit()関数を一つ含む形になっていました。

// 元のコードの概念的な構造
func main() {
    // ...
    do := func(t *template.Template) {
        fmt.Fprintln(out, `func init() {`) // ループの外側
        for ; next(); a.reset() {
            run(t, a, out)
        }
        fmt.Fprintln(out, `}`) // ループの外側
    }
    // ...
}

このような巨大な関数は、当時の6gコンパイラにとって処理が重く、最適化に多大な時間を要していました。コンパイラは、関数の呼び出しグラフの構築、データフロー解析、レジスタ割り当てなど、多くの複雑な処理を行います。関数が大きくなればなるほど、これらの処理の計算量が増大し、コンパイル時間が長くなります。

このコミットでは、fmt.Fprintln(out, func init() {)fmt.Fprintln(out, }の行をループの内側に移動させました。

// 変更後のコードの概念的な構造
func main() {
    // ...
    do := func(t *template.Template) {
        for ; next(); a.reset() {
            fmt.Fprintln(out, `func init() {`) // ループの内側
            run(t, a, out)
            fmt.Fprintln(out, `}`) // ループの内側
        }
    }
    // ...
}

この変更により、run(t, a, out)が実行されるたびに、新しいinit()関数が生成されるようになりました。つまり、テストケースの数だけ個別のinit()関数が生成されることになります。

例えば、1000個のテストケースがある場合、

  • 変更前: 1つの巨大なinit()関数
  • 変更後: 1000個の小さなinit()関数

となります。個々のinit()関数は小さいため、コンパイラはそれぞれを迅速に解析し、最適化することができます。これにより、全体のコンパイル時間が大幅に短縮されるという効果が得られました。機能的な振る舞いは変わらないため、テストの正確性には影響を与えません。

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

test/chan/select5.goファイルにおける変更は以下の通りです。

--- a/test/chan/select5.go
+++ b/test/chan/select5.go
@@ -27,16 +27,16 @@ func main() {
 	fmt.Fprintln(out, header)
 	a := new(arg)
 
-	// Generate each kind of test as a separate function to avoid
+	// Generate each test as a separate function to avoid
 	// hitting the 6g optimizer with one enormous function.
 	// If we name all the functions init we don't have to
 	// maintain a list of which ones to run.
 	do := func(t *template.Template) {
-		fmt.Fprintln(out, `func init() {`)\n
 		for ; next(); a.reset() {
+\t\t\tfmt.Fprintln(out, `func init() {`)\n
 \t\t\trun(t, a, out)\n
+\t\t\tfmt.Fprintln(out, `}`)\n
 \t\t}\n
-\t\tfmt.Fprintln(out, `}`)\n
 	}\n
 
 	do(recv)\n

コアとなるコードの解説

変更の中心は、do関数内のfmt.Fprintln(out, ...)の呼び出し位置です。

  • 変更前:

    do := func(t *template.Template) {
        fmt.Fprintln(out, `func init() {`) // ループの外側で一度だけ出力
        for ; next(); a.reset() {
            run(t, a, out)
        }
        fmt.Fprintln(out, `}`) // ループの外側で一度だけ出力
    }
    

    このコードは、run(t, a, out)によって生成されるすべてのテストロジックを、単一のfunc init() { ... }ブロック内にまとめて出力していました。結果として、生成されるGoソースコードには、非常に巨大なinit関数が一つだけ存在することになります。

  • 変更後:

    do := func(t *template.Template) {
        for ; next(); a.reset() {
            fmt.Fprintln(out, `func init() {`) // ループの内側で各イテレーションごとに出力
            run(t, a, out)
            fmt.Fprintln(out, `}`) // ループの内側で各イテレーションごとに出力
        }
    }
    

    この修正により、next()が真を返すたびに(つまり、新しいテストケースが生成されるたびに)、func init() { ... }の開始と終了の行が出力されるようになりました。これにより、run(t, a, out)によって生成される各テストケースのコードが、それぞれ独立した小さなinit関数として出力されることになります。

この「大きな関数を小さな関数に分割する」という戦略は、コンパイラが個々の関数をより効率的に処理できるようにするためのものです。特に、当時の6gコンパイラの最適化器は、関数のサイズが大きいと処理に時間がかかるという特性があったため、この変更はコンパイル時間の劇的な短縮に繋がりました。機能的な振る舞いは変わらず、テストの網羅性や正確性は維持されたまま、開発効率が向上しました。

関連リンク

参考にした情報源リンク

  • Go言語のコミットメッセージ (上記「元コミット内容」に記載)
  • Go言語のinit関数に関する公式ドキュメントや解説記事 (一般的なGoの知識として参照)
  • Goコンパイラの最適化に関する一般的な情報 (一般的なGoの知識として参照)
  • Go言語のselectステートメントとチャネルに関する公式ドキュメントや解説記事 (一般的なGoの知識として参照)
  • Raspberry Piの性能に関する一般的な情報 (一般的なハードウェアの知識として参照)
  • Go Issue #7503 (GitHub上で確認可能)
  • Go CL 72590045 (Goのコードレビューシステム上で確認可能)
# [インデックス 19558] ファイルの概要

このコミットは、Go言語のテストスイートに含まれる`test/chan/select5.go`テストのコンパイル時間を短縮することを目的としています。機能的な変更は一切なく、テストの実行速度ではなく、テストコード自体のコンパイル効率を改善することで、全体のテスト実行時間を削減しています。

## コミット

このコミットは、`test/chan/select5.go`テストにおいて、生成される関数の構造を変更することで、コンパイル時間を大幅に短縮しました。具体的には、テストケースごとに個別の`init`関数を生成するように修正し、コンパイラ(当時の`6g`)が処理しやすいように最適化を施しています。これにより、テスト全体の実行時間が短縮され、特にリソースが限られた環境(例: Raspberry Pi)での効果が顕著でした。

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

[https://github.com/golang/go/commit/63393faedf65b5b3719965cd4dead9a634be352f](https://github.com/golang/go/commit/63393faedf65b5b3719965cd4dead9a634be352f)

## 元コミット内容

commit 63393faedf65b5b3719965cd4dead9a634be352f Author: Josh Bleecher Snyder josharian@gmail.com Date: Tue Jun 17 09:07:18 2014 -0700

test: speed up chan/select5

No functional changes.

Generating shorter functions improves compilation time. On my laptop, this test's running time goes from 5.5s to 1.5s; the wall clock time to run all tests goes down 1s. On Raspberry Pi, this CL cuts 50s off the wall clock time to run all tests.

Fixes #7503.

LGTM=bradfitz
R=golang-codereviews, bradfitz
CC=golang-codereviews
https://golang.org/cl/72590045

## 変更の背景

この変更の背景には、Go言語のテストスイート全体の実行時間の最適化という課題がありました。特に`test/chan/select5.go`のような、多数のテストケースを動的に生成するタイプのテストは、生成されるコードが非常に大きくなる傾向がありました。当時のGoコンパイラ(`6g`)は、非常に大きな関数を最適化する際に多くの時間を要するという特性がありました。

コミットメッセージによると、このテストはラップトップで5.5秒から1.5秒に短縮され、全体のテスト実行時間も1秒短縮されました。さらに注目すべきは、Raspberry Piのような低スペックな環境では、この変更だけで全体のテスト実行時間が50秒も短縮されたという点です。これは、コンパイル時間の最適化が、特にリソースが限られた環境において、開発サイクルやCI/CDパイプラインの効率に大きな影響を与えることを示しています。

この変更は、GoのIssue #7503を修正するものであり、コンパイル時間の改善がコミュニティからの要望であったことが伺えます。

## 前提知識の解説

### Go言語の`init()`関数

Go言語において、`init()`関数は特別な関数です。各パッケージは複数の`init()`関数を持つことができ、これらはパッケージがインポートされた際、またはプログラムの実行が開始される前に、自動的に(そして定義された順序で)実行されます。`init()`関数は、プログラムの起動時に必要な初期化処理(例: グローバル変数の設定、データベース接続の確立、設定ファイルの読み込みなど)を行うために使用されます。

### Goコンパイラの最適化

Goコンパイラ(当時は`6g`、現在は`go tool compile`)は、ソースコードを機械語に変換する際に様々な最適化を行います。これには、デッドコードの削除、インライン化、レジスタ割り当ての最適化などが含まれます。一般的に、コンパイラは小さな関数の方が最適化しやすく、大きな関数は解析と最適化に時間がかかる傾向があります。これは、関数のサイズが大きくなるほど、コンパイラが考慮すべきパスや状態が増加するためです。

### `select`ステートメントとチャネル

Go言語の`select`ステートメントは、複数のチャネル操作(送受信)を同時に待機し、準備ができた最初の操作を実行するための強力な並行処理プリミティブです。`select`は、デッドロックを回避し、複数の並行処理のフローを調整するために不可欠な要素です。`test/chan/select5.go`は、この`select`ステートメントとチャネルの様々な組み合わせをテストするためのものです。

### `go test`コマンド

`go test`は、Go言語の標準的なテストツールです。ソースコード内のテスト関数(`TestXxx`、`BenchmarkXxx`、`ExampleXxx`など)を自動的に発見し、実行します。テストの実行時間やカバレッジレポートの生成など、Goプロジェクトの品質保証において中心的な役割を果たします。

### `6g`コンパイラ

`6g`は、Go言語の初期のコンパイラの一つで、主にx86-64アーキテクチャをターゲットとしていました。Go 1.5以降、コンパイラはGo自身で書かれるようになり、`go tool compile`というコマンドに統合されましたが、このコミットが作成された2014年当時は`6g`が広く使われていました。このコンパイラの特性が、今回の変更の動機の一つとなっています。

## 技術的詳細

このコミットの技術的な核心は、「大きな関数を小さな関数に分割することで、コンパイラの最適化効率を向上させる」という点にあります。

元の`test/chan/select5.go`では、テストケースを動的に生成する際に、すべてのテストロジックを単一の大きな`init()`関数内にまとめていました。これは、`do`関数内で`fmt.Fprintln(out, func init() {)`と`fmt.Fprintln(out, }`がループの外側にあったためです。結果として、生成されるGoコードは、非常に巨大な`init()`関数を一つ含む形になっていました。

```go
// 元のコードの概念的な構造
func main() {
    // ...
    do := func(t *template.Template) {
        fmt.Fprintln(out, `func init() {`) // ループの外側
        for ; next(); a.reset() {
            run(t, a, out)
        }
        fmt.Fprintln(out, `}`) // ループの外側
    }
    // ...
}

このような巨大な関数は、当時の6gコンパイラにとって処理が重く、最適化に多大な時間を要していました。コンパイラは、関数の呼び出しグラフの構築、データフロー解析、レジスタ割り当てなど、多くの複雑な処理を行います。関数が大きくなればなるほど、これらの処理の計算量が増大し、コンパイル時間が長くなります。

このコミットでは、fmt.Fprintln(out, func init() {)fmt.Fprintln(out, }の行をループの内側に移動させました。

// 変更後のコードの概念的な構造
func main() {
    // ...
    do := func(t *template.Template) {
        for ; next(); a.reset() {
            fmt.Fprintln(out, `func init() {`) // ループの内側
            run(t, a, out)
            fmt.Fprintln(out, `}`) // ループの内側
        }
    }
    // ...
}

この変更により、run(t, a, out)が実行されるたびに、新しいinit()関数が生成されるようになりました。つまり、テストケースの数だけ個別のinit()関数が生成されることになります。

例えば、1000個のテストケースがある場合、

  • 変更前: 1つの巨大なinit()関数
  • 変更後: 1000個の小さなinit()関数

となります。個々のinit()関数は小さいため、コンパイラはそれぞれを迅速に解析し、最適化することができます。これにより、全体のコンパイル時間が大幅に短縮されるという効果が得られました。機能的な振る舞いは変わらないため、テストの正確性には影響を与えません。

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

test/chan/select5.goファイルにおける変更は以下の通りです。

--- a/test/chan/select5.go
+++ b/test/chan/select5.go
@@ -27,16 +27,16 @@ func main() {
 	fmt.Fprintln(out, header)
 	a := new(arg)
 
-	// Generate each kind of test as a separate function to avoid
+	// Generate each test as a separate function to avoid
 	// hitting the 6g optimizer with one enormous function.
 	// If we name all the functions init we don't have to
 	// maintain a list of which ones to run.
 	do := func(t *template.Template) {
-		fmt.Fprintln(out, `func init() {`)\n
 		for ; next(); a.reset() {
+\t\t\tfmt.Fprintln(out, `func init() {`)\n
 \t\t\trun(t, a, out)\n
+\t\t\tfmt.Fprintln(out, `}`)\n
 \t\t}\n
-\t\tfmt.Fprintln(out, `}`)\n
 	}\n
 
 	do(recv)\n

コアとなるコードの解説

変更の中心は、do関数内のfmt.Fprintln(out, ...)の呼び出し位置です。

  • 変更前:

    do := func(t *template.Template) {
        fmt.Fprintln(out, `func init() {`) // ループの外側で一度だけ出力
        for ; next(); a.reset() {
            run(t, a, out)
        }
        fmt.Fprintln(out, `}`) // ループの外側で一度だけ出力
    }
    

    このコードは、run(t, a, out)によって生成されるすべてのテストロジックを、単一のfunc init() { ... }ブロック内にまとめて出力していました。結果として、生成されるGoソースコードには、非常に巨大なinit関数が一つだけ存在することになります。

  • 変更後:

    do := func(t *template.Template) {
        for ; next(); a.reset() {
            fmt.Fprintln(out, `func init() {`) // ループの内側で各イテレーションごとに出力
            run(t, a, out)
            fmt.Fprintln(out, `}`) // ループの内側で各イテレーションごとに出力
        }
    }
    

    この修正により、next()が真を返すたびに(つまり、新しいテストケースが生成されるたびに)、func init() { ... }の開始と終了の行が出力されるようになりました。これにより、run(t, a, out)によって生成される各テストケースのコードが、それぞれ独立した小さなinit関数として出力されることになります。

この「大きな関数を小さな関数に分割する」という戦略は、コンパイラが個々の関数をより効率的に処理できるようにするためのものです。特に、当時の6gコンパイラの最適化器は、関数のサイズが大きいと処理に時間がかかるという特性があったため、この変更はコンパイル時間の劇的な短縮に繋がりました。機能的な振る舞いは変わらず、テストの網羅性や正確性は維持されたまま、開発効率が向上しました。

関連リンク

参考にした情報源リンク

  • Go言語のコミットメッセージ (上記「元コミット内容」に記載)
  • Go言語のinit関数に関する公式ドキュメントや解説記事 (一般的なGoの知識として参照)
  • Goコンパイラの最適化に関する一般的な情報 (一般的なGoの知識として参照)
  • Go言語のselectステートメントとチャネルに関する公式ドキュメントや解説記事 (一般的なGoの知識として参照)
  • Raspberry Piの性能に関する一般的な情報 (一般的なハードウェアの知識として参照)
  • Go Issue #7503 (GitHub上で確認可能)
  • Go CL 72590045 (Goのコードレビューシステム上で確認可能)