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

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

このコミットは、Go言語のランタイムパッケージ内のチャネル(chan)に関するベンチマークテストを追加・改善するものです。具体的には、src/pkg/runtime/chan_test.go ファイルに、非ブロッキング受信、チャネルベースのセマフォ、およびselect文を用いたプロデューサー・コンシューマーパターンに関する新たなベンチマークが追加されています。これにより、Goランタイムにおけるチャネル操作のパフォーマンス特性をより詳細に測定し、最適化の機会を特定することが可能になります。

コミット

commit 3baceaa1519baada9f040c14b7f36e89a6c83144
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue Feb 4 09:41:48 2014 +0400

    runtime: add more chan benchmarks
    Add benchmarks for:
    1. non-blocking failing receive (polling of "stop" chan)
    2. channel-based semaphore (gate pattern)
    3. select-based producer/consumer (pass data through a channel, but also wait on "stop" and "timeout" channels)
    
    LGTM=r
    R=golang-codereviews, r
    CC=bradfitz, golang-codereviews, iant, khr
    https://golang.org/cl/59040043

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

https://github.com/golang/go/commit/3baceaa1519baada9f040c14b7f36e89a6c83144

元コミット内容

このコミットの元の内容は、Goランタイムにおけるチャネルのベンチマークを拡充することです。具体的には、以下の3つのシナリオに対するベンチマークが追加されました。

  1. 非ブロッキング受信("stop"チャネルのポーリング): select文のdefaultケースを利用した、チャネルからの非ブロッキング受信のパフォーマンス測定。これは、チャネルにデータがない場合にブロックせずに即座に処理を継続するパターンを評価します。
  2. チャネルベースのセマフォ(ゲートパターン): バッファ付きチャネルをセマフォとして利用する際のパフォーマンス測定。これは、並行処理におけるリソースアクセス制御の一般的なパターンです。
  3. selectベースのプロデューサー・コンシューマー: データをチャネル経由で受け渡しつつ、同時に"stop"チャネルや"timeout"チャネルも監視するselect文を用いたプロデューサー・コンシューマーパターンのパフォーマンス測定。これは、複数のイベントソースを同時に扱う複雑な並行処理シナリオを評価します。

変更の背景

Go言語のチャネルは、ゴルーチン間の安全な通信と同期のための基本的なプリミティブです。その効率性は、Goプログラム全体のパフォーマンスに直接影響します。既存のベンチマークだけでは、チャネルの様々な利用パターンにおけるパフォーマンス特性を完全に把握するには不十分でした。

このコミットの背景には、以下のような目的があったと考えられます。

  • パフォーマンスのボトルネック特定: 特定のチャネル利用パターン(特にselect文の複雑な使用や非ブロッキング操作)が、ランタイムにどの程度のオーバーヘッドをもたらすかを正確に測定し、潜在的なパフォーマンスボトルネックを特定するため。
  • ランタイムの最適化: ベンチマークの結果に基づいて、チャネルの実装やスケジューラの動作をさらに最適化するためのデータ駆動型のアプローチを可能にするため。
  • Go言語の並行処理モデルの理解深化: 開発者やランタイムエンジニアが、Goの並行処理モデル、特にチャネルとselect文の挙動とパフォーマンスをより深く理解するための具体的な測定基準を提供するため。
  • 回帰テストの強化: 将来のランタイム変更がチャネルのパフォーマンスに悪影響を与えないことを確認するための、より堅牢な回帰テストスイートを構築するため。

特に、select文は複数のチャネル操作を同時に待機できる強力な機能ですが、その内部実装は複雑であり、ケースの数やチャネルの状態によってパフォーマンスが変動する可能性があります。非ブロッキング操作やセマフォパターンもまた、Goプログラムで頻繁に用いられるイディオムであり、その効率性は重要です。これらのシナリオを網羅的にベンチマークすることで、Goランタイムの堅牢性とパフォーマンスを向上させるための貴重な洞察が得られます。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語の概念とベンチマークに関する知識が前提となります。

1. Go言語のチャネル (Channels)

チャネルは、Go言語におけるゴルーチン間の通信と同期のための主要なメカニズムです。型付けされており、make(chan Type)で作成されます。

  • 送信 (Send): ch <- value でチャネルに値を送信します。
  • 受信 (Receive): value := <-ch でチャネルから値を受信します。
  • バッファ付きチャネル: make(chan Type, capacity) で作成され、指定された容量まで値をバッファできます。バッファが満杯でない限り送信はブロックせず、バッファが空でない限り受信はブロックしません。
  • バッファなしチャネル: make(chan Type) で作成され、送信と受信が同時に行われるまでブロックします(同期チャネル)。

