【问题标题】:FindAll vs Where extension-methodFindAll vs Where 扩展方法
【发布时间】:2010-12-04 15:21:28
【问题描述】:

我只想知道“FindAll”是否会比“Where”扩展方法更快,为什么?

例子:

myList.FindAll(item=> item.category == 5);

myList.Where(item=> item.category == 5);

哪个更好?

【问题讨论】:

  • @Kevin: btw "FindAll" 不是扩展方法
  • FindAll 是 List 类型的函数,它不是 LINQ 扩展方法。

标签: c# lambda extension-methods


【解决方案1】:

好吧,FindAll 将匹配的元素复制到一个新列表中,而 Where 只是返回一个惰性求值的序列 - 不需要复制。

因此,我希望 WhereFindAll 稍微快一点,即使结果序列已被完全评估 - 当然Where 的惰性评估策略意味着如果你只看(比如说)第一场比赛,它不需要检查列表的其余部分。 (正如 Matthew 所指出的,维护Where 的状态机是有工作的。但是,这只会有固定的内存成本——而构建一个新列表可能需要多个数组分配等)

基本上,FindAll(predicate) 更接近于Where(predicate).ToList() 而不仅仅是Where(predicate)

只是为了对 Matthew 的回答做出更多反应,我认为他的测试还不够彻底。他的谓词恰好选择了一半项。这是一个简短但完整的程序,它测试同一个列表但使用三个不同的谓词 - 一个不选择任何项目,一个选择所有项目,一个选择一半。在每种情况下,我都会运行 50 次测试以获得更长的时间。

我使用Count() 来确保对Where 结果进行全面评估。结果显示,收集了大约一半的结果,两者并驾齐驱。未收集任何结果,FindAll 获胜。收集所有结果,Where 获胜。我发现这很有趣:随着找到越来越多的匹配项,所有解决方案都会变得更慢:FindAll 需要进行更多复制,Where 必须返回匹配的值,而不仅仅是在 MoveNext() 实现中循环。但是,FindAll 的速度比Where 慢,因此失去了早期的领先优势。很有趣。

结果:

FindAll: All: 11994
Where: All: 8176
FindAll: Half: 6887
Where: Half: 6844
FindAll: None: 3253
Where: None: 4891

(使用 /o+ /debug- 编译并从命令行运行,.NET 3.5。)

代码:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

class Test
{
    static List<int> ints = Enumerable.Range(0, 10000000).ToList();

    static void Main(string[] args)
    {
        Benchmark("All", i => i >= 0); // Match all
        Benchmark("Half", i => i % 2 == 0); // Match half
        Benchmark("None", i => i < 0); // Match none
    }

    static void Benchmark(string name, Predicate<int> predicate)
    {
        // We could just use new Func<int, bool>(predicate) but that
        // would create one delegate wrapping another.
        Func<int, bool> func = (Func<int, bool>) 
            Delegate.CreateDelegate(typeof(Func<int, bool>), predicate.Target,
                                    predicate.Method);
        Benchmark("FindAll: " + name, () => ints.FindAll(predicate));
        Benchmark("Where: " + name, () => ints.Where(func).Count());
    }

    static void Benchmark(string name, Action action)
    {
        GC.Collect();
        Stopwatch sw = Stopwatch.StartNew();
        for (int i = 0; i < 50; i++)
        {
            action();
        }
        sw.Stop();
        Console.WriteLine("{0}: {1}", name, sw.ElapsedMilliseconds);
    }
}

【讨论】:

  • 我不太确定你的肯定,因为如果我使用 findAll 并且我修改了我的结果集合,初始集合被修改为,所以不能是副本
  • 如果元素是值而不是引用,你会看到副本
  • 你是如何修改结果集合的?我的猜测是您正在更改其中一个元素中的数据。该列表仅包含引用(假设 T 是引用类型)。修改对象的内容并没有真正修改列表本身。您会以任何其他方式看到相同的行为。但是,FindAll 返回的 list 本身 独立于原始列表 - 例如,如果您在调用 FindAll 之后 将新元素添加到原始列表中,则不会显示在结果列表中。
  • @Cédric:您确定修改的是集合,而不是集合中的对象吗?由于返回的列表将包含对与原始列表相同的对象(但只是其中一些)的引用,因此它与您正在使用的对象实例相同。
  • 是的,我在评论中犯了一个错误,集合中的对象是相同的,但集合是一个新集合,我理解。对不起乔恩
【解决方案2】:

我们测试而不是猜测怎么样?很遗憾看到错误的答案出来了。

var ints = Enumerable.Range(0, 10000000).ToList();
var sw1 = Stopwatch.StartNew();
var findall = ints.FindAll(i => i % 2 == 0);
sw1.Stop();

var sw2 = Stopwatch.StartNew();
var where = ints.Where(i => i % 2 == 0).ToList();
sw2.Stop();

Console.WriteLine("sw1: {0}", sw1.ElapsedTicks);
Console.WriteLine("sw2: {0}", sw2.ElapsedTicks);
/*
Debug
sw1: 1149856
sw2: 1652284

Release
sw1: 532194
sw2: 1016524
*/

