【问题标题】:Is there a performance impact when calling ToList()?调用 ToList() 时是否会影响性能?
【发布时间】:2013-03-09 03:06:04
【问题描述】:

使用ToList()时,是否需要考虑性能影响?

我正在编写一个查询来从目录中检索文件,这就是查询:

string[] imageArray = Directory.GetFiles(directory);

但是,因为我喜欢使用List<>,所以我决定加入...

List<string> imageList = Directory.GetFiles(directory).ToList();

那么,在决定进行这样的转换时是否应该考虑某种性能影响 - 还是只在处理大量文件时才考虑?这是可以忽略不计的转换吗?

【问题讨论】:

  • +1 也想知道这里的答案。恕我直言,除非应用程序对性能至关重要,否则我想我总是使用 List<T> 来支持 T[] 如果它使代码更具逻辑性/可读性/可维护性(当然,除非转换 导致明显性能问题,在这种情况下我想我会重新访问它)。
  • 从数组创建列表应该非常便宜。
  • @Sepster 我只指定我需要做的工作的具体数据类型。如果我不必致电AddRemove,我会将其保留为IEnumerable<T>(或者更好的是var
  • 我认为,这种情况下最好调用EnumerateFiles而不是GetFiles,这样只会创建一个数组。
  • GetFiles(directory),因为它目前是在 .NET 中实现的,所以 new List<string>(EnumerateFiles(directory)).ToArray() 几乎可以做到。所以GetFiles(directory).ToList() 创建一个列表,从中创建一个数组,然后再次创建一个列表。就像 2kay 说的那样,你应该更喜欢在这里做EnumerateFiles(directory).ToList()

标签: c# arrays performance list


【解决方案1】:

IEnumerable.ToList()

是的,IEnumerable<T>.ToList() 确实会对性能产生影响,这是一个 O(n) 操作,尽管它可能只需要在性能关键操作中引起注意。

ToList() 操作将使用List(IEnumerable<T> collection) 构造函数。此构造函数必须制作数组的副本(更普遍的是IEnumerable<T>),否则原始数组的未来修改将在源T[] 上发生更改,这通常也是不可取的。

我想重申,这只会对一个巨大的列表产生影响,复制内存块是一个相当快速的操作。

方便的提示,AsTo

您会注意到,在 LINQ 中有几个方法以 As(如 AsEnumerable())和 To(如 ToList())开头。以To 开头的方法需要进行上述转换(即可能会影响性能),而以As 开头的方法不需要并且只需要一些转换或简单操作。

List<T> 的其他详细信息

这里有更多关于List<T> 工作原理的详细信息,以防您感兴趣:)

List<T> 还使用一种称为动态数组的构造,该构造需要按需调整大小,此调整大小事件将旧数组的内容复制到新数组。所以它从小开始,increases in size if required

这是List<T> 上的CapacityCount 属性之间的区别。 Capacity 是指幕后数组的大小,CountList<T> 中的项目数,始终为 <= Capacity。因此,当将一个项目添加到列表中时,将其增加到超过 Capacity 时,List<T> 的大小会加倍并复制数组。

【讨论】:

  • 我只是想强调List(IEnumerable<T> collection) 构造函数检查集合参数是否为ICollection<T>,然后立即创建一个具有所需大小的新内部数组。如果参数集合不是ICollection<T>,则构造函数遍历它并为每个元素调用Add
  • 请务必注意,您可能经常将 ToList() 视为一种具有误导性的要求操作。当您通过 LINQ 查询创建 IEnumerable 时会发生这种情况。 linq 查询已构造但未执行。调用 ToList() 将运行查询,因此看起来是资源密集型的 - 但它是密集型查询而不是 ToList() 操作(除非它是一个非常大的列表)
【解决方案2】:

调用 toList() 对性能有影响吗?

当然可以。从理论上讲,即使i++ 也会对性能产生影响,它可能会使程序减慢几个滴答声。

.ToList 有什么作用?

当您调用.ToList 时,代码会调用Enumerable.ToList(),这是return new List<TSource>(source) 的扩展方法。在对应的构造函数中,最坏的情况会遍历item container,将它们一个一个添加到一个新的容器中。所以它的行为对性能影响很小。不可能成为您的应用程序的性能瓶颈。

问题中的代码有什么问题

Directory.GetFiles 遍历文件夹并将所有文件的名称​​立即返回到内存中,字符串[] 可能会占用大量内存,从而减慢一切速度。

那该怎么办

这取决于。如果您(以及您的业务逻辑)保证文件夹中的文件量总是很小,那么代码是可以接受的。但仍然建议使用惰性版本:C#4 中的Directory.EnumerateFiles。这更像是一个查询,不会立即执行,您可以在其上添加更多查询,例如:

Directory.EnumerateFiles(myPath).Any(s => s.Contains("myfile"))

一旦找到名称包含“myfile”的文件,就会停止搜索路径。这显然比.GetFiles 有更好的性能。

