LinqのCount()拡張メソッドと向き合う #1

LinqのCount()拡張メソッドと向き合う

Collection Object色々

C#には、Collectionを扱う型がいくつもあります。IEnumerable<T>や、List<T>等が代表選手です。

Collectionの要素数を取得する方法として、System.Linq名前空間に用意されている拡張メソッドに、.Count()メソッドがあります。

Microsoft Docsを読んでみる

docs.microsoft.com リンク先のDocsを読んでいると

source の型が ICollection<T> を実装している場合は、その実装を使用して要素の数を取得します。 それ以外の場合、このメソッドはカウントを決定します。

という記述があります。

ICollection<T>を実装している場合、Count()メソッドの中で、Countプロパティを参照し、要素数を返します。

実装を見て、この最適化について考える🤔

githubのCoreFx(.NET CoreのBCLに相当するアレ)のRepoだと以下の部分です。

https://github.com/dotnet/corefx/blob/master/src/System.Linq/src/System/Linq/Count.cs#L19github.com

CoreFxなこともあって、C#7の、[ is演算子の拡張] の際に入った型パターンで実装されていますが、実装仕様はC#7以前でも変わりはありません。

型パターンなので、

IEnumerable<string> hoges = new List<string> { "hoge", "fuga" };

で、List<T> 型を、IEnumerable<T> 型で受けたところで、このhogesに対して

if (hoges is ICollection<string>collectionHoges ){
//・・・
}

のisで検査した場合、結果はtrueになります。(ListはICollectionを実装しているため。)

.Count()って書いても大体の業務コードでパフォーマンスペナルティがあまり問題にならないのは、この最適化を、Count()側で入れてくれているからです。

あくまでパフォーマンスペナルティが「小さくなるよう」最適化がかかっているだけで、isでの検査はノーコストではないので、可能な場合は.Count()メソッドより、Countプロパティを直接参照してもらったほうがペナルティーは少ないです。

そして、ICollection<T> を実装していない場合に.Count()メソッドを使うと、Collectionのカウントを一生懸命数えることを知っている人は、IEnumerable<T> の件数を複数回数える場合は.ToList()などで変換します。

その結果、List<T> のInstance生成コストが余分にかかります。

余談:個人的なオススメ

List<T> にキャストできるならキャストし、そうでなければ.ToList()でList<T> のInstance生成して積み替えることです。

IEnumerable<T> の存在意義

yield returnを含んだイテレーターブロックを使用しているなら、IEnumerable<T> で返すのが気楽ですし、IEnumerable<T>Linqの特徴である遅延評価は、パフォーマンス面でのメリットとして存在しています。

後続処理で、Where等の拡張メソッドを重ねていく場合には、IEnumerable<T> で返してあげたほうがペナルティーが少なくなることも多いです。

.Countメソッドも、条件を受け取るオーバーロードのほうを利用する場合は、IEnumerableのほうが省メモリーな場合もあるでしょう。

逆に、お仕事でしばしば散見されるのは、IEnumerable<T> に対して、Linqで同じ条件で何度もFilterしたりCountしたりするコードです。

「取り合えずIEnumerable<T> で返すべし」と教えて、遅延評価に関する説明が相手に伝わっていないと大体こうなるので、自分含めて、「なぜ」この型で取り扱うべきなのかというのは、知らない人にわかりやすく説明するべきなのだろうなぁと感じます。

※ 遅延評価で特に怖いのはやっぱり遅延実行のObject周辺。

「Count()って軽い気持ちで書いただけなのに、実はCollection作成するところも毎回走ってるから、受けるパフォーマンスペナルティーが目も当てられない」なんてことになることも。

が、これはCount()だけの話というよりはLinq全般の注意点なので今回は割愛します。

おまけ:IReadOnlyList<T> とかの、ICollection<T> は実装していないんだけど、Countプロパティを持っているObjectの処遇

docs.microsoft.com

IReadOnlyList<T> では.Count()メソッドを呼び出しても、型パターン使ってCountプロパティを見てくれる最適化は、行われません。

github.com

Github を見る感じ、最適化入れても効果が非常に限定的だから見送られたように読めます。 なのでIReadOnlyList<T> の類を使う場合には、Count()メソッドではなく、Countプロパティを参照しないとパフォーマンスペナルティを強く受けることになります。