[インデックス 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つのシナリオに対するベンチマークが追加されました。
- 非ブロッキング受信("stop"チャネルのポーリング):
select
文のdefault
ケースを利用した、チャネルからの非ブロッキング受信のパフォーマンス測定。これは、チャネルにデータがない場合にブロックせずに即座に処理を継続するパターンを評価します。 - チャネルベースのセマフォ(ゲートパターン): バッファ付きチャネルをセマフォとして利用する際のパフォーマンス測定。これは、並行処理におけるリソースアクセス制御の一般的なパターンです。
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
でデータを送信し、同時にmytimer
とmyclose
チャネルもselect
で監視します。データ送信の間に簡単な計算(foo *= 2; foo /= 2
)で「ローカルな作業」をシミュレートします。 - コンシューマー:
<-myc
でデータを受信し、同時にmytimer
とmyclose
チャネルも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言語のチャネルに関する公式ドキュメント: https://go.dev/tour/concurrency/2
- Go言語の
select
文に関する公式ドキュメント: https://go.dev/tour/concurrency/5 - Go言語のベンチマークに関する公式ドキュメント: https://go.dev/doc/articles/go_benchmarking.html
sync/atomic
パッケージのドキュメント: https://pkg.go.dev/sync/atomic
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語の標準ライブラリのソースコード
- Go言語の並行処理に関する一般的な知識
- セマフォとプロデューサー・コンシューマーパターンの一般的な概念