编辑:

即使我把上面的代码从

var findall = ints.FindAll(i => i % 2 == 0);
...
var where = ints.Where(i => i % 2 == 0).ToList();

...到...

var findall = ints.FindAll(i => i % 2 == 0).Count;
...
var where = ints.Where(i => i % 2 == 0).Count();

我得到了这些结果

/*
Debug
sw1: 1250409
sw2: 1267016

Release
sw1: 539536
sw2: 600361
*/

编辑 2.0...

如果你想要一个当前列表子集的列表最快的方法如果是FindAll()。这样做的原因是简单的。 FindAll 实例方法使用当前 List 上的索引器而不是枚举器状态机。 Where() 扩展方法是对使用枚举器的不同类的外部调用。如果您从列表中的每个节点步进到下一个节点,您将不得不在后台调用 MoveNext() 方法。正如您从上面的示例中看到的那样,使用索引条目创建一个新列表(即指向原始项目,因此内存膨胀最小)甚至只获取过滤项目的计数会更快。

现在,如果您要提前从 Enumerator 中止,Where() 方法可能会更快。当然,如果您将早期中止逻辑移至 FindAll() 方法的谓词,您将再次使用索引器而不是枚举器。

现在还有其他原因使用 Where() 语句(例如其他 linq 方法、foreach 块等等),但问题是 FindAll() 比 Where() 快。除非您不执行 Where() ,否则答案似乎是肯定的。 (比较苹果和苹果时)

我并不是说不要使用 LINQ 或 .Where() 方法。它们使代码更易于阅读。问题是关于性能,而不是你阅读和理解代码的难易程度。快速完成这项工作的最快方法是使用 for 块步进每个索引并根据需要执行任何逻辑(甚至是提前退出)。 LINQ 之所以如此出色,是因为您可以使用它们进行复杂的表达式树和转换。但是使用 .Where() 方法中的迭代器必须通过大量代码才能找到一个内存状态机,该状态机只是从 List 中获取下一个索引。还应该注意的是,这个 .FindAll() 方法只对实现它的对象有用(例如 Array 和 List。)

还有更多……

for (int x = 0; x < 20; x++)
{
    var ints = Enumerable.Range(0, 10000000).ToList();
    var sw1 = Stopwatch.StartNew();
    var findall = ints.FindAll(i => i % 2 == 0).Count;
    sw1.Stop();

    var sw2 = Stopwatch.StartNew();
    var where = ints.AsEnumerable().Where(i => i % 2 == 0).Count();
    sw2.Stop();

    var sw4 = Stopwatch.StartNew();
    var cntForeach = 0;
    foreach (var item in ints)
        if (item % 2 == 0)
            cntForeach++; 
    sw4.Stop();

    Console.WriteLine("sw1: {0}", sw1.ElapsedTicks);
    Console.WriteLine("sw2: {0}", sw2.ElapsedTicks);
    Console.WriteLine("sw4: {0}", sw4.ElapsedTicks);
}


/* averaged results
sw1 575446.8
sw2 605954.05
sw3 394506.4
/*

【讨论】:

  • +1,但请注意,如果您不需要在 Where 之后执行 ToList,它可能会更快,并且占用的内存更少。
  • 这可能是真的。但是你必须对 where 做一些事情。如果你 .ToArray 结果是一样的。如果您查看这两种方法背后的代码,您会发现哪个更有效。
  • 如果你不加入 tolist,你的结果会是什么?
  • 只是迭代它会强制对所有内容进行评估。调用 ToList 将提供不同的性能,因为这确实 迫使它执行 FindAll 所做的一切。它也不给你只在结果中迭代一半的选项。
  • 是的 - FindAll vs Where,而不是 FindAll vs Where.ToList。
【解决方案3】:

好吧,至少你可以尝试测量它。

静态Where方法是使用迭代器块(yield关键字)实现的,这基本上意味着执行将被延迟。如果你只比较这两个方法的调用,第一个会更慢,因为它立即意味着整个集合将被迭代。

但是,如果您包含所获得结果的完整迭代,情况可能会有所不同。我很确定 yield 解决方案速度较慢,因为它暗示了生成的状态机机制。 (见@Matthew anwser)

【讨论】:

  • 我不确定这两种解决方案是否具有相同的算法。
  • 你是对的,FindAll 没有使用迭代器块实现。所以它可能比静态 Where 方法慢一些。
【解决方案4】:

我可以提供一些线索,但不确定哪个更快。 FindAll() 立即执行。 Where() 被延迟执行。

【讨论】:

    【解决方案5】:

    where 的优点是延迟执行。如果您具有以下功能,请查看差异

    BigSequence.FindAll( x =>  DoIt(x) ).First();
    BigSequence.Where( x => DoIt(x) ).First();
    

    FindAll 已经覆盖了完整的序列,而大多数序列中的 Where 将在找到一个元素后立即停止枚举。

    使用 Any()、Take()、Skip() 等会产生相同的效果。我不确定,但我想你会在所有延迟执行的函数中拥有巨大的优势

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-01-09
      • 2011-01-16
      • 1970-01-01
      • 1970-01-01
      • 2015-04-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多