【讨论】:

    【解决方案3】:

    调用 toList() 对性能有影响吗?

    是的,有。使用扩展方法Enumerable.ToList() 将从IEnumerable<T> 源集合构造一个新的List<T> 对象,这当然会影响性能。

    但是,了解List<T> 可能有助于您确定性能影响是否显着。

    List<T> 使用数组 (T[]) 来存储列表的元素。数组一旦分配就无法扩展,因此List<T> 将使用一个过大的数组来存储列表的元素。当List<T> 增长到超过底层数组的大小时,必须分配一个新数组,并且必须将旧数组的内容复制到新的更大的数组中,然后列表才能增长。

    当从IEnumerable<T> 构造新的List<T> 时,有两种情况:

    1. 源集合实现ICollection<T>:然后ICollection<T>.Count用于获取源集合的确切大小,并在将源集合的所有元素复制到后备数组之前分配匹配的后备数组使用ICollection<T>.CopyTo()。此操作非常有效,并且可能会映射到一些用于复制内存块的 CPU 指令。但是,就性能而言,新数组需要内存,并且复制所有元素需要 CPU 周期。

    2. 否则源集合的大小是未知的,IEnumerable<T> 的枚举器用于将每个源元素一次添加到新的List<T>。最初,后备数组为空,并创建了一个大小为 4 的数组。然后,当这个数组太小时,大小会加倍,因此后备数组会像 4、8、16、32 等一样增长。每次后备数组增长时,都必须重新分配它,并且必须复制到目前为止存储的所有元素。与可以立即创建正确大小的数组的第一种情况相比,此操作的成本要高得多。

      此外,如果您的源集合包含 33 个元素,则列表最终将使用 64 个元素的数组,这会浪费一些内存。

    在您的情况下,源集合是一个实现 ICollection<T> 的数组,因此除非您的源数组非常大,否则您不应该担心性能影响。调用ToList() 将简单地复制源数组并将其包装在List<T> 对象中。对于小型集合,即使是第二种情况的性能也不值得担心。

    【讨论】:

      【解决方案4】:

      它会像这样做一样(低效):

      var list = new List<T>(items);
      

      如果你反汇编带有IEnumerable&lt;T&gt; 的构造函数的源代码,你会看到它会做一些事情:

      • 调用collection.Count,所以如果collectionIEnumerable&lt;T&gt;,它将强制执行。如果collection 是一个数组、列表等,它应该是O(1)

      • 如果collection 实现ICollection&lt;T&gt;,它将使用ICollection&lt;T&gt;.CopyTo 方法将项目保存在内部数组中。它应该O(n),是n集合的长度。

      • 如果collection 没有实现ICollection&lt;T&gt;,它将遍历集合中的项目,并将它们添加到内部列表中。

      所以,是的,它会消耗更多内存,因为它必须创建一个新列表,并且在最坏的情况下,它将是 O(n),因为它会遍历 @987654336 @ 复制每个元素。

      【讨论】:

      • close, 0(n) 其中n 是原始集合中的字符串占用的总字节数,而不是元素的计数(更准确地说,n = 字节/字大小)
      • @user1416420 我可能错了,但这是为什么呢?如果它是一些其他类型的集合(例如boolint 等)怎么办?您实际上不必复制集合中的每个字符串。您只需将它们添加到新列表中即可。
      • 仍然没有关系,新的内存分配和字节复制是杀死这种方法的原因。在 .NET 中,布尔值也将占用 4 个字节。实际上,.NET 中对象的每个引用至少有 8 个字节长,所以它非常慢。前 4 个字节指向类型表,后 4 个字节指向值或找到值的内存位置
      【解决方案5】:

      “是否存在需要考虑的性能影响?”

      您的精确方案的问题在于,您对性能的真正关注首先是硬盘驱动器的速度和驱动器缓存的效率。

      从这个角度来看,影响肯定可以忽略不计,以至于NO不需要考虑。

      但只有当你真的需要List&lt;&gt; 结构的特性来提高你的生产力,或者你的算法更友好,或者其他一些优势时。否则,你只是故意添加一个微不足道的性能影响,完全没有理由。在这种情况下,您自然不应该这样做! :)

      【讨论】:

        【解决方案6】:

        ToList() 创建一个新列表并将元素放入其中,这意味着执行ToList() 会产生相关成本。在小集合的情况下,成本不会很明显,但在使用 ToList 的情况下,拥有大量集合可能会导致性能下降。

        通常你不应该使用 ToList() 除非你正在做的工作不能在不将集合转换为列表的情况下完成。例如,如果您只想遍历集合,则不需要执行 ToList

        如果您正在对数据源(例如使用 LINQ to SQL 的数据库)执行查询,则执行 ToList 的成本要高得多,因为当您将 ToList 与 LINQ to SQL 一起使用而不是执行延迟执行时,即在需要时加载项目(这在许多情况下都是有益的)它会立即将数据库中的项目加载到内存中

        【讨论】:

        • Haris:我不确定原始来源在调用 ToList() 后原始来源会发生什么
        • @Saurabh GC 会清理它
        • @Saurabh 原始来源不会发生任何事情。新创建的列表将引用原始源的元素
        • "如果你只是想遍历你不需要执行 ToList 的集合" - 那么你应该如何迭代呢?
        【解决方案7】:

        考虑到检索文件列表的性能,ToList() 可以忽略不计。但对于其他情况并非如此。这真的取决于你在哪里使用它。

        • 当调用数组、列表或其他集合时,您会创建一个集合的副本作为List&lt;T&gt;。这里的性能取决于列表的大小。你应该在真正需要的时候这样做。

          在您的示例中,您在数组上调用它。它遍历数组并将项目一一添加到新创建的列表中。所以性能影响取决于文件的数量。

        • 当调用 IEnumerable&lt;T&gt; 时,您实现 IEnumerable&lt;T&gt;(通常是查询)。

        【讨论】:

          【解决方案8】:

          ToList 将创建一个新列表并将元素从原始源复制到新创建的列表,所以唯一的事情就是从原始源复制元素并取决于源大小

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2018-04-03
            • 1970-01-01
            • 2013-06-14
            • 2013-01-18
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多