【问题标题】:How can I overcome the overhead of creating a List<T> from an IEnumerable<T>?如何克服从 IEnumerable<T> 创建 List<T> 的开销?
【发布时间】:2009-06-22 16:49:56
【问题描述】:

我正在使用一些 LINQ 选择内容来创建一些集合,这些集合返回 IEnumerable&lt;T&gt;

就我而言,我需要一个List&lt;T&gt;,所以我将结果传递给List&lt;T&gt; 的构造函数来创建一个。

我想知道这样做的开销。我收藏的物品通常以数百万计,所以我需要考虑这一点。

我假设,如果IEnumerable&lt;T&gt; 包含ValueTypes,这是最差的性能。

我说的对吗? Ref 类型呢?无论哪种方式,都需要调用List&lt;T&gt;.Add 一百万次的成本,对吧?

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

【问题讨论】:

  • 为什么需要一个包含一百万个项目的列表?除了枚举之外,您的调用者还会做其他事情吗?如果没有,那么调用者可能需要更改为使用 IEnumerable.
  • 因为我有 1M+ 件或更多。我没有编写主 API,但这里和那里都有一些索引。
  • @Joan:我只是建议他们重新考虑他们是否真的需要一次将所有数百万个项目都存储在内存中的问题。例如,最好将它们留在数据库中,或者一次性完成所有处理,允许它们留在 IEnumerable 中。列表在构建后是否真的被修改过?
  • 基本上当我有列表时,我必须创建一个新的子列表并修改它们。就像:10M 元素 -> 变成 1M -> 做一些操作 -> 结果列表。元素是不可变的,在得到我想要的结果后我就去掉了列表,以及这个例子的比喻数量。
  • @Joan:如果他们不使用 List 超出 IEnumerable 的功能,那么他们当然应该重构。这就是 ReSharper 付出的代价:它会告诉您可以使用基类或接口的方法。

标签: c# .net linq performance ienumerable


【解决方案1】:

不,如果您使用的是IEnumerable&lt;T&gt; 而不是IEnumerable,则元素类型是值类型并没有特别的惩罚。你不会参加任何拳击比赛。

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

例如,而不是做:

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 将产生与原始查询源长度相同的序列,但据我所知,执行环境中没有任何信息。 p>

【讨论】:

  • 谢谢乔恩。对于值类型,我认为它们可能会为每个元素再次复制到新列表中。不是这样吗?也请你解释一下你的最后一句话。我没明白。使用“from i in collection where ... Select 怎么样?”都一样吧?
  • 列表本身不会因为您更改了列表中引用所引用的对象的内容而被修改。这么说吧 - 如果有人有一份房屋地址列表,那么如果有人在房子里添加了一些家具,该列表会改变吗?
  • @Joan:是的,它会的。如果您使用的是引用类型,它会创建一个新的引用列表,但它们仍然指向相同的原始对象。如果您想要不同的行为,则需要明确替换新列表中的完整对象。即:result[10] = new ResultType(5, result[10].Y, result[10].Z, ...);或者,您需要执行某种形式的深拷贝操作,在其中克隆元素,而不是仅仅复制对它们的引用。
  • @Joan Venge:不,两者几乎相同。如果您需要完整的深拷贝,则需要实现自己的深拷贝。对于引用类型,这将意味着 100 万次构造函数调用......使用 Select 并不会真正改变任何东西,而只是直接执行这些操作。
  • 不,在每种情况下,序列中的值都会被复制 - 对于值类型,这些值是实际数据(数字等)。对于引用类型,它们是引用。见pobox.com/~skeet/csharp/references.html
【解决方案2】:

最好避免需要列表。如果你可以让你的调用者使用 IEnumerable,你会省去一些麻烦。

LINQ 的 ToList() 将获取您的枚举,并使用 List(IEnumerable) 构造函数直接从中构造一个新的 List。这与自己制作列表一样,在性能方面(尽管 LINQ 也会进行空值检查)。

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

