coro プールに関するベンチマーク
Coroでスレッドプールを使う - 北海道苫小牧市出身の初老PGが書くブログで Coro でスレッドプールを使う際の注意点が、様々なベンチマークと共に示されている。まとめとして
「async_poolを使う時には、同時にたくさんのスレッドを利用し過ぎないように気をつける」の一点に尽きます。
とあるのだが、これは順序が違うのではないかと思ったりする。ごく単純な話だが、「async_pool を使う」ことが先に来るわけではなく、「スレッドを起こしたり破棄したりを繰り返すケース」が存在し、そういう場合は async を使うよりも async_pool を使ったほうがいい、という順序になる。もちろん、そういうケースで async_pool を使うことを選択したときに、「同時にたくさんのスレッドを利用し過ぎないように気をつける」というのは適切な注意であるわけだが。同エントリでは、最初に次のようなベンチマークをとっている。
my $tasks = 100; Benchmark::cmpthese 1000, { async => sub { my $sem = Coro::Semaphore->new( 1 - $tasks ); for(1 .. $tasks){ async { $sem->up }; } $sem->down; }, async_pool => sub { my $sem = Coro::Semaphore->new( 1 - $tasks ); for(1 .. $tasks){ async_pool { $sem->up }; } $sem->down; }, };
このクロージャはそもそも async_pool が活きてくる「スレッドを起こしたり破棄したりを繰り返す」パターンではない。100 個の coro が作られるけれども、それらは実行されず ready キューに入れられたままになっている。そして main スレッドが $sem->down できずにブロックされたあと、次々に実行され、$sem->up して (async_pool ブロックを抜けて) terminate する。100 個の coro が terminateの処理が終了した後、セマフォのカウンタが 1 になって main スレッドが動いて一回のイテレーションが終了する。
1,000 回イテレーションが行われるので、そこでは「スレッドを起こしたり破棄したりを繰り返す」パターンになるのだが、破棄された coro のうち再利用に回されるのは $Coro::POOL_SIZE == 8 個だけなので 92 個の coro は毎回オブジェクトの生成、破棄が繰り返されることになり、coro プールのありがたみはほとんど得られない。それ以上にプール操作のオーバヘッドが利いてしまう。
coro プールを使うことで得られるアドバンテージ、プール管理にかかるオーバヘッドを測定するために次のようなベンチマークをしてみた。
use strict; use warnings; use Coro; use Benchmark; timethese 50_000, { async_7 => sub { async {} for 1..7; cede }, async_8 => sub { async {} for 1..8; cede }, async_9 => sub { async {} for 1..9; cede }, pool_7 => sub { async_pool {} for 1..7; cede }, pool_8 => sub { async_pool {} for 1..8; cede }, pool_9 => sub { async_pool {} for 1..9; cede }, }; __END__
それぞれ、何もしない coro を 7 (もしくは 8 もしくは 9) 個生成し、main スレッドで cede することですべての coro を走らせ、terminate 処理を終了させる。これを繰り返す。coro プールの利用、すなわち async_pool が有効と考えられるパターンである。async_7 と async_8、async_9 は 7:8:9 の比になることが予想される。pool_7 と pool_8 も 7:8 の比になることが予想される。pool_8 より pool_9 にかかる時間がひとつの coro を余分に生成、破棄するコストと考えられる。これは async_7 と async_8 の差 (async_8 と async_9 の差) に近いと予想される。
Benchmark: timing 50000 iterations of async_7, async_8, async_9, pool_7, pool_8, pool_9... async_7: 4 wallclock secs ( 3.78 usr + 0.00 sys = 3.78 CPU) @ 13227.51/s (n=50000) async_8: 4 wallclock secs ( 4.24 usr + 0.01 sys = 4.25 CPU) @ 11764.71/s (n=50000) async_9: 5 wallclock secs ( 4.81 usr + 0.00 sys = 4.81 CPU) @ 10395.01/s (n=50000) pool_7: 1 wallclock secs ( 0.79 usr + 0.00 sys = 0.79 CPU) @ 63291.14/s (n=50000) pool_8: 1 wallclock secs ( 0.86 usr + 0.00 sys = 0.86 CPU) @ 58139.53/s (n=50000) pool_9: 1 wallclock secs ( 1.63 usr + 0.00 sys = 1.63 CPU) @ 30674.85/s (n=50000)
async_7 : async_8 : async_9 = 3.78 : 4.25 : 4.81 = 7.12 : 8.00 : 9.06 となった。予想どおり 7:8:9 に近い。一方 pool_7 : pool_8 : pool_9 = 0.79 : 0.86 : 1.63 = 7.35 : 8.00 : 15.2 となった。これも予想どおり 7:8 に近く、pool_8 と pool_9 では 8:9 よりも大幅に時間がかかっている。async_8 - async_7 = 0.47 は pool_9 - pool_8 = 0.77 よりもだいぶ少ない。この差が coro プールを管理するオーバヘッドと考えられる。