2. select

select文は、複数のチャネル操作を同時に待機し、準備ができた最初の操作を実行するために使用されます。

select {
case <-ch1:
    // ch1 から受信できた場合の処理
case ch2 <- value:
    // ch2 に送信できた場合の処理
default:
    // どのチャネル操作も準備ができていない場合の処理(非ブロッキング)
}
  • defaultケースが存在する場合、どのcaseも即座に実行できない場合でもselect文はブロックしません。これは非ブロッキング操作やポーリングに利用されます。
  • defaultケースがない場合、いずれかのcaseが準備できるまでselect文はブロックします。

3. Go言語のベンチマーク (Benchmarking)

Go言語には、標準ライブラリにベンチマーク機能が組み込まれています。

  • testingパッケージ: ベンチマークはtestingパッケージの一部として提供されます。
  • BenchmarkXxx関数: func BenchmarkXxx(b *testing.B)というシグネチャを持つ関数がベンチマーク関数として認識されます。
  • b.N: ベンチマーク関数内で、操作をb.N回繰り返すことで、その操作の平均実行時間を測定します。b.Nの値は、ベンチマーク実行時に自動的に調整され、統計的に有意な結果が得られるようにします。
  • go test -bench=.: コマンドラインからgo test -bench=.を実行することで、プロジェクト内のすべてのベンチマークを実行できます。

4. runtime.GOMAXPROCS

runtime.GOMAXPROCSは、Goスケジューラが同時に実行できるOSスレッドの最大数を設定します。この値は、並行処理のベンチマークにおいて、ゴルーチンのスケジューリングとコンテキストスイッチのオーバーヘッドを評価する上で重要です。runtime.GOMAXPROCS(-1)は現在の設定値を返します。

5. sync/atomicパッケージ

sync/atomicパッケージは、低レベルのアトミック(不可分)な操作を提供します。これにより、ミューテックスなどのロックを使用せずに、共有変数への安全なアクセスが可能になります。

  • atomic.AddInt32: int32型の変数にアトミックに値を加算します。このコミットでは、ベンチマークの繰り返し回数を複数のゴルーチンで安全にカウントダウンするために使用されています。

6. セマフォ (Semaphore)

セマフォは、並行プログラミングにおける同期プリミティブの一つで、共有リソースへのアクセスを制御するために使用されます。Goでは、バッファ付きチャネルを使ってセマフォを実装するのが一般的なイディオムです。チャネルのバッファサイズがセマフォのカウンタとなり、チャネルへの送信がacquire(リソース獲得)、チャネルからの受信がrelease(リソース解放)に対応します。

7. プロデューサー・コンシューマーパターン (Producer-Consumer Pattern)

プロデューサー・コンシューマーパターンは、並行処理における一般的なデザインパターンです。プロデューサーがデータを生成し、コンシューマーがそのデータを消費します。通常、両者はキュー(Goではチャネル)を介して通信し、互いに独立して動作できます。

技術的詳細

このコミットでは、src/pkg/runtime/chan_test.goファイルに以下の3つの新しいベンチマーク関数が追加されています。

1. BenchmarkChanNonblocking

このベンチマークは、チャネルからの非ブロッキング受信のパフォーマンスを測定します。

  • 目的: select文のdefaultケースを使用して、チャネルにデータがない場合にブロックせずに即座に処理を継続するシナリオのオーバーヘッドを評価します。これは、イベントループで複数のチャネルをポーリングするような場合に重要です。
  • 実装:
    • myc := make(chan int): バッファなしチャネルを作成します。このチャネルにはデータが送信されないため、常に受信は失敗します。
    • select { case <-myc: default: }: 各ゴルーチン内で、このselect文がCallsPerSched回繰り返されます。mycからの受信は常に失敗するため、defaultケースが実行されます。
    • 複数のゴルーチン(procsの数だけ)が並行してこの非ブロッキング受信操作を実行し、atomic.AddInt32(&N, -1)で全体の繰り返し回数を管理します。
  • 評価ポイント: select文がdefaultケースにフォールバックする際のオーバーヘッド、およびチャネルが空である場合の受信操作のコスト。

2. BenchmarkSelectProdCons

