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

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

このコミットは、Go言語のランタイムパッケージ内のテストファイル src/pkg/runtime/proc_test.go に変更を加えています。このファイルは、Goランタイムのプロセス管理やスケジューリングに関連する機能のベンチマークテストを記述するために使用されます。具体的には、ゴルーチンの作成とスケジューリングのパフォーマンスを測定するための新しいベンチマークが追加されています。

コミット

このコミットは、Goランタイムにおけるゴルーチン作成のベンチマークを追加します。これにより、ゴルーチンの生成コストと並行実行時のパフォーマンス特性を測定できるようになります。コミットメッセージには、2コアのdarwin/amd64環境でのベンチマーク結果の例も含まれており、BenchmarkGoroutineChainBenchmarkConcGoroutineChain の両方で、ゴルーチン作成の効率が示されています。

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

https://github.com/golang/go/commit/5a5e698c8fceec38c34f86375dcd44fb1a7a8939

元コミット内容

commit 5a5e698c8fceec38c34f86375dcd44fb1a7a8939
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed Jun 27 21:57:49 2012 +0400

    runtime: add goroutine creation benchmark
    Current results on 2 core darwin/amd64:
    BenchmarkGoroutineChain         351 ns/op
    BenchmarkGoroutineChain-2       3840 ns/op
    BenchmarkGoroutineChain-4       4040 ns/op
    BenchmarkConcGoroutineChain     350 ns/op
    BenchmarkConcGoroutineChain-2   875 ns/op
    BenchmarkConcGoroutineChain-4   2027 ns/op
    
    R=r, rsc
    CC=golang-dev
    https://golang.org/cl/6332054
---
 src/pkg/runtime/proc_test.go | 26 ++++++++++++++++++++++++++\n 1 file changed, 26 insertions(+)\n

変更の背景

Go言語は、その並行処理モデルの中核として「ゴルーチン(goroutine)」を提供しています。ゴルーチンは軽量なスレッドのようなもので、数百万単位で作成してもシステムリソースを効率的に利用できるように設計されています。しかし、どんなに軽量であっても、ゴルーチンの作成にはオーバーヘッドが伴います。

このコミットの背景には、Goランタイムのパフォーマンス最適化への継続的な取り組みがあります。ゴルーチンの作成コストを正確に測定し、そのパフォーマンス特性を理解することは、ランタイムの改善、スケジューラの最適化、そしてGoアプリケーション全体の応答性とスループット向上に不可欠です。特に、多数のゴルーチンを生成するようなアプリケーション(例:ネットワークサービス、並行処理フレームワーク)では、このコストがボトルネックになる可能性があります。

ベンチマークを追加することで、開発者は以下のことを実現できます。

  • パフォーマンスの回帰テスト: 将来のランタイム変更がゴルーチン作成のパフォーマンスに悪影響を与えないことを確認する。
  • 最適化の評価: ゴルーチン作成パスの改善が実際にパフォーマンス向上につながるかを定量的に評価する。
  • 特性の理解: 特定の環境(OS、アーキテクチャ、CPUコア数)におけるゴルーチン作成の挙動を深く理解する。

前提知識の解説

ゴルーチン (Goroutine)

Go言語におけるゴルーチンは、軽量な並行実行単位です。OSのスレッドよりもはるかに軽量であり、数千から数百万のゴルーチンを同時に実行しても、メモリ消費やコンテキストスイッチのオーバーヘッドが少ないように設計されています。go キーワードを使って関数呼び出しの前に置くことで、新しいゴルーチンが作成され、その関数が並行して実行されます。

runtime.GOMAXPROCS

runtime.GOMAXPROCS は、Goスケジューラが同時に実行できるOSスレッドの最大数を設定する関数です。デフォルトでは、CPUの論理コア数に設定されます。この値は、Goプログラムが利用できる並行性の度合いに影響を与えます。ベンチマークでは、runtime.GOMAXPROCS(-1) を使用して現在の GOMAXPROCS の値を取得し、それに応じて並行テストの数を調整しています。

testing パッケージとベンチマーク

Go言語の標準ライブラリには、テストとベンチマークのための testing パッケージが含まれています。

  • ベンチマーク関数: Benchmark というプレフィックスで始まる関数は、Goのテストツールによってベンチマークとして認識されます。これらの関数は *testing.B 型の引数を取ります。
  • *testing.B: ベンチマーク実行のためのコンテキストを提供します。
    • b.N: ベンチマーク関数が実行されるイテレーション回数を示します。Goのテストツールは、安定した測定結果が得られるように b.N の値を自動的に調整します。
    • b.ResetTimer(): タイマーをリセットし、それ以前の処理時間を測定から除外します。
    • b.StopTimer(): タイマーを停止します。
    • b.StartTimer(): タイマーを再開します。
    • b.RunParallel(func(pb *testing.PB)): ベンチマークを並行して実行するためのヘルパー関数です。複数のゴルーチンが同時にベンチマークコードを実行します。

チャネル (Channel)

チャネルは、ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは、Goにおける並行処理の同期と通信の主要な手段であり、デッドロックやデータ競合を防ぐのに役立ちます。このベンチマークでは、ゴルーチンが完了したことを親ゴルーチンに通知するためにチャネルが使用されています。

技術的詳細

このコミットで追加されたベンチマークは、ゴルーチンを連鎖的に作成し、その完了をチャネルで待機するというパターンを使用しています。これにより、単一のゴルーチン作成コストだけでなく、多数のゴルーチンが相互に依存しながら作成されるシナリオでのパフォーマンスを測定できます。

主要な関数は benchmarkCreateGoroutines で、これは b *testing.Bprocs int を引数に取ります。

  • procs 引数は、ベンチマークを並行して実行するゴルーチンの数を制御します。BenchmarkCreateGoroutinesprocs=1 で実行され、単一スレッドでのゴルーチン作成コストを測定します。BenchmarkCreateGoroutinesParallelruntime.GOMAXPROCS(-1) で取得した値(通常はCPUコア数)を procs に渡し、複数のOSスレッドを利用した並行実行でのゴルーチン作成コストを測定します。
  • c := make(chan bool): 完了通知用のチャネルを作成します。
  • var f func(n int): 再帰的なゴルーチン作成のための関数 f を宣言します。
  • f = func(n int) { ... }:
    • if n == 0: ベースケースです。n が0になったら、チャネル ctrue を送信して完了を通知します。
    • go f(n - 1): 再帰的に新しいゴルーチンを作成し、n-1f を呼び出します。これにより、n 個のゴルーチンが連鎖的に作成されます。
  • for i := 0; i < procs; i++ { go f(b.N / procs) }: procs の数だけゴルーチンを起動し、それぞれが b.N / procs 回のゴルーチン連鎖作成を実行します。b.N はベンチマークのイテレーション回数であり、各並行ゴルーチンに均等に割り振られます。
  • for i := 0; i < procs; i++ { <-c }: procs の数だけチャネル c から値を受信し、すべてのゴルーチン連鎖が完了するのを待ちます。これにより、ベンチマークの測定範囲がすべてのゴルーチン作成と完了処理を含むことが保証されます。

このベンチマークは、ゴルーチンが作成され、実行され、そして終了するまでの一連のライフサイクル全体にかかる時間を測定します。特に、再帰的なゴルーチン作成は、コールスタックの深さやゴルーチン間の連携がパフォーマンスに与える影響を評価するのに役立ちます。

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

diff --git a/src/pkg/runtime/proc_test.go b/src/pkg/runtime/proc_test.go
index 32111080a5..1d51c5271e 100644
--- a/src/pkg/runtime/proc_test.go
+++ b/src/pkg/runtime/proc_test.go
@@ -123,3 +123,29 @@ func BenchmarkSyscallWork(b *testing.B) {
 		<-c
 	}
 }\n+\n+func BenchmarkCreateGoroutines(b *testing.B) {\n+\tbenchmarkCreateGoroutines(b, 1)\n+}\n+\n+func BenchmarkCreateGoroutinesParallel(b *testing.B) {\n+\tbenchmarkCreateGoroutines(b, runtime.GOMAXPROCS(-1))\n+}\n+\n+func benchmarkCreateGoroutines(b *testing.B, procs int) {\n+\tc := make(chan bool)\n+\tvar f func(n int)\n+\tf = func(n int) {\n+\t\tif n == 0 {\n+\t\t\tc <- true\n+\t\t\treturn\n+\t\t}\n+\t\tgo f(n - 1)\n+\t}\n+\tfor i := 0; i < procs; i++ {\n+\t\tgo f(b.N / procs)\n+\t}\n+\tfor i := 0; i < procs; i++ {\n+\t\t<-c\n+\t}\n+}\n

