リストをn個ずつのリストに分割する

[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  
MethodMeanErrorStdDev
Splitter189.94 μs0.351 μs1.779 μs
Splitter224.04 μs0.100 μs0.517 μs

ただし、.NET 6にはChunkという標準のメソッドが追加されているので、.NET 6以降が対象ならChunkを使うべきだ。Chunkを加えて上と同じベンチマークを実行すると結果はこうなる。Splitter2と比較しても4,000倍以上速い。

MethodMeanErrorStdDev
Splitter188,650.102 ns245.2557 ns1,251.8625 ns
Splitter223,481.503 ns117.8934 ns600.6975 ns
Chunk5.609 ns0.0198 ns0.0984 ns

dotnet formatコマンドをStyleCop.Analyzersと組み合わせて使う

この記事では、.editorconfigを使用したdotnet formatコマンドの使用方法とその限界について説明し、StyleCop.Analyzersでそれを解決する方法を紹介する。また、ソリューション内のすべてのプロジェクトにStyleCopルールセットを適用する方法についても言及する。

"dotnet formatコマンドをStyleCop.Analyzersと組み合わせて使う" の続きを読む

システムトレイアプリケーションをWPFとMVVMで実装する

この記事では、WPFとMVVMとによるシステムトレイアプリケーションの一実装について解説する。ソースコード全体はGitHubリポジトリにある。

この実装は二つの特徴がある。まず、よく使われているWPF NotifyIcon使っていない。ライセンスのCPOLが、どのOSSライセンスとも互換性がないからだ。また、MVVMパターンを採用していてコードビハインドがない。

"システムトレイアプリケーションをWPFとMVVMで実装する" の続きを読む

C#でロックフリーなメモリープールを自作する

基本的にシングルスレッドでしか使わないライブラリを、速度を落とさずにスレッドセーフにするために、ロックフリーなメモリープールを自作した話を紹介する。この記事はC# その2 Advent Calendar 2020の18日目の記事である。

"C#でロックフリーなメモリープールを自作する" の続きを読む

厳格なJSONパーザーの作り方

僕の作ったJSONパーザーのDynaJsonはとても厳格にできている。RFC 8259に準拠しているものはすべて受理するし、そうでないものは二つの例外を除きすべて受理しない。

受理する例外は、ケツカンマと02-02のような0で始まる数字である。前者を受理するのは実用性のためであり、後者を受理するのはDynamicJsonとの互換性のためである。

JSONの文法はとても単純だが、不用意にパーザーを実装するとRFCに準拠したJSONを受理しなかったり、非準拠のものをエラーにできなかったりする。パーザーを実装する際の地雷ポイントはParsing JSON is a Minefieldによくまとまっている。

"厳格なJSONパーザーの作り方" の続きを読む