簡體   English   中英

Linq + foreach循環優化

[英]Linq + foreach loop optimization

所以我最近發現自己寫了一個類似於這個的循環:

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

哪個工作正常,它遍歷字典一次 ,並完成我需要它做的所有事情。 但是,我的IDE建議將其作為更具可讀性/優化的替代方案,但我不同意:

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

但是不會兩次遍歷字典嗎? 一次評估.Where(...)然后一次評估for-each循環?

如果沒有,並且第二個代碼示例僅迭代字典一次,請解釋原因和方法。

帶有continue的代碼大約快兩倍。

我在LINQPad中運行了以下代碼,結果一致地說,帶有continue的子句的速度是后者的兩倍。

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

通常,在使用已經評估的IEnumerable<T> ,LINQ總是會比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方法中看到它實際執行的位置(以及所有冗余空值檢查)。 您還將看到可枚舉只循環一次。 堆疊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代碼更容易維護和重用。 我也做了一些事情來抵消性能問題(比如將我的所有匿名lambda委托和表達式聲明為公共類中的靜態只讀字段)。 但是在參考您的實際問題時,您的continue子句肯定比LINQ替代方案更快。

不,它不會兩次迭代它。 .Where實際上並沒有自己評估。 foreach實際上從滿足該條款的地方拉出每個元素。

類似地,headers.Select(x)實際上並不處理任何事情,直到你在其后面放置一個強制它進行評估的.ToList()或其他東西。

編輯:為了解釋它,正如Marcus所指出的, .Where返回一個迭代器,因此每個元素都被迭代並且表達式被處理一次,如果匹配則它進入循環體。

我認為第二個例子只會迭代一次dict。 因為header.Where(...)返回的只是一個“迭代器”,而不是一個臨時值,每次循環迭代時,它將使用在Where(...)中定義的過濾器,這使得一次性迭代工作。

但是,我不是一個復雜的C#編碼器,我不確定C#將如何處理這種情況,但我認為事情應該是一樣的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM