Go のチャンネルオーバヘッド
Goではチャンネルをいろいろな目的に使うことが推奨されている。pythonならgeneratorで書くようなこともgoroutineとチャンネルでやる。確かに書きやすくはなるのだろうけど、どの程度オーバヘッドがあるのか調べてみた。
調べること
プロデューサとコンシューマをそれぞれ別のgoroutineで動かしておいて、その間に64bit intをひとつずつ流す。バッファなしのチャネルなので、1つ流れるごとにgoroutine間のコンテクストスイッチが起こる。これを単純な関数呼び出しのオーバヘッドと比較する。
チャンネルを使ったコード
package main import "fmt" func produce(sink chan int64, num int64){ var i int64 for i = 0; i < num; i++ { sink <- i } close(sink) } func consume(src chan int64) { for i := range src { if i % 10000000 == 0 { fmt.Printf("%d\n", i) } } } const c int64 = 100 * 1000 * 1000 func main() { ch := make(chan int64) go produce(ch, c) consume(ch) }
関数呼び出しのコード
package main import "fmt" func produce(i int64) int64 { return i } func consume(num int64) { var i int64 for i = 0; i < num; i++ { if i % 10000000 == 0 { fmt.Printf("%d\n", produce(i)) } } } const c int64 = 100 * 1000 * 1000 func main() { consume(c) }
結果
1億回での実行結果は、チャンネルを使う方が43秒! 関数のみのほうは、0.33秒だ。ひょっとするとインライン展開されて関数呼び出し消えてるかも知れないけど。1回のコンテクストスイッチにかかる時間が、0.43マイクロ秒というのは、速いのか遅いのか。
複数コアにすると
現在のGoの処理系はデフォルトではシングルコアで動く。複数コアを使うには環境変数GOMAXPROCSを設定する。手元の環境は、late 2007 のMacBook Proで一応デュアルコアなので、GOMAXPROCS=2として同じプログラムを実行してみた。結果はなんと、380秒!しかもシステムで300秒近く食っている。今現在どういう実装になってるのかわからないけど、これはちょっとひどいかも。だからデフォルトでは1コアなんだなあ、と納得。
バッファを使うと
ついでに、バッファ付きチャンネルにして試してみた。
ch := make(chan int64)
を
ch := make(chan int64, 10)
とすると、バッファサイズ10になるのだけど、こうすると22秒ぐらい。速度がだいたい倍になっている。これはコンテクストスイッチの回数を減らすことができるからだろう。では、というのでバッファを1000にすると、17秒ぐらい。10000にしても17秒でほとんど変わらない。
理論的にはバッファサイズ10であれば、コンテクストスイッチ回数は10分の1になるはず。何も考えずにチャンネルで読み込み/書き込みブロックするまで一つのgoroutineが実行されるようにしてあればそういう動作になりそうなものだけど、どうもそんなに単純じゃなさそう。ブロックしなくてもある程度フェアにスケジュールされるようにがんばってるのかも。面白い。