Edited at

System.Threading.Channels性能検証

More than 1 year has passed since last update.


はじめに

この記事では、System.Threading.Channelsについて、色々と性能検証をしてみたので、その結果を書く。

この検証結果はあくまでも自分のコードの中でという条件付きなので、実際に使う際は性能比較は当然必要だが、何かの指針になれば幸い。

System.Threading.Channelsって何?という人は、System.Threading.Channelsを使うにて解説ページを書いたので、まずそちらを参照のこと。

テストコードについての指摘は随時歓迎。


検証項目について

さて、今回検証したのは以下の項目。


  • BlockingCollectionとの比較

  • SingleReader/SingleWriter設定時の性能差

  • AllowSynchronousContinuationsオンオフの性能差


BlockingCollectionとの比較

役割として最も重複すると思われるBlockingCollectionとの比較をしてみた。


シングルスレッド

単純に全て同一スレッドで行った場合の所要時間の比較。

結果は以下。

Method
Toolchain
LoopNum
Mean
Error
StdDev
Gen 0
Gen 1
Gen 2
Allocated

ConcurrentQueueBenchWrite
.NET Core 2.0
100000
1.816 ms
0.3366 ms
0.0190 ms
285.1563
285.1563
285.1563
1 MB

BlockingCollectionWrite
.NET Core 2.0
100000
6.002 ms
1.6192 ms
0.0915 ms
281.2500
281.2500
281.2500
1 MB

ThreadingChannelsWrite
.NET Core 2.0
100000
4.256 ms
0.5604 ms
0.0317 ms
281.2500
281.2500
281.2500
1 MB

ConcurrentQueueBenchWriteRead
.NET Core 2.0
100000
3.243 ms
0.3243 ms
0.0183 ms
285.1563
285.1563
285.1563
1 MB

BlockingCollectionWriteRead
.NET Core 2.0
100000
12.885 ms
2.9885 ms
0.1689 ms
281.2500
281.2500
281.2500
1 MB

ThreadingChannelsWriteRead
.NET Core 2.0
100000
6.144 ms
2.7683 ms
0.1564 ms
281.2500
281.2500
281.2500
1 MB

ConcurrentQueueBenchWrite
.NET Core 2.1
100000
1.872 ms
0.2313 ms
0.0131 ms
285.1563
285.1563
285.1563
1 MB

BlockingCollectionWrite
.NET Core 2.1
100000
6.289 ms
1.4640 ms
0.0827 ms
281.2500
281.2500
281.2500
1.01 MB

ThreadingChannelsWrite
.NET Core 2.1
100000
4.618 ms
1.6158 ms
0.0913 ms
281.2500
281.2500
281.2500
1.01 MB

ConcurrentQueueBenchWriteRead
.NET Core 2.1
100000
3.593 ms
0.8562 ms
0.0484 ms
285.1563
285.1563
285.1563
1 MB

BlockingCollectionWriteRead
.NET Core 2.1
100000
13.606 ms
2.1582 ms
0.1219 ms
281.2500
281.2500
281.2500
1.01 MB

ThreadingChannelsWriteRead
.NET Core 2.1
100000
6.412 ms
0.6740 ms
0.0381 ms
281.2500
281.2500
281.2500
1.01 MB


マルチスレッド

マルチスレッド環境下での読み書き所要時間を計測。

結果は以下。

Method
Toolchain
LoopNum
WriteTaskNum
ReadTaskNum
Mean
Error
StdDev

BlockingCollectionReadWrite
.NET Core 2.0
10000
1
1
13.320 ms
3.9529 ms
0.2233 ms

ThreadingChannelReadWrite
.NET Core 2.0
10000
1
1
11.181 ms
6.4940 ms
0.3669 ms

BlockingCollectionReadWrite
.NET Core 2.1
10000
1
1
12.506 ms
2.1169 ms
0.1196 ms