このベンチマークは、select文を用いたプロデューサー・コンシューマーパターンのパフォーマンスを測定します。

  • 目的: 複数のチャネル(データチャネル、タイムアウトチャネル、終了チャネル)をselect文で同時に監視する、より複雑な並行処理シナリオのオーバーヘッドを評価します。これは、リアルタイムシステムやイベント駆動型アプリケーションでよく見られるパターンです。
  • 実装:
    • myc := make(chan int, 128): プロデューサーとコンシューマーがデータをやり取りするためのバッファ付きチャネル。
    • myclose := make(chan bool): 終了シグナルを送るためのチャネル。
    • mytimer := time.After(time.Hour): タイムアウトをシミュレートするためのチャネル。ベンチマーク中は発火しないように長い時間が設定されています。
    • procsに対して、1つのプロデューサーゴルーチンと1つのコンシューマーゴルーチンが起動されます。
    • プロデューサー: myc <- 1でデータを送信し、同時にmytimermycloseチャネルもselectで監視します。データ送信の間に簡単な計算(foo *= 2; foo /= 2)で「ローカルな作業」をシミュレートします。
    • コンシューマー: <-mycでデータを受信し、同時にmytimermycloseチャネルもselectで監視します。データ受信の間に簡単な計算で「ローカルな作業」をシミュレートします。
  • 評価ポイント: select文が複数のチャネルを監視する際のオーバーヘッド、特にデータチャネルが準備できた場合のパスと、他のチャネルが準備できた場合のパスのパフォーマンス。また、プロデューサーとコンシューマー間の同期とデータ転送の効率。

3. BenchmarkChanSem

このベンチマークは、チャネルベースのセマフォのパフォーマンスを測定します。既存のBenchmarkChanSem関数がより現実的なセマフォ利用パターンを反映するように修正されています。

  • 目的: バッファ付きチャネルをセマフォとして使用する際の、リソースの獲得(acquire)と解放(release)操作のオーバーヘッドを評価します。これは、並行アクセスを制限する必要がある場合に広く使用されるパターンです。
  • 実装:
    • myc := make(chan Empty, procs): procsの容量を持つバッファ付きチャネルをセマフォとして使用します。Empty構造体は、メモリを消費しないプレースホルダーとして使用されます。
    • procsの数だけゴルーチンが起動されます。
    • 各ゴルーチンは、myc <- Empty{}でセマフォを獲得し、<-mycでセマフォを解放します。これにより、procs個のリソースを同時に利用できるセマフォがシミュレートされます。
    • 各ゴルーチンはCallsPerSched回、セマフォの獲得と解放を繰り返します。
  • 評価ポイント: バッファ付きチャネルへの送信と受信が、並行環境下でセマフォとして機能する際のパフォーマンス。特に、チャネルのバッファが満杯または空の場合のブロックとアンブロックの効率。

これらのベンチマークは、Goランタイムのチャネル実装の様々な側面を深く掘り下げ、そのパフォーマンス特性を詳細に分析するための重要なツールとなります。

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

変更はすべて src/pkg/runtime/chan_test.go ファイル内で行われています。

--- a/src/pkg/runtime/chan_test.go
+++ b/src/pkg/runtime/chan_test.go
@@ -430,6 +430,30 @@ func TestMultiConsumer(t *testing.T) {
 	}\n
 }\n
 
