Linq + foreach 循环优化
本文关键字:优化 循环 foreach Linq | 更新日期: 2023-09-27 18:36:28
所以我最近发现自己写了一个类似于这个的循环:
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
一般来说,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
方法中的实际执行位置(以及所有冗余的空检查)。 您还将看到枚举对象仅循环访问一次。 堆叠where
子句将导致创建多个迭代器类(包装其前置对象),但不会导致多个枚举操作(由于延迟执行)。 请记住,当您像这样编写 Lambda 时,您实际上也在创建一个新的委托实例(也会以轻微的方式影响您的性能)。
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 实际上从满足子句的位置中提取每个元素。
类似地,标头。Select(x) 实际上不会处理任何东西,直到你把一个.ToList()
或一些东西放在它后面,迫使它进行评估。
编辑:为了进一步解释它,正如 Marcus 指出的那样,.Where
返回一个迭代器,因此每个元素都被迭代并且表达式被处理一次,如果它匹配,那么它就会进入循环的主体。
我认为第二个示例只会迭代一次字典。因为什么标题。Where(...) 返回的正好是一个"迭代器",而不是一个临时值,每次循环迭代时,它都会使用 Where(...) 中定义的过滤器,这使得一次性迭代工作。
但是,我不是一个复杂的C#编码人员,我不确定C#将如何处理这种情况,但我认为事情应该是一样的。