【问题标题】:where lambda vs. first lambda其中 lambda 与第一个 lambda
【发布时间】:2013-01-11 01:03:14
【问题描述】:

假设我有一些字符串:

string[] strings = { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" };

有什么区别:

string startsWithO = strings.First(s => s[0] == 'o');

还有:

string startsWithO = strings.Where(s => s[0] == 'o').First();

既然 Where() 被延迟,它不应该减慢执行速度,对吧?

【问题讨论】:

  • 我 99% 确定它们在编译时会变成完全相同的东西,但我会让有把握的人回答
  • 我自己的问题可能会回答你的问题:stackoverflow.com/questions/10110013/…
  • 我似乎发现了一个微小的区别: First() 似乎没有采用索引(即 First( (s, index) => (s[0] == 'o') && (索引 == 1)))
  • 这是一个公平的观点,尽管没有什么可以阻止你自己实现 First :p

标签: .net linq c#-3.0


【解决方案1】:

使用.Where(filter).First() 而不是.First(filter) 的性能损失通常非常很小。

但是,它们并不相同 - Where 生成一个新的迭代器,First 可以简单地获取其中的一个元素,而 First(filter) 可以通过仅使用一个循环和在 filter 匹配时直接返回来进行微优化.

因此,虽然这两种方法具有相同的语义并且都同样频繁地执行 filter(仅在必要时执行),但使用带有 filter 参数的 First 不需要创建中间迭代器对象并且 可能也避免了对该迭代器的一些非常简单的方法调用。

换句话说,如果您正在执行这样的代码数百万次,您会看到轻微的性能差异 - 但没有什么大不了的;我会永远担心它。每当这种微小的性能差异实际上很重要时,您最好只编写等效的(非常简单的)foreach-with-if 语句并避免 LINQ 中固有的额外调用和对象分配 - 但请记住,这是一个微优化,您将很少需要。

编辑:基准演示效果:

这需要 0.78 秒:

for(int i=0;i<10*1000*1000;i++)
  Enumerable.Range(0,1000).First(n=> n > 2);
GC.Collect();

但这需要 1.41 秒:

for(int i=0;i<10*1000*1000;i++)
  Enumerable.Range(0,1000).Where(n=> n > 2).First();
GC.Collect();

而普通循环要快得多(0.13 秒):

long bla = 0;
for(int i=0;i<10*1000*1000;i++)
    for(int n=0;n<1000;n++)
        if(n > 2) { bla+=n; break; }
GC.Collect();
Console.WriteLine(bla);//avoid optimizer cheating.

请注意,此基准仅显示如此极端的差异,因为我有一个普通的过滤器和一个非常短的不匹配前缀。

根据一些快速实验,差异似乎很大程度上归因于采用哪个代码路径的细节。因此,对于数组和List&lt;&gt;s,第一个变体实际上更快,可能在.Where 中对First 没有的那些类型进行特殊处理;对于自定义迭代器,第二个版本比预期的要快一点。

总结:

.Where(...).First().First(...) 大致一样快 - 不要费心选择其中一个作为优化。在一般中,.First(...) 会稍微快一些,但在某些常见情况下会更慢。如果你真的需要微优化,那么使用比任何一个都快的普通循环。

【讨论】:

  • 是的,在 LINQ to SQL 中,您可能会看到生成完全相同的 SQL,从而获得相同的数据库性能。但是,您可能在 SQL 生成性能上有完全不相关的微小差异;可能是可测量的,但在各个方面都被数据库查询的开销所淹没。
  • 有趣的答案@Eamon。我想回报太小了,无法在 Tim Schmelter 的(上面的链接)测试中显示出来。
  • 不错的基准测试!我想知道为什么 @Eamon 的基准测试在高达 2 倍时没有显示出差异。
  • @micahhoover:Tim 的基准测试有一个慢得多的过滤器 - string.Contains,它可能会主导基准测试时间。此外,Tim 的场景并没有明确地 GC,我怀疑微小的对象和 GC 开销很容易影响这个基准,因为当你想检测差异时可能会创建很多。
【解决方案2】:

这里没有区别。

调用 Where first 返回一个迭代器,直到 First 开始循环它。

如果谓词不匹配任何元素,则抛出相同的异常 InvalidOperationException。

唯一的区别是代码的冗长,所以 .First 没有 .Where 应该是首选

【讨论】:

    【解决方案3】:

    在特定情况下,在string[] 上调用FirstWhere,调用的方法是Enumerable.WhereEnumerable.Firstextension 方法。

    Enumerable.Where 这样做:

    public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) 
    {
      // null checks omitted
      if (source is TSource[]) 
         return new WhereArrayIterator<TSource>((TSource[])source, predicate); 
      //the rest of the method will not execute
    }
    

    WhereArrayIterator 的构造函数只是:

    public WhereArrayIterator(TSource[] source, Func<TSource, bool> predicate) {
      this.source = source; 
      this.predicate = predicate;
    } 
    

    所以这里实际上什么也没做,除了创建一个迭代器。

    第一个First 方法,没有谓词这样做:

    public static TSource First<TSource>(this IEnumerable<TSource> source) { 
      //null check
      IList<TSource> list = source as IList<TSource>;
      if (list != null) {
         //this branch is not taken as string[] does not implement IList<string>
         if (list.Count > 0) return list[0]; 
      }
      else { 
        //this is actually the WhereArrayIterator from before
        using (IEnumerator<TSource> e = source.GetEnumerator()) { 
          if (e.MoveNext()) 
            return e.Current;
        } 
      }
      throw Error.NoElements();
    }
    

    但是,第二个First 会这样做

    public static TSource First<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) {
       //null checks
       foreach (TSource element in source) {
         if (predicate(element)) return element; 
       }
       throw Error.NoMatch();
    }
    

    在数组的情况下,它与直接线性访问一样快。
    简而言之,这意味着在数组上调用First(predicate) 会更快一些,速度不大,但仍然可以检测到。这可能不适用于列表,当然也不适用于 IQueryable 对象,这是一个完全不同的故事。

    但是,这是最糟糕的微优化。除非这样做数百万次,否则不会节省太多秒。即使我现在知道这一点,我仍然会使用更清晰易读的内容。

    【讨论】:

    • 实际上,根据我在我的机器上对阵列和List&lt;&gt;s 的测量结果,特别是Where 然后First 解决方案稍微更快
    • 在我的机器上,对于 100M 的数组迭代,.Where.First 需要 14.5 秒,.First 需要 13 秒,而显式 while 循环需要 1.5 秒
    • 是的,这更有意义 - 所以你在外循环中进行了 100M 次迭代 - 但在内循环中有多少次? .Where.First 似乎随着最里面的循环变得更长(即需要更多的迭代来找到第一个元素)而获得支持。交叉点大约有 11 个元素;在 1000 个元素时,First(...) 接近两倍。
    猜你喜欢
    • 1970-01-01
    • 2020-11-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-01-08
    • 1970-01-01
    相关资源
    最近更新 更多