【讨论】:

  • 谢谢里德。当您说“如果您的 IEnumerable 已经是 IList”时,您的意思是 LINQ 中有一些方法返回 IList?
  • 老鼠,一半的信息被剪掉了。意思是说 IIRC Enumerable.ToList 类型返回 List,而不是 IList
  • “除非那里已经有一个列表”是什么意思?我希望 Enumerable.ToList 总是创建一个新的、独立的源数据副本不管原始序列的类型。
  • @Joan Venge:不,这是运行时检查。采用 IEnumerable 的 List 构造函数对 ICollection 进行强制转换(即:ICollection coll = source as ICollection),如果是 ICollection,则直接使用 Count 属性预分配,然后使用 CopyTo 而不是枚举所有元素。如果源是列表(或任何其他 ICollection 实现),这可能会使其更快。
  • @VinneyK:是的,它总是会创建一个新列表。
【解决方案3】:

一般来说,返回IEnumerable 的方法不必在实际需要项目之前评估任何项目。因此,理论上,当您返回 IEnumerable 时,此时您不需要存在任何项目。

因此,创建列表意味着您确实需要评估项目,获取它们并将它们放置在内存中的某个位置(至少是它们的引用)。对此无能为力 - 如果您真的需要一份清单。

【讨论】:

    【解决方案4】:

    许多其他响应者已经提供了有关如何提高将 IEnumerable&lt;T&gt; 复制到 List&lt;T&gt; 的性能的想法 - 我认为在这方面可以添加的内容不多。

    但是,根据您所描述的内容,您需要对结果进行处理,以及您在完成后摆脱列表的事实(我认为这意味着中间结果并不有趣) - 您可能想考虑你是否真的需要实现一个List&lt;T&gt;

    与其创建List&lt;T&gt; 并对该列表的内容进行操作,不如考虑为IEnumerable&lt;T&gt; 编写一个执行相同处理逻辑的惰性扩展方法。我自己在很多情况下都这样做过,在使用编译器支持的[yield return][1] 语法时,用 C# 编写这样的逻辑还不错。

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

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

    【讨论】:

      【解决方案5】:

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

      也就是说,这只会将您的问题的答案更改为“取决于” - 特别是,它取决于 IEnumerable 实际上在幕后是什么。如果它恰好已经是一个 List,那么 ToList 将实际上是免费的,当然 会比其他类型快得多。它仍然不是超级快。

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


      编辑:cmets 中的一些人正在争论 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;
          }
      }
      

      使用实际上是 List 的 IEnumerable 运行此代码比我用 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(),但没有使用它的性能理由。

      所以简短的版本是最好的答案仍然是“如果可以避免的话,不要转换为列表”。除此之外,实际性能将在很大程度上取决于 IEnumerable 的实际情况,但充其量它会很缓慢,而不是冰冷的。我已经修改了我原来的答案以反映这一点。

      【讨论】:

      • IEnumerable 本身没有 ToList - 只是有一个扩展方法来进行转换。该扩展方法不能拥有比 List 构造函数更多(或更少)的信息,因此我看不出有任何理由认为它的性能会更好或更差。
      • (而且我不相信在现有 List 上调用 ToList 扩展方法是免费的。它不应该,因为它应该创建一个 独立副本 i> 的序列数据。)
      • @Jon:与必须将所有项目复制到列表中的构造函数相比,ToList 可以尝试首先强制转换 IList,并且只有在确实需要时才实际创建一个新列表。因此,根据 IEnumerable 实例的具体情况,它可以完全避免复制任何内容。
      • 我刚刚检查过,文档并不像我希望的那样清晰 - 但 ToList is 声明返回 List,而不是只是 IList。我也强烈相信任何创建独立副本的实现都会由于不一致而导致广泛的混乱。
      • 不,我的意思是更改一个列表(例如删除一个项目)不会影响另一个。
      【解决方案6】:

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

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

      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);
      

      【讨论】:

      • 谢谢你的第二个例子。如果嵌套选择在外部选择之外会更快吗?就像:collection = ... select Operation(item) from filtereditem in collection ... ?
      • 查询的顺序将取决于每个完成的工作,越早完成的工作越好。这基本上是尽可能将工作从内循环移动到外循环的规则。 (注意:嵌套查询不是嵌套循环,但逻辑仍然适用,因为传递给第二个查询的元素列表将受到第一个查询中 where 子句的限制)
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-02-24
      • 1970-01-01
      • 1970-01-01
      • 2019-07-01
      相关资源
      最近更新 更多