アプリケーションの1つをプロファイリングするときに、コレクションの開始近くの述語に一致する複数のアイテムを持つ大規模なコレクションに対してEnumerable.Single(source, predicate)
を呼び出していたコードで不思議なスローダウンを発見しました。
調査の結果、 Enumerable.Single()
の実装 は次のとおりです。
public static TSource Single<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
TSource result = default(TSource);
long count = 0;
// Note how this always iterates through ALL the elements:
foreach (TSource element in source) {
if (predicate(element)) {
result = element;
checked { count++; }
}
}
switch (count) {
case 0: throw Error.NoMatch();
case 1: return result;
}
throw Error.MoreThanOneMatch();
}
その実装は、複数の要素がすでに述語に一致している場合でも、シーケンスのすべての要素を反復処理します。
次の実装では、同じ結果が得られます。
public static TSource Single<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
TSource result = default(TSource);
long count = 0;
foreach (TSource element in source) {
if (predicate(element)) {
if (count == 1) // Exit loop immediately if more than one match found.
throw Error.MoreThanOneMatch();
result = element;
count++; // "checked" is no longer needed.
}
}
if (count == 0)
throw Error.NoMatch();
return result;
}
実際の実装がこの明らかな最適化を使用しない理由を誰もが知っていますか?私が見逃しているものはありますか? (このような明白な最適化が見落とされることは想像できないので、そのための具体的な理由がなければなりません。)
(注:この質問は意見である答えを引き付ける可能性があることを理解しています。すべての要素を反復する具体的な理由を提供する答えを望んでいます。答えが実際にこの質問は答えられないので、削除する必要があると思います...)
比較のために、述語をとらないSingle()
の実装を見てください:
public static TSource Single<TSource>(this IEnumerable<TSource> source)
{
IList<TSource> list = source as IList<TSource>;
if (list != null) {
switch (list.Count) {
case 0: throw Error.NoElements();
case 1: return list[0];
}
}
else {
using (IEnumerator<TSource> e = source.GetEnumerator()) {
if (!e.MoveNext()) throw Error.NoElements();
TSource result = e.Current;
if (!e.MoveNext()) return result;
}
}
throw Error.MoreThanOneElement();
}
この場合、彼らはIList
の最適化を追加する努力をしました。
それを考えているのはあなただけではないようです。 。NET Core実装 には最適化されたバージョンがあります:
using (IEnumerator<TSource> e = source.GetEnumerator())
{
while (e.MoveNext())
{
TSource result = e.Current;
if (predicate(result))
{
while (e.MoveNext())
{
if (predicate(e.Current))
{
throw Error.MoreThanOneMatch();
}
}
return result;
}
}
}
したがって、あなたの質問に答えるには、このユースケースの最適化を考えていない開発者以外には、「良い」理由はないようです。
コードは次のとおりです。
public static TSource Single<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
if (source == null)
{
throw Error.ArgumentNull(nameof(source));
}
if (predicate == null)
{
throw Error.ArgumentNull(nameof(predicate));
}
using (IEnumerator<TSource> e = source.GetEnumerator())
{
while (e.MoveNext())
{
TSource result = e.Current;
if (predicate(result))
{
while (e.MoveNext())
{
if (predicate(e.Current))
{
throw Error.MoreThanOneMatch();
}
}
return result;
}
}
}
throw Error.NoMatch();
}
コードは可能な限り、ターゲットがIList<T>
であるかどうかもチェックするため、反復を回避できます。
public static TSource Single<TSource>(this IEnumerable<TSource> source)
{
if (source == null)
{
throw Error.ArgumentNull(nameof(source));
}
if (source is IList<TSource> list)
{
switch (list.Count)
{
case 0:
throw Error.NoElements();
case 1:
return list[0];
}
}
else
{
using (IEnumerator<TSource> e = source.GetEnumerator())
{
if (!e.MoveNext())
{
throw Error.NoElements();
}
TSource result = e.Current;
if (!e.MoveNext())
{
return result;
}
}
}
throw Error.MoreThanOneElement();
}
UPDATE
git blame の出力をチェックすると、2016年に反復最適化が適用されたことがわかります!
IList<>
最適化は、おそらくCore 2.1最適化の一部として1年前に追加されました
他の答えが指摘したように、最適化が適用されましたが、述語関数にサイドがないことを保証する方法がないという事実を元々考えて、彼らがそのようにしたという仮説を上げたいと思いますエフェクト。
そのような振る舞いが実際に使用/有用になる場合があるかどうかはわかりませんが、留意する必要があります。