5

だから私は最近、これに似たループを書いていることに気づきました:

        var headers = new Dictionary<string, string>();
        ...
        foreach (var header in headers)
        {
            if (String.IsNullOrEmpty(header.Value)) continue;
            ...
        }

これは正常に機能し、辞書を1回繰り返して、必要なすべてのことを実行します。ただし、私のIDEは、これをより読みやすく最適化された代替手段として提案していますが、私は同意しません。

        var headers = new Dictionary<string, string>();
        ...
        foreach (var header in headers.Where(header => !String.IsNullOrEmpty(header.Value)))
        {
            ...
        }

しかし、それは辞書を2回繰り返すのではないでしょうか。を評価する.Where(...)ために1回、次にfor-eachループを評価するために1回?

そうでない場合、2番目のコード例は辞書を1回だけ繰り返すので、その理由と方法を説明してください。

4

3 に答える 3

9

のコードcontinueは約2倍高速です。

LINQPadで次のコードを実行しましたが、結果は一貫してwith句continueが2倍速いことを示しています。

void Main()
{
    var headers = Enumerable.Range(1,1000).ToDictionary(i => "K"+i,i=> i % 2 == 0 ? null : "V"+i);
    var stopwatch = new Stopwatch(); 
    var sb = new StringBuilder();

    stopwatch.Start();

    foreach (var header in headers.Where(header => !String.IsNullOrEmpty(header.Value)))
        sb.Append(header);
    stopwatch.Stop();
    Console.WriteLine("Using LINQ : " + stopwatch.ElapsedTicks);

    sb.Clear();
    stopwatch.Reset();

    stopwatch.Start();
    foreach (var header in headers)
    {
        if (String.IsNullOrEmpty(header.Value)) continue;
        sb.Append(header);
    }
    stopwatch.Stop();

    Console.WriteLine("Using continue : " + stopwatch.ElapsedTicks);

}

これが私が得た結果のいくつかです

Using LINQ : 1077
Using continue : 348

Using LINQ : 939
Using continue : 459

Using LINQ : 768
Using continue : 382

Using LINQ : 1256
Using continue : 457

Using LINQ : 875
Using continue : 318

一般に、LINQは、すでに評価されているものを使用するIEnumerable<T>場合、foreach対応するものよりも常に遅くなります。その理由は、LINQ-to-Objectsは、これらの低レベル言語機能の高レベルラッパーにすぎないためです。ここでLINQを使用する利点は、パフォーマンスではなく、一貫性のあるインターフェイスの提供です。LINQは絶対にパフォーマンス上の利点を提供しますが、アクティブなメモリにまだ存在しないリソースを操作しているときに効果を発揮します(実際に実行されるコードを最適化する機能を活用できます)。代替コードが最適な代替コードである場合、LINQは冗長なプロセスを実行するだけで、とにかく作成したのと同じコードを呼び出すことができます。これを説明するために、LINQを使用するときに実際に呼び出される以下のコードを貼り付けます。Whereロードされた列挙型の演算子:

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
    if (source == null)
    {
        throw Error.ArgumentNull("source");
    }
    if (predicate == null)
    {
        throw Error.ArgumentNull("predicate");
    }
    if (source is Iterator<TSource>)
    {
        return ((Iterator<TSource>) source).Where(predicate);
    }
    if (source is TSource[])
    {
        return new WhereArrayIterator<TSource>((TSource[]) source, predicate);
    }
    if (source is List<TSource>)
    {
        return new WhereListIterator<TSource>((List<TSource>) source, predicate);
    }
    return new WhereEnumerableIterator<TSource>(source, predicate);
}

そして、これがWhereSelectEnumerableIterator<TSource,TResult>クラスです。このpredicateフィールドは、Where()メソッドに渡すデリゲートです。メソッド内で実際に実行される場所MoveNext(およびすべての冗長nullチェック)が表示されます。また、列挙可能なものが1回だけループされていることもわかります。句をスタックwhereすると、複数のイテレータクラスが作成されます(先行クラスがラップされます)が、複数の列挙アクションは発生しません(実行が延期されるため)。このようなLambdaを作成すると、実際には新しいDelegateインスタンスも作成されることに注意してください(パフォーマンスにもわずかな影響があります)。

private class WhereSelectEnumerableIterator<TSource, TResult> : Enumerable.Iterator<TResult>
{
    private IEnumerator<TSource> enumerator;
    private Func<TSource, bool> predicate;
    private Func<TSource, TResult> selector;
    private IEnumerable<TSource> source;

    public WhereSelectEnumerableIterator(IEnumerable<TSource> source, Func<TSource, bool> predicate, Func<TSource, TResult> selector)
    {
        this.source = source;
        this.predicate = predicate;
        this.selector = selector;
    }

    public override Enumerable.Iterator<TResult> Clone()
    {
        return new Enumerable.WhereSelectEnumerableIterator<TSource, TResult>(this.source, this.predicate, this.selector);
    }

    public override void Dispose()
    {
        if (this.enumerator != null)
        {
            this.enumerator.Dispose();
        }
        this.enumerator = null;
        base.Dispose();
    }

    public override bool MoveNext()
    {
        switch (base.state)
        {
            case 1:
                this.enumerator = this.source.GetEnumerator();
                base.state = 2;
                break;

            case 2:
                break;

            default:
                goto Label_007C;
        }
        while (this.enumerator.MoveNext())
        {
            TSource current = this.enumerator.Current;
            if ((this.predicate == null) || this.predicate(current))
            {
                base.current = this.selector(current);
                return true;
            }
        }
        this.Dispose();
    Label_007C:
        return false;
    }

    public override IEnumerable<TResult2> Select<TResult2>(Func<TResult, TResult2> selector)
    {
        return new Enumerable.WhereSelectEnumerableIterator<TSource, TResult2>(this.source, this.predicate, Enumerable.CombineSelectors<TSource, TResult, TResult2>(this.selector, selector));
    }

    public override IEnumerable<TResult> Where(Func<TResult, bool> predicate)
    {
        return (IEnumerable<TResult>) new Enumerable.WhereEnumerableIterator<TResult>(this, predicate);
    }
}

個人的には、LINQコードの保守と再利用がはるかに簡単であるため、パフォーマンスの違いは完全に正当化できると思います。また、パフォーマンスの問題を相殺するためのことも行います(すべての匿名ラムダデリゲートと式を、共通クラスの静的読み取り専用フィールドとして宣言するなど)。しかし、実際の質問を参照すると、あなたのcontinue条項はLINQの代替案よりも間違いなく高速です。

于 2012-06-08T04:06:19.160 に答える
8

いいえ、2回繰り返されることはありません。実際にはそれ自体では評価され.Whereません。foreachは、実際には、句を満たすwhereから各要素を引き出します。

.ToList()同様に、headers.Select(x)は、評価を強制する何かを背後に置くまで、実際には何も処理しません。

編集: もう少し説明すると、マーカスが指摘したように、.Whereはイテレータを返すため、各要素が繰り返され、式が1回処理されます。一致する場合は、ループの本体に入ります。

于 2012-06-08T03:29:50.787 に答える
6

2番目の例では、dictを1回だけ繰り返すと思います。header.Where(...)が返すのは、一時的な値ではなく、正確に「イテレーター」であるため、ループが繰り返されるたびに、Where(...)で定義されたフィルターが使用されます。 1回限りの反復作業。

ただし、私は高度なC#コーダーではないため、C#がこのような状況にどのように対処するかはわかりませんが、状況は同じである必要があると思います。

于 2012-06-08T03:29:42.337 に答える