+func BenchmarkChanNonblocking(b *testing.B) {
+	const CallsPerSched = 1000
+	procs := runtime.GOMAXPROCS(-1)
+	N := int32(b.N / CallsPerSched)
+	c := make(chan bool, procs)
+	myc := make(chan int)
+	for p := 0; p < procs; p++ {
+		go func() {
+			for atomic.AddInt32(&N, -1) >= 0 {
+				for g := 0; g < CallsPerSched; g++ {
+					select {
+					case <-myc:
+					default:
+					}
+				}
+			}
+			c <- true
+		}()
+	}
+	for p := 0; p < procs; p++ {
+		<-c
+	}
+}
+
 func BenchmarkSelectUncontended(b *testing.B) {
 	const CallsPerSched = 1000
 	procs := runtime.GOMAXPROCS(-1)
@@ -670,6 +694,66 @@ func BenchmarkChanProdConsWork100(b *testing.B) {
 	benchmarkChanProdCons(b, 100, 100)
 }\n
 
+func BenchmarkSelectProdCons(b *testing.B) {
+	const CallsPerSched = 1000
+	procs := runtime.GOMAXPROCS(-1)
+	N := int32(b.N / CallsPerSched)
+	c := make(chan bool, 2*procs)
+	myc := make(chan int, 128)
+	myclose := make(chan bool)
+	for p := 0; p < procs; p++ {
+		go func() {
+			// Producer: sends to myc.
+			foo := 0
+			// Intended to not fire during benchmarking.
+			mytimer := time.After(time.Hour)
+			for atomic.AddInt32(&N, -1) >= 0 {
+				for g := 0; g < CallsPerSched; g++ {
+					// Model some local work.
+					for i := 0; i < 100; i++ {
+						foo *= 2
+						foo /= 2
+					}
+					select {
+					case myc <- 1:
+					case <-mytimer:
+					case <-myclose:
+					}
+				}
+			}
+			myc <- 0
+			c <- foo == 42
+		}()
+		go func() {
+			// Consumer: receives from myc.
+			foo := 0
+			// Intended to not fire during benchmarking.
+			mytimer := time.After(time.Hour)
+		loop:
+			for {
+				select {
+				case v := <-myc:
+					if v == 0 {
+						break loop
+					}
+				case <-mytimer:
+				case <-myclose:
+				}
+				// Model some local work.
+				for i := 0; i < 100; i++ {
+					foo *= 2
+					foo /= 2
+				}
+			}
+			c <- foo == 42
+		}()
+	}
+	for p := 0; p < procs; p++ {
+		<-c
+		<-c
+	}
+}
+
 func BenchmarkChanCreation(b *testing.B) {
 	const CallsPerSched = 1000
 	procs := runtime.GOMAXPROCS(-1)
@@ -694,9 +778,23 @@ func BenchmarkChanCreation(b *testing.B) {
 
 func BenchmarkChanSem(b *testing.B) {
 	type Empty struct{}
-	c := make(chan Empty, 1)
-	for i := 0; i < b.N; i++ {
-		c <- Empty{}
+	const CallsPerSched = 1000
+	procs := runtime.GOMAXPROCS(0)
+	N := int32(b.N / CallsPerSched)
+	c := make(chan bool, procs)
+	myc := make(chan Empty, procs)
+	for p := 0; p < procs; p++ {
+		go func() {
+			for atomic.AddInt32(&N, -1) >= 0 {
+				for g := 0; g < CallsPerSched; g++ {
+					myc <- Empty{}
+					<-myc
+				}
+			}
+			c <- true
+		}()
+	}
+	for p := 0; p < procs; p++ {
 		<-c
 	}
 }

コアとなるコードの解説

BenchmarkChanNonblocking

この関数は、select文のdefaultケースを利用した非ブロッキングチャネル受信のパフォーマンスを測定します。

func BenchmarkChanNonblocking(b *testing.B) {
	const CallsPerSched = 1000 // 1ゴルーチンあたりの呼び出し回数
	procs := runtime.GOMAXPROCS(-1) // 現在のGOMAXPROCS値を取得
	N := int32(b.N / CallsPerSched) // 全体の繰り返し回数をゴルーチン数で割って調整
	c := make(chan bool, procs) // ゴルーチンの終了を待つためのチャネル
	myc := make(chan int) // 非ブロッキング受信をテストするチャネル(常に空)

	for p := 0; p < procs; p++ { // GOMAXPROCSの数だけゴルーチンを起動
		go func() {
			for atomic.AddInt32(&N, -1) >= 0 { // 全体の繰り返し回数が0になるまでループ
				for g := 0; g < CallsPerSched; g++ { // 1ゴルーチンあたりの呼び出し回数
					select {
					case <-myc: // mycからの受信を試みる
					default: // mycが空なので、常にこちらが実行される
					}
				}
			}
			c <- true // ゴルーチン終了を通知
		}()
	}
	for p := 0; p < procs; p++ { // 全てのゴルーチンの終了を待つ
		<-c
	}
}

このベンチマークの核心は、select { case <-myc: default: }というパターンです。mycチャネルにはデータが送信されないため、case <-mycは常に準備ができておらず、結果としてdefaultケースが実行されます。これにより、チャネルが空である場合の非ブロッキング受信のオーバーヘッドが測定されます。atomic.AddInt32は、複数のゴルーチンが共有するカウンタを安全にデクリメントするために使用され、全体のベンチマーク実行回数を管理します。

BenchmarkSelectProdCons

この関数は、select文を用いたプロデューサー・コンシューマーパターンのパフォーマンスを測定します。

func BenchmarkSelectProdCons(b *testing.B) {
	const CallsPerSched = 1000
	procs := runtime.GOMAXPROCS(-1)
	N := int32(b.N / CallsPerSched)
	c := make(chan bool, 2*procs) // プロデューサーとコンシューマーの終了を待つためのチャネル
	myc := make(chan int, 128) // データ転送用チャネル
	myclose := make(chan bool) // 終了シグナル用チャネル

	for p := 0; p < procs; p++ {
		// プロデューサーゴルーチン
		go func() {
			foo := 0
			mytimer := time.After(time.Hour) // ベンチマーク中は発火しないタイマー
			for atomic.AddInt32(&N, -1) >= 0 {
				for g := 0; g < CallsPerSched; g++ {
					// ローカルな作業をシミュレート
					for i := 0; i < 100; i++ {
						foo *= 2
						foo /= 2
					}
					select {
					case myc <- 1: // データ送信
					case <-mytimer: // タイムアウト監視
					case <-myclose: // 終了シグナル監視
					}
				}
			}
			myc <- 0 // 終了シグナル(コンシューマーに0を送信してループを終了させる)
			c <- foo == 42 // 終了通知
		}()

		// コンシューマーゴルーチン
		go func() {
			foo := 0
			mytimer := time.After(time.Hour) // ベンチマーク中は発火しないタイマー
		loop:
			for {
				select {
				case v := <-myc: // データ受信
					if v == 0 { // 0を受信したら終了
						break loop
					}
				case <-mytimer: // タイムアウト監視
				case <-myclose: // 終了シグナル監視
				}
				// ローカルな作業をシミュレート
				for i := 0; i < 100; i++ {
					foo *= 2
					foo /= 2
				}
			}
			c <- foo == 42 // 終了通知
		}()
	}
	for p := 0; p < procs; p++ { // 全てのプロデューサーとコンシューマーの終了を待つ
		<-c
		<-c
	}
}

このベンチマークは、プロデューサーとコンシューマーがそれぞれselect文を使って複数のチャネルを監視するシナリオをシミュレートします。mycチャネルはデータ転送用、mytimerはタイムアウト、mycloseは終了シグナルを模倣しています。各ゴルーチンは、チャネル操作の間に簡単な計算(foo *= 2; foo /= 2)を行うことで、実際のアプリケーションにおけるCPU作業をシミュレートしています。これにより、select文が複数のチャネルを同時に扱う際のオーバーヘッドと、プロデューサー・コンシューマー間のデータフローの効率が測定されます。

BenchmarkChanSem

この関数は、チャネルベースのセマフォのパフォーマンスを測定します。既存のBenchmarkChanSem関数が、より現実的な並行アクセスシナリオを反映するように変更されています。

func BenchmarkChanSem(b *testing.B) {
	type Empty struct{} // メモリを消費しない空の構造体
	const CallsPerSched = 1000
	procs := runtime.GOMAXPROCS(0) // 現在のGOMAXPROCS値を取得
	N := int32(b.N / CallsPerSched)
	c := make(chan bool, procs) // ゴルーチンの終了を待つためのチャネル
	myc := make(chan Empty, procs) // セマフォとして使用するバッファ付きチャネル

	for p := 0; p < procs; p++ { // GOMAXPROCSの数だけゴルーチンを起動
		go func() {
			for atomic.AddInt32(&N, -1) >= 0 {
				for g := 0; g < CallsPerSched; g++ {
					myc <- Empty{} // セマフォ獲得 (チャネルに送信)
					<-myc // セマフォ解放 (チャネルから受信)
				}
			}
			c <- true // ゴルーチン終了を通知
		}()
	}
	for p := 0; p < procs; p++ { // 全てのゴルーチンの終了を待つ
		<-c
	}
}

このベンチマークでは、mycというバッファ付きチャネルがセマフォとして機能します。チャネルのバッファサイズはprocsに設定されており、同時にprocs個のゴルーチンがセマフォを獲得できます。myc <- Empty{}はセマフォの獲得(リソースのロック)を、<-mycはセマフォの解放(リソースのアンロック)を意味します。複数のゴルーチンが並行してこの獲得・解放操作を繰り返すことで、チャネルをセマフォとして使用する際の競合と同期のオーバーヘッドが測定されます。

これらのベンチマークは、Goランタイムのチャネル実装の効率性を、様々な並行処理パターンにおいて深く評価するための重要な追加となります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語の標準ライブラリのソースコード
  • Go言語の並行処理に関する一般的な知識
  • セマフォとプロデューサー・コンシューマーパターンの一般的な概念