【问题标题】:Linq + foreach loop optimizationLinq + foreach 循环优化
【发布时间】:2012-06-12 03:34:00
【问题描述】:

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

        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循环?

如果不是,并且第二个代码示例只迭代字典一次,请说明原因和方法。

【问题讨论】:

  • 第二个将迭代选定的值
  • 至于可读性。尝试将 linq 查询存储在本地并对其进行枚举。一条“线”,一种功能。

标签: c# linq optimization


【解决方案1】:

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&lt;T&gt; 时总是比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&lt;TSource,TResult&gt; 类。 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 替代方案快。

【讨论】:

  • +1 是一个很好的答案,而 continue 版本对我来说几乎快 10 倍。但请注意,问题是“这会迭代两次吗?”;答案是“不会”。
  • @Kirk Broadhurst,如果您阅读我的全部答案,您会看到“您还会看到可枚举仅循环一次。堆叠 where 子句将导致创建多个迭代器类(包装它们的前辈),但不会导致多个枚举操作(由于延迟执行)。"
  • 我正在解决最后一行 - “但就您的实际问题而言,您的 continue 子句肯定比 LINQ 替代方案更快。”这不是参考,也没有回答实际问题。
  • @KirkBroadhurst,我的错。我的印象是关心答案的人会阅读全文。
  • 是的,但是 tl;博士。与此同时,我 +1 了你的答案,其他人都没有,所以别担心,要开心。
【解决方案2】:

不,它不会遍历它两次。 .Where 本身并不实际评估。 foreach 实际上是从满足子句的 where 中取出每个元素。

类似地,headers.Select(x) 实际上不会处理任何内容,直到您放置 .ToList() 或在其后面强制其评估的内容。

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

【讨论】:

  • 你的意思是说Select 做了一个Deferred Execution?
  • 是的,我不知道该行为的技术术语。但在这种情况下,重要的不是延迟执行,而是这两个 linq 方法都遍历它们的集合而不是返回整个集合,从而节省了 OP 想知道的第二次迭代。
【解决方案3】:

我认为第二个示例只会迭代 dict 一次。 因为 header.Where(...) 返回的正是一个“迭代器”,而不是一个临时值,所以每次循环迭代时,它都会使用 Where(...) 中定义的过滤器,这使得一次性迭代工作。

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

【讨论】:

  • 是的,我相信这就是它的工作原理,否则,我的 blah.Where(x).Where(y).Where(z) 将非常效率低下。 . ;)
  • 我认为 C# 将像您的手写代码一样工作,因此两者的开销几乎没有区别 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-30
  • 1970-01-01
  • 1970-01-01
  • 2016-06-23
  • 2013-01-03
相关资源
最近更新 更多