[a, b, c, d, e, f, g, h]
というリストを3個ずつに分割して、最後の要素は3個以下の[a, b, c], [d, e, f], [g, h]
というリストを得たいとする。そのときC#でどう書くかという質問の答えとして人気があるのが、以下のLINQである。
stackoverflowでは二つの質問(Split a List into smaller lists of N size [duplicate]、Split List into Sublists with LINQ)の合計で900以上のup voteを得ている。
しかし、この方法は性能の点では最悪である。まずリストの要素数だけIndexとValueを持つオブジェクトを生成している。オブジェクトの生成はメモリと速度両面で、とてもコストの高い操作である。それと比べると小さなコストではあるが、要素の数だけ除算と比較を行うのも性能上よくない。
速度にこだわるのならそもそもLINQを使わなければいいのだが、もしLINQで簡潔に書きたいのなら、以下の解はどうだろうか。
以下はBenchmarkDotNetで取った、1000要素のintの配列を3個ずつに分割するベンチマークの結果である。最初に挙げたものがSplitter1、これがSplitter2である。Meanが平均値で、それ以外は測定誤差を表している。Meanを見るとSplitter2が3.5倍以上速いのがわかる。
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1766 (21H2)
AMD Ryzen 7 3800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK=6.0.106
[Host] : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT
LongRun : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT
Job=LongRun IterationCount=100 LaunchCount=3
WarmupCount=15
Method | Mean | Error | StdDev |
---|---|---|---|
Splitter1 | 89.94 μs | 0.351 μs | 1.779 μs |
Splitter2 | 24.04 μs | 0.100 μs | 0.517 μs |
ただし、.NET 6にはChunkという標準のメソッドが追加されているので、.NET 6以降が対象ならChunkを使うべきだ。Chunkを加えて上と同じベンチマークを実行すると結果はこうなる。Splitter2と比較しても4,000倍以上速い。
Method | Mean | Error | StdDev |
---|---|---|---|
Splitter1 | 88,650.102 ns | 245.2557 ns | 1,251.8625 ns |
Splitter2 | 23,481.503 ns | 117.8934 ns | 600.6975 ns |
Chunk | 5.609 ns | 0.0198 ns | 0.0984 ns |