如何克服从IEnumerable <t>创建List <t>的开销?</t> </t>

时间:2009-06-22 16:49:56

标签: c# .net linq performance ienumerable

我正在使用一些LINQ选择内容来创建一些返回IEnumerable<T>的集合。

在我的情况下,我需要一个List<T>,所以我将结果传递给List<T>的构造函数来创建一个。

我想知道这样做的开销。我的馆藏中的项目通常是数百万,所以我需要考虑这个。

我认为,如果IEnumerable<T>包含ValueTypes,则表现最差。

我是对的吗? Ref类型怎么样?无论哪种方式,还有拨打费用,List<T>.Add一百万次,对吧?

有什么方法可以解决这个问题?就像我可以使用扩展方法“重载”LINQ Select这样的方法吗?

6 个答案:

答案 0 :(得分:6)

最好避免使用列表。如果你可以使用IEnumerable&lt; T&gt;来保持你的来电者,你会省去一些麻烦。

LINQ的ToList()将获取您的可枚举,并且只构造一个新的List&lt; T&gt;直接使用List&lt; T&gt;(IEnumerable&lt; T&gt;)构造函数。这与自己制作列表相同,性能明智(尽管LINQ也进行了空检查)。

如果您自己添加元素,请使用AddRange方法而不是Add。 ToList()与AddRange非常相似(因为它使用的构造函数采用IEnumerable&lt; T&gt;),在这种情况下,这通常是你最好的选择,性能明智。

答案 1 :(得分:6)

不,假设您使用IEnumerable<T>而不是IEnumerable,元素类型作为值类型没有特别的惩罚。你不会得到任何拳击。

如果实际预先知道结果的大小(Select的结果可能不会),您可能要考虑使用该大小的缓冲区创建列表,然后使用AddRange添加值。否则,每次填充时,列表都必须调整缓冲区的大小。

例如,而不是:

Foo[] foo = new Foo[100];
IEnumerable<string> query = foo.Select(foo => foo.Name);
List<string> queryList = new List<string>(query);
你可能会这样做:

Foo[] foo = new Foo[100];
IEnumerable<string> query = foo.Select(x => x.Name);
List<string> queryList = new List<string>(foo.Length);
queryList.AddRange(query);

知道调用Select将产生与原始查询源相同长度的序列,但据我所知,执行环境中没有任何内容具有该信息。

答案 2 :(得分:1)

不要将IEnumerable传递给List构造函数。 IEnumerable有一个ToList()方法,它不可能做得更糟,并且语法更好(恕我直言)。

那就是说,这只会将你的问题的答案改为“它取决于” - 特别是,它取决于IEnumerable实际上是幕后的内容。如果它恰好是List,那么ToList 实际上是免费的,当然比它是另一种类型要快得多。它仍然没有超快。

当然,解决此问题的最佳方法是尝试弄清楚如何在IEnumerable而不是List上进行处理。这可能是不可能的。


编辑:评论中的一些人正在辩论ToList()在List上调用时实际上是否会比没有更快,并且ToList()是否会比列表构造函数更快。在这一点上,推测变得毫无意义,所以这里有一些代码:

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

public static class ToListTest
{
    public static int Main(string[] args)
    {
        List<int> intlist = new List<int>();
        for (int i = 0; i < 1000000; i++)
            intlist.Add(i);

        IEnumerable<int> intenum = intlist;

        for (int i = 0; i < 1000; i++)
        {
            List<int> foo = intenum.ToList();
        }

        return 0;
    }
}

使用IEnumerable运行此代码实际上是一个List比使用LinkedList或Stack替换它快6到10倍(在我的pokey 2.4 GHz P4上,使用Mono 1.2.6)。可以想象,这可能是由于ToList()与LinkedList或Stack的枚举的特定实现之间的一些不幸的交互,但至少还有一点:速度将取决于IEnumerable的基础类型。也就是说,即使使用List作为源代码,我仍然需要6秒才能进行1000个ToList()调用,所以它远非免费。

接下来的问题是ToList()是否比List构造函数更智能。答案结果证明是:List构造函数与ToList()一样快。事后看来,Jon Skeet的推理是有道理的 - 我只是忘记了ToList()是一种扩展方法。我仍然(很多)更喜欢ToList()语法,但没有使用它的性能原因。

所以简短的版本是最好的答案仍然是“如果可以避免它就不要转换为List”。除此之外,实际表现将大大取决于IEnumerable实际上是什么,但充其量只是缓慢,而不是冰川。我修改了原来的答案以反映这一点。

答案 3 :(得分:1)

一般来说,返回IEnumerable的方法不必在实际需要项目之前评估任何项目。所以,从理论上讲,当你返回IEnumerable时,你们当中没有任何物品需要存在。

因此创建一个列表意味着你真的需要评估项目,获取它们并将它们放在内存中(至少是它们的引用)。没有什么可以做的 - 如果真的需要有一个列表。

答案 4 :(得分:1)

其他一些响应者已经提出了如何提高将IEnumerable<T>复制到List<T>的效果的想法 - 我认为在这方面不能添加太多。

然而,根据你所描述的你需要对结果做的事情,以及当你完成时你摆脱列表的事实(我认为这意味着中间结果不是很有趣) - 你可以想要考虑是否真的需要实现List<T>

考虑为List<T>编写一个执行相同处理逻辑的延迟扩展方法,而不是创建IEnumerable<T>并对该列表的内容进行操作。我在很多情况下都是这样做的,在使用编译器支持的[yield return][1]语法时,用C#编写这样的逻辑并不是那么糟糕。

如果你要做的就是访问结果中的每个项目并从中收集一些信息,这种方法很有效。通常,您需要做的只是按需访问集合中的每个元素,使用它进行一些处理,然后继续。这种方法通常具有更高的可扩展性和性能,可以创建集合的副本,只是为了迭代它。

现在,由于其他原因,此建议可能对您不起作用,但值得考虑作为替代方法,找到实现非常大的列表的最有效方法。

答案 5 :(得分:1)

通过阅读各种评论和问题,我得到以下要求

对于需要在该集合中运行的数据集合,过滤掉一些对象,然后对其余对象执行一些转换。如果是这样的话,你可以这样做:

var result = from item in collection
             where item.Id > 10 //or some more sensible condition
             select Operation(item);

如果您需要执行更多过滤和转换,您可以嵌套LINQ查询,如

var result = from filteredItem in (from item in collection
                                  where item.Id > 10 //or some more sensible condition
                                  select Operation(item))
                 where filteredItem.SomePropertyAvailableAfterFirstTransformation == "new"
                 select SecondTransfomation(filteredItem);