Linq + foreach循环优化

cae*_*say 5 c# linq optimization

所以我最近发现自己写了一个类似于这个的循环:

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

哪个工作正常,它遍历字典一次,并完成我需要它做的所有事情.但是,我的IDE建议将其作为更具可读性/优化的替代方案,但我不同意:

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

但是不会两次遍历字典吗?一次评估.Where(...),然后一次为for-each循环?

如果没有,并且第二个代码示例仅迭代字典一次,请解释原因和方法.

sma*_*man 9

代码的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);

}
Run Code Online (Sandbox Code Playgroud)

以下是我得到的一些结果

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
Run Code Online (Sandbox Code Playgroud)

一般情况下,使用已经评估过的LINQ总是IEnumerable<T>foreach对应的慢.原因是LINQ-to-Objects只是这些低级语言功能的高级包装器.这里使用LINQ的好处不是性能,而是提供一致的接口.LINQ绝对可以提供性能优势,但是当您使用尚未处于活动内存中的资源时,它们会发挥作用(并允许您利用优化实际执行的代码的能力).当替代代码是最佳替代方案时,LINQ只需要通过冗余过程来调用您原本应该编写的相同代码.为了说明这一点,我将粘贴以下代码,当您Where在加载的枚举上使用LINQ的运算符时,实际调用该代码:

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);
}
Run Code Online (Sandbox Code Playgroud)

这是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);
    }
}
Run Code Online (Sandbox Code Playgroud)

我个人认为性能差异是完全合理的,因为LINQ代码更容易维护和重用.我也做了一些事情来抵消性能问题(比如将我的所有匿名lambda委托和表达式声明为公共类中的静态只读字段).但是在参考您的实际问题时,您的continue子句肯定比LINQ替代方案更快.


Joe*_*Joe 8

不,它不会两次迭代它.在.Where实际上并不自行评估.foreach实际上从满足该条款的地方拉出每个元素.

类似地,header.Select(x)实际上并不处理任何事情,直到你.ToList()在它后面放置一个强迫它评估的东西.

编辑: 为了解释它,正如Marcus指出的那样,.Where返回一个迭代器,因此每个元素都被迭代并且表达式被处理一次,如果匹配则它进入循环体.


Mar*_*cus 6

我认为第二个例子只会迭代一次dict.因为header.Where(...)返回的只是一个"迭代器",而不是一个临时值,每次循环迭代时,它将使用在Where(...)中定义的过滤器,这使得一次性迭代工作.

但是,我不是一个复杂的C#编码器,我不确定C#将如何处理这种情况,但我认为事情应该是一样的.