ThreadingChannelReadWrite
.NET Core 2.1
10000
1
1
11.865 ms
6.1815 ms
0.3493 ms

BlockingCollectionReadWrite
.NET Core 2.0
10000
1
100
257.627 ms
143.1464 ms
8.0880 ms

ThreadingChannelReadWrite
.NET Core 2.0
10000
1
100
173.708 ms
36.2240 ms
2.0467 ms

BlockingCollectionReadWrite
.NET Core 2.1
10000
1
100
28.791 ms
280.0257 ms
15.8220 ms

ThreadingChannelReadWrite
.NET Core 2.1
10000
1
100
107.643 ms
27.7679 ms
1.5689 ms

BlockingCollectionReadWrite
.NET Core 2.0
10000
100
1
13.668 ms
1.5261 ms
0.0862 ms

ThreadingChannelReadWrite
.NET Core 2.0
10000
100
1
13.040 ms
0.5886 ms
0.0333 ms

BlockingCollectionReadWrite
.NET Core 2.1
10000
100
1
11.976 ms
0.3496 ms
0.0198 ms

ThreadingChannelReadWrite
.NET Core 2.1
10000
100
1
11.635 ms
1.3184 ms
0.0745 ms

BlockingCollectionReadWrite
.NET Core 2.0
10000
100
100
86.314 ms
32.3458 ms
1.8276 ms

ThreadingChannelReadWrite
.NET Core 2.0
10000
100
100
6.034 ms
1.1547 ms
0.0652 ms

BlockingCollectionReadWrite
.NET Core 2.1
10000
100
100
16.961 ms
63.2741 ms
3.5751 ms

ThreadingChannelReadWrite
.NET Core 2.1
10000
100
100
2.876 ms
2.6431 ms
0.1493 ms


考察

単独スレッドにおける処理速度は、ConcurrentQueue > Channels > BlockingCollectionというのは恐らく間違いないと見ていいと思う。

ConcurrentQueueが最速なのは、担当する仕事の少なさに由来する。実際待ち受け処理が欲しい場合はセマフォ等を駆使することになるので、処理性能的には似たような所に落ち着くかもしれない。

読み出し側が単一の場合は、多くの場合においてChannelsの方がかなりいい成績を出したが、

読み出しをマルチで行った場合は、Channelsの方は著しく性能が落ちてしまった。

この辺り、読み:書き=M:Nで行おうとしている人は要注意。

なお、今回の場合、両方ともオプション無指定なので、チューニングすればもっと早くなる可能性はある。

特に後述するSingleReaderオプションを付ければ、差は更に広がる。


SingleReader/SingleWriterオンオフの性能差

次にSingleReaderとSingleWriterの設定値が、実際にどの程度性能に影響を及ぼすのか、検証してみた。


結果

Method
Toolchain
LoopNum
SingleReader
SingleWriter
Mean
Error
StdDev
Gen 0
Allocated

SettingBench
.NET Core 2.0
10000
False
False
1,091.2 us
352.63 us
19.924 us
15.6250
1.88 KB

SettingBench
.NET Core 2.1
10000
False
False
1,145.9 us
917.52 us
51.841 us
1.9531
2.05 KB

SettingBench
.NET Core 2.0
10000
False
True
1,093.2 us
159.06 us
8.987 us
19.5313
1.88 KB

SettingBench
.NET Core 2.1
10000
False
True
1,054.6 us
303.04 us
17.122 us
1.9531
2.05 KB

SettingBench
.NET Core 2.0
10000
True
False
882.1 us
84.44 us
4.771 us
16.6016
1.87 KB

SettingBench
.NET Core 2.1
10000
True
False
644.6 us
426.33 us
24.088 us
1.9531
1.85 KB

SettingBench
.NET Core 2.0
10000
True
True
875.6 us
54.41 us
3.075 us
15.6250
1.87 KB

SettingBench
.NET Core 2.1
10000
True
True
629.9 us
228.31 us
12.900 us
1.9531
1.85 KB


考察

特にSingleReaderについて、思ったよりも多大な効果が得られた。Channelsは読み出し側を一本に集約するという使い方がかなり多いと思うので、要件を確認して、可能ならばこのフラグを立てた方がいいと思う。


AllowSynchronousContinuationsオンオフの性能差

最後に、AllowSynchronousContinuationsのオンオフによる性能差を比較してみた。


結果

Method
Toolchain
LoopNum
WriteTaskNum
ReadTaskNum
AllowAsync
Mean
Error
StdDev
Gen 0
Gen 1
Allocated

AllowAsyncBench
.NET Core 2.0
10000
1
1
False
11.653 ms
0.7141 ms
0.0403 ms
250.0000
-
2.73 KB

AllowAsyncBench
.NET Core 2.1
10000
1
1
False
11.013 ms
1.7051 ms
0.0963 ms
250.0000
-
2.69 KB

AllowAsyncBench
.NET Core 2.0
10000
1
1
True
12.351 ms
3.2105 ms
0.1814 ms
250.0000
-
2.73 KB

AllowAsyncBench
.NET Core 2.1
10000
1
1
True
10.855 ms
2.2629 ms
0.1279 ms
250.0000
-
2.69 KB

AllowAsyncBench
.NET Core 2.0
10000
1
100
False
169.190 ms
65.8978 ms
3.7233 ms
17562.5000
-
39.01 KB

AllowAsyncBench
.NET Core 2.1
10000
1
100
False
105.575 ms
64.6819 ms
3.6546 ms
15937.5000
-
28.03 KB

AllowAsyncBench
.NET Core 2.0
10000
1
100
True
293.862 ms
13.3134 ms
0.7522 ms
35875.0000
-
39.09 KB

AllowAsyncBench
.NET Core 2.1
10000
1
100
True
301.211 ms
146.6420 ms
8.2855 ms
33125.0000
-
28.24 KB

AllowAsyncBench
.NET Core 2.0
10000
100
1
False
12.921 ms
1.6427 ms
0.0928 ms
265.6250
46.8750
31.09 KB

AllowAsyncBench
.NET Core 2.1
10000
100
1
False
11.487 ms
0.8529 ms
0.0482 ms
281.2500
46.8750
20.99 KB

AllowAsyncBench
.NET Core 2.0
10000
100
1
True
12.872 ms
0.2811 ms
0.0159 ms
265.6250
46.8750
31.09 KB

AllowAsyncBench
.NET Core 2.1
10000
100
1
True
11.378 ms
1.1391 ms
0.0644 ms
296.8750
15.6250
20.99 KB

AllowAsyncBench
.NET Core 2.0
10000
100
100
False
5.755 ms
1.0442 ms
0.0590 ms
492.1875
-
60.98 KB

AllowAsyncBench
.NET Core 2.1
10000
100
100
False
2.810 ms
0.4191 ms
0.0237 ms
285.1563
-
40.82 KB

AllowAsyncBench
.NET Core 2.0
10000
100
100
True
7.418 ms
3.4187 ms
0.1932 ms
562.5000
-
60.98 KB

AllowAsyncBench
.NET Core 2.1
10000
100
100
True
4.918 ms
9.8277 ms
0.5553 ms
281.2500
-
40.83 KB


考察

コメントにはtrueの方がスループット出るみたいなこと書かれていたけど、結果を見るとfalseが全般的に成績が良いように見える。

もしかしたら何か見落としがあるかもしれないが、とりあえずfalseの方が良さそう。


終りに

今まで非同期FIFOとしてBlockingCollectionを使用していた部分については、Channelsに移行した方が良さそう。

特にW:R=N:1の場合は、かなり性能向上が望めるかもしれない。

後、SingleReaderフラグは結構影響が大なので、要件が許すならば必ず設定した方が良さそう。