8

いくつかの文字列があるとします:

string[] strings = { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" };

違いは何ですか:

string startsWithO = strings.First(s => s[0] == 'o');

と:

string startsWithO = strings.Where(s => s[0] == 'o').First();

Where() は延期されているため、実行が遅くなることはありませんよね?

4

3 に答える 3

12

.Where(filter).First()ではなくを使用することによるパフォーマンスの低下.First(filter)は、通常は非常に小さいものです。

ただし、それらは同じではありません。単純に の 1 つの要素を取得できるWhere新しいイテレータを生成しますが、一致する場合は常に 1 つのループと直接リターンを使用してマイクロ最適化できます。FirstFirst(filter)filter

したがって、どちらのアプローチも同じセマンティクスを持ち、同じfilter頻度で (必要な頻度でのみ) を実行しますがFirstfilterパラメーターを使用して中間イテレーター オブジェクトを作成する必要はなく、おそらくそのイテレーターでの非常に単純なメソッド呼び出しも回避できます。

つまり、このようなコードを何百万回も実行すると、わずかなパフォーマンスの違いが見られますが、大きな違いはありません。私はそれについて心配することはありません。その小さなパフォーマンスの違いが実際に問題になるときはいつでも、同等の (非常に単純な) foreach-with-if ステートメントを記述し、LINQ に固有の余分な呼び出しとオブジェクトの割り当てを回避する方がはるかに優れています。ただし、これはマイクロ最適化であることを忘れないでください。めったに必要ありません。

編集:効果を示すベンチマーク:

これには 0.78 秒かかります。

for(int i=0;i<10*1000*1000;i++)
  Enumerable.Range(0,1000).First(n=> n > 2);
GC.Collect();

しかし、これには 1.41 秒かかります。

for(int i=0;i<10*1000*1000;i++)
  Enumerable.Range(0,1000).Where(n=> n > 2).First();
GC.Collect();

単純なループははるかに高速ですが (0.13 秒):

long bla = 0;
for(int i=0;i<10*1000*1000;i++)
    for(int n=0;n<1000;n++)
        if(n > 2) { bla+=n; break; }
GC.Collect();
Console.WriteLine(bla);//avoid optimizer cheating.

些細なフィルターと非常に短い一致しないプレフィックスがあるため、このベンチマークは極端な違いしか示していないことに注意してください。

いくつかの簡単な実験に基づくと、違いは主にどのコードパスが取得されるかの詳細に起因するようです。したがって、配列とList<>s の場合、最初のバリアントは実際には高速であり、持っていない.Where型に対して特別なケースを行う可能性があります。Firstカスタム イテレータの場合、予想どおり、2 番目のバージョンの方がわずかに高速です。

概要:

.Where(...).First()ほぼ同じくらい高速です .First(...)-最適化としてどちらかを選択する必要はありません。一般的 .First(...)には非常にわずかに高速ですが、一般的なケースでは遅くなります。そのマイクロ最適化が本当に必要な場合は、どちらよりも高速な単純なループを使用してください。

于 2013-01-10T16:09:39.360 に答える
1

特定のケースでは、とを呼び出すFirstWherestring[]呼び出されるメソッドはEnumerable.WhereEnumerable.First拡張メソッドです。

Enumerable.Whereこれを行います:

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) 
{
  // null checks omitted
  if (source is TSource[]) 
     return new WhereArrayIterator<TSource>((TSource[])source, predicate); 
  //the rest of the method will not execute
}

のコンストラクタは次のことをWhereArrayIterator行います。

public WhereArrayIterator(TSource[] source, Func<TSource, bool> predicate) {
  this.source = source; 
  this.predicate = predicate;
} 

したがって、イテレータを作成することを除いて、ここでは実際には何も行われません。

First述語のない最初の方法はこれを行います:

public static TSource First<TSource>(this IEnumerable<TSource> source) { 
  //null check
  IList<TSource> list = source as IList<TSource>;
  if (list != null) {
     //this branch is not taken as string[] does not implement IList<string>
     if (list.Count > 0) return list[0]; 
  }
  else { 
    //this is actually the WhereArrayIterator from before
    using (IEnumerator<TSource> e = source.GetEnumerator()) { 
      if (e.MoveNext()) 
        return e.Current;
    } 
  }
  throw Error.NoElements();
}

しかし、2番目Firstはこれを行います

public static TSource First<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) {
   //null checks
   foreach (TSource element in source) {
     if (predicate(element)) return element; 
   }
   throw Error.NoMatch();
}

アレイの場合、これは直接線形アクセスと同じくらい高速です。
一言で言えば、これはFirst(predicate)、配列の呼び出しが、大きくはないが、それでも検出可能な要因によって、いくらか高速になることを意味します。これはリストには当てはまらないかもしれませんしIQueryable、まったく別の話であるオブジェクトにも当てはまらないでしょう。

ただし、これは最悪の場合、マイクロ最適化です。これが何百万回も行われない限り、それはあまり多くの秒を節約しません。私は今これを知っていますが、私はまだ読んで理解するためにより明確なものを使用します。

于 2013-01-10T16:49:44.340 に答える
1

ここに違いはありません。

Where first を呼び出すと、First がループを開始するまで使用されない反復子が返されます。

述語がどの要素とも一致しない場合、同じ例外 InvalidOperationException がスローされます。

唯一の違いはコードの冗長性です。

于 2013-01-10T16:09:11.587 に答える