System.Threading.Channels性能検証

# はじめに

この記事では、System.Threading.Channelsについて、色々と性能検証をしてみたので、その結果を書く。
この検証結果はあくまでも自分のコードの中でという条件付きなので、実際に使う際は性能比較は当然必要だが、何かの指針になれば幸い。
System.Threading.Channelsって何?という人は、[System.Threading.Channelsを使う](https://qiita.com/skitoy4321/items/c19ca3dc7624a7049fd5)にて解説ページを書いたので、まずそちらを参照のこと。

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

# 検証項目について

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

* BlockingCollectionとの比較
* SingleReader/SingleWriter設定時の性能差
* AllowSynchronousContinuationsオンオフの性能差

# BlockingCollectionとの比較

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

### シングルスレッド

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

* [ソースコード](https://github.com/itn3000/channelbench/blob/3c63a1180309135f125f3848fa578811abbcda16/Program.cs#L241-L317)

結果は以下。

|                        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 |


### マルチスレッド

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

* [ベンチマークコード](https://github.com/itn3000/channelbench/blob/3c63a1180309135f125f3848fa578811abbcda16/Program.cs#L174-L240)

結果は以下。

|                      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の設定値が、実際にどの程度性能に影響を及ぼすのか、検証してみた。

* [ベンチマークコード](https://github.com/itn3000/channelbench/blob/3c63a1180309135f125f3848fa578811abbcda16/Program.cs#L363-L399)

## 結果

|       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のオンオフによる性能差を比較してみた。

* [ベンチマークコード](https://github.com/itn3000/channelbench/blob/3c63a1180309135f125f3848fa578811abbcda16/Program.cs#L318-L362)

## 結果

|          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フラグは結構影響が大なので、要件が許すならば必ず設定した方が良さそう。