[英]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.