コアとなるコードの解説

追加されたコードは、以下の3つの関数で構成されています。

  1. BenchmarkCreateGoroutines(b *testing.B):

    • この関数は、Goのベンチマークツールによって自動的に実行されます。
    • benchmarkCreateGoroutines(b, 1) を呼び出しています。これは、ゴルーチン作成のベンチマークを単一のプロセッサ(または論理コア)上で実行することを意味します。これにより、ゴルーチン作成の基本的なオーバーヘッドを測定できます。
  2. BenchmarkCreateGoroutinesParallel(b *testing.B):

    • この関数もベンチマークツールによって実行されます。
    • benchmarkCreateGoroutines(b, runtime.GOMAXPROCS(-1)) を呼び出しています。runtime.GOMAXPROCS(-1) は現在の GOMAXPROCS の値(通常はCPUの論理コア数)を返します。これにより、利用可能なすべてのCPUコアを使って並行にゴルーチンを作成するシナリオでのパフォーマンスを測定します。これは、実際のマルチコア環境でのGoアプリケーションの挙動をより正確に反映します。
  3. benchmarkCreateGoroutines(b *testing.B, procs int):

    • この関数が実際のベンチマークロジックを含んでいます。
    • c := make(chan bool): ゴルーチンが完了したことを通知するためのチャネルを作成します。
    • var f func(n int): 再帰的にゴルーチンを作成するヘルパー関数 f を宣言します。
    • f = func(n int) { ... }:
      • if n == 0: 再帰の終了条件です。n が0になったら、現在のゴルーチンは完了し、チャネル ctrue を送信して親ゴルーチンに通知します。
      • go f(n - 1): 新しいゴルーチンを起動し、fn-1 で再帰的に呼び出します。これにより、n 個のゴルーチンが連鎖的に作成されます。例えば、f(3) を呼び出すと、f(2) を実行するゴルーチン、f(1) を実行するゴルーチン、f(0) を実行するゴルーチンが順に作成されます。
    • for i := 0; i < procs; i++ { go f(b.N / procs) }:
      • procs の数だけゴルーチンを起動します。
      • 各ゴルーチンは f(b.N / procs) を呼び出します。b.N はベンチマークのイテレーション回数であり、Goのテストフレームワークによって自動的に調整されます。b.N / procs は、各並行実行パスで作成されるゴルーチンの総数を制御します。
    • for i := 0; i < procs; i++ { <-c }:
      • procs の数だけチャネル c から値を受信します。これにより、すべての連鎖的なゴルーチン作成と完了が測定範囲に含まれることが保証され、ベンチマークがすべてのゴルーチンが終了するまで待機します。

このベンチマークは、ゴルーチンの作成、スケジューリング、および終了にかかる平均時間をナノ秒単位で測定することを目的としています。特に、再帰的な呼び出しパターンは、ゴルーチンが多数生成され、かつ相互に依存するようなシナリオをシミュレートしており、Goランタイムの効率性を評価する上で非常に有用です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント: https://go.dev/
  • Goの testing パッケージに関するドキュメント: https://pkg.go.dev/testing
  • Goの runtime パッケージに関するドキュメント: https://pkg.go.dev/runtime
  • Goにおけるベンチマークの書き方に関する一般的な情報源 (例: "Writing benchmarks in Go" で検索)
  • Goにおけるゴルーチンとチャネルに関する一般的な情報源 (例: "Go Concurrency", "Go Goroutines and Channels" で検索)