在LINQ查询中,调用ToList()还是ToArray()更好?

621

我经常遇到这样的情况,即我希望在声明查询时立即评估它。这通常是因为我需要多次迭代查询,而且计算代价很高。例如:

string raw = "...";
var lines = (from l in raw.Split('\n')
             let ll = l.Trim()
             where !string.IsNullOrEmpty(ll)
             select ll).ToList();

这样做很好。但是如果我不打算修改结果,那么我可能会直接调用 ToArray() 而不是 ToList()

不过,我想知道 ToArray() 是否是通过先调用 ToList() 实现的,因此比直接调用 ToList() 内存效率更低。

我疯了吗?我应该直接调用 ToArray() 吗?它是安全的,而且我知道内存不会被分配两次吗?


12
如果你想要了解.NET幕后发生了什么,我非常推荐使用.NET Reflector - David Hedlund
37
我建议查看 .net源代码,以获得更多信息。 - Gqqnbig
1
我不同意https://dev59.com/02w15IYBdhLWcg3wO5IH是这个问题的重复,尽管它们之间存在重要关系。内存使用(本问题)和性能(其他问题)都是有趣且非平凡的考虑因素。它们可以分别描述,但两者都应该纳入选择一个而不是另一个的决策中。我不能推荐任何一个问题或另一个问题的答案作为全面的答案。当综合考虑几个答案时,它们提供了一个相当完整的讨论,说明如何选择一个而不是另一个。 - steve
17个回答

458

除非你仅需要一个数组来满足其他限制,否则应该使用ToList。在大多数情况下,ToArray将分配比ToList更多的内存。

两者都使用数组进行存储,但ToList具有更灵活的约束条件。它需要数组大小至少与集合中的元素数量一样大。如果数组更大,那就不是问题。然而,ToArray需要数组大小正好为元素数量。

为了满足这个约束条件,ToArray通常会进行一个额外的分配。一旦它有一个足够大的数组,它会分配一个大小恰好正确的数组,并将元素复制回该数组中。唯一可以避免这种情况的情况是数组的增长算法恰好与需要存储的元素数量重合(绝对是少数情况)。

编辑

有几个人问我List<T>值中存在额外未使用内存的后果。

这是一个有效的关注点。如果创建的集合长期存在,在创建后从未修改,并且有很高的机会落入Gen2堆中,则最好直接采用ToArray的额外分配。

但通常我发现这是较少见的情况。更常见的是看到许多ToArray调用,它们立即传递给其他短暂使用内存的情况,在这种情况下,ToList明显更好。

关键在于进行分析,不断分析,然后再进行更多分析。


22
另一方面,用于创建数组的额外内存分配是否可被垃圾回收,而List的额外开销则会保留?我建议保持简单。如果需要添加或删除元素,有相应的工具可用。如果不需要,则有另一种工具可用。使用合适的工具。如果以后发现内存和性能方面存在问题,仅在这种情况下再作更改。 - Anthony Pegram
3
@AnthonyPegram 是的,这是一个需要考虑的有效因素。如果该值将用于长期存储,不会被修改,并且可能会进入第二代,则现在支付额外分配可能比污染第二代堆更好。但根据我的经验,我很少看到这种情况。更常见的是将ToArray立即传递给另一个短暂的LINQ查询。 - JaredPar
11
我不明白 ToArray 如何能够在需要确切的大小时分配更多内存,而 ToList<> 显然具有自动增长的备用空间。 - Royi Namir
6
因为ToArray方法首先会像ToList一样进行带有开销的分配,然后再进行额外的恰好大小的分配。 - Tim Sparkles
6
针对 .NET Core 3.1 的性能差异,还需考虑这个答案,其中 ToArray 的实现比 ToList 更高效。 - Luca Cremonesi
显示剩余9条评论

177

性能差异将是微不足道的,因为List<T>被实现为动态大小数组。无论是调用ToArray()(它使用一个内部的Buffer<T>类来增长数组)还是ToList()(它调用List<T>(IEnumerable<T>) 构造函数),最终都是将它们放入一个数组中并增加数组大小直到它们全部适合。

如果您想获得确切的确认,请查看Reflector中有关这些方法的实现-您会发现它们归结为几乎相同的代码。


2
我发现一个有趣的事实,即使用组连接(group join)在投影中定义的分组会导致相关查询(correlated queries),这时Linq to SQL会添加另一个子查询来检索该组的计数。我认为这意味着在这些情况下,在检索项之前将知道集合的大小,因此可以直接创建精确大小的数组,这将节省处理和内存资源。 - jpierson
149
如果事先知道数量,性能是相同的。然而,如果事先不知道数量,ToArray()ToList()之间唯一的区别在于前者必须修剪多余部分,这涉及到复制整个数组,而后者不会修剪多余部分,但会使用平均多25%的内存。这只会影响数据类型为大型结构体的情况。仅供参考。 - Scott Rippey
9
25%来自以下逻辑:如果项目数量未知,则调用ToListToArray将开始创建一个小缓冲区。当该缓冲区被填满时,它会将缓冲区容量加倍并继续使用。由于容量始终加倍,未使用的缓冲区始终在0%和50%之间。 - Scott Rippey
2
@ScottRippey 我刚刚查看了从IEnumerable源创建新List的源代码,它会检查IEnumerable是否为ICollection,如果是,则首先分配一个数组,其大小与Count属性完全相同,因此在这种情况下使用ToList()肯定更快。完整的答案可以包括这个事实,尽管我认为这不是最常见的情况。 - AndyClaw
6
无论是 List 还是 Buffer,它们都会检查 ICollection,此时它们的性能是相同的。 - Scott Rippey
显示剩余2条评论

127
编辑2:(这是对原答案的更正)

使用Benchmark.NET,我们可以通过性能测试来确认接受的答案实际上是正确的:ToList在一般情况下更快,因为它不必从分配的缓冲区中修剪空格。ToArray可能会执行额外的分配和复制操作,以便缓冲区的大小恰好等于元素的数量。

为了确认这一点,使用以下基准测试。
[MemoryDiagnoser]
[ShortRunJob]
public class Benchmarks
{
    [Params(0, 1, 6, 10, 42, 100, 1337, 10000)]
    public int Count { get; set; }

    public IEnumerable<int> Items => Enumerable.Range(0, Count).Where(i => i > 0);

    [Benchmark(Baseline = true)]
    public int[] ToArray() => Items.ToArray();

    [Benchmark]
    public List<int> ToList() => Items.ToList();
}

结果证实,在大多数情况下,ToList速度快10%-15%。
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
Intel Core i9-10885H CPU 2.40GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK=6.0.302
  [Host]     : .NET 6.0.7 (6.0.722.32202), X64 RyuJIT
  DefaultJob : .NET 6.0.7 (6.0.722.32202), X64 RyuJIT


|  Method | Count |         Mean |      Error |     StdDev | Ratio | RatioSD |   Gen 0 |  Gen 1 | Allocated |
|-------- |------ |-------------:|-----------:|-----------:|------:|--------:|--------:|-------:|----------:|
| ToArray |     0 |     29.73 ns |   0.546 ns |   0.536 ns |  1.00 |    0.00 |  0.0067 |      - |      56 B |
|  ToList |     0 |     31.51 ns |   0.485 ns |   0.405 ns |  1.06 |    0.02 |  0.0105 |      - |      88 B |
|         |       |              |            |            |       |         |         |        |           |
| ToArray |     1 |     37.36 ns |   0.314 ns |   0.294 ns |  1.00 |    0.00 |  0.0114 |      - |      96 B |
|  ToList |     1 |     36.75 ns |   0.605 ns |   0.537 ns |  0.98 |    0.01 |  0.0153 |      - |     128 B |
|         |       |              |            |            |       |         |         |        |           |
| ToArray |     6 |    100.05 ns |   1.522 ns |   1.349 ns |  1.00 |    0.00 |  0.0286 |      - |     240 B |
|  ToList |     6 |     85.16 ns |   0.808 ns |   0.756 ns |  0.85 |    0.01 |  0.0267 |      - |     224 B |
|         |       |              |            |            |       |         |         |        |           |
| ToArray |    10 |    137.20 ns |   2.766 ns |   2.452 ns |  1.00 |    0.00 |  0.0372 |      - |     312 B |
|  ToList |    10 |    123.05 ns |   2.198 ns |   1.949 ns |  0.90 |    0.01 |  0.0372 |      - |     312 B |
|         |       |              |            |            |       |         |         |        |           |
| ToArray |    42 |    398.25 ns |   6.583 ns |   5.836 ns |  1.00 |    0.00 |  0.0877 |      - |     736 B |
|  ToList |    42 |    352.04 ns |   4.976 ns |   4.411 ns |  0.88 |    0.02 |  0.0887 |      - |     744 B |
|         |       |              |            |            |       |         |         |        |           |
| ToArray |   100 |    730.80 ns |   6.501 ns |   6.081 ns |  1.00 |    0.00 |  0.1488 |      - |   1,248 B |
|  ToList |   100 |    705.49 ns |   9.947 ns |   9.305 ns |  0.97 |    0.01 |  0.1526 |      - |   1,280 B |
|         |       |              |            |            |       |         |         |        |           |
| ToArray |  1337 |  8,023.57 ns | 147.388 ns | 137.867 ns |  1.00 |    0.00 |  1.6785 | 0.0458 |  14,056 B |
|  ToList |  1337 |  7,980.27 ns | 138.469 ns | 122.749 ns |  1.00 |    0.02 |  1.9989 | 0.1221 |  16,736 B |
|         |       |              |            |            |       |         |         |        |           |
| ToArray | 10000 | 57,037.19 ns | 510.492 ns | 452.538 ns |  1.00 |    0.00 | 12.6343 | 1.7700 | 106,280 B |
|  ToList | 10000 | 57,728.15 ns | 583.353 ns | 517.127 ns |  1.01 |    0.01 | 15.5640 | 5.1270 | 131,496 B |

以下是原始答案,不幸的是它只在一个非常特殊的情况下进行了基准测试,避免了中间调整大小和复制操作。

原始答案:

现在已经是2020年了,每个人都在使用.NET Core 3.1,所以我决定使用Benchmark.NET运行一些基准测试。

简而言之:如果您不打算改变集合,ToArray()在性能上更好,并且更好地传达了意图。

编辑:从评论中可以看出,这些基准测试可能并不具有指示性,因为Enumerable.Range(...)返回一个IEnumerable<T>,其中包含有关序列大小的信息,随后由ToArray()用于优化,以预先分配正确大小的数组。请考虑为您的确切情况手动测试性能。


    [MemoryDiagnoser]
    public class Benchmarks
    {
        [Params(0, 1, 6, 10, 39, 100, 666, 1000, 1337, 10000)]
        public int Count { get; set; }
    
        public IEnumerable<int> Items => Enumerable.Range(0, Count);
    
        [Benchmark(Description = "ToArray()", Baseline = true)]
        public int[] ToArray() => Items.ToArray();
    
        [Benchmark(Description = "ToList()")]
        public List<int> ToList() => Items.ToList();
    
        public static void Main() => BenchmarkRunner.Run<Benchmarks>();
    }


结果如下:

    BenchmarkDotNet=v0.12.0, OS=Windows 10.0.14393.3443 (1607/AnniversaryUpdate/Redstone1)
    Intel Core i5-4460 CPU 3.20GHz (Haswell), 1 CPU, 4 logical and 4 physical cores
    Frequency=3124994 Hz, Resolution=320.0006 ns, Timer=TSC
    .NET Core SDK=3.1.100
      [Host]     : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT
      DefaultJob : .NET Core 3.1.0 (CoreCLR 4.700.19.56402, CoreFX 4.700.19.56404), X64 RyuJIT
    
    
    |    Method | Count |          Mean |       Error |      StdDev |        Median | Ratio | RatioSD |   Gen 0 | Gen 1 | Gen 2 | Allocated |
    |---------- |------ |--------------:|------------:|------------:|--------------:|------:|--------:|--------:|------:|------:|----------:|
    | ToArray() |     0 |      7.357 ns |   0.2096 ns |   0.1960 ns |      7.323 ns |  1.00 |    0.00 |       - |     - |     - |         - |
    |  ToList() |     0 |     13.174 ns |   0.2094 ns |   0.1958 ns |     13.084 ns |  1.79 |    0.05 |  0.0102 |     - |     - |      32 B |
    |           |       |               |             |             |               |       |         |         |       |       |           |
    | ToArray() |     1 |     23.917 ns |   0.4999 ns |   0.4676 ns |     23.954 ns |  1.00 |    0.00 |  0.0229 |     - |     - |      72 B |
    |  ToList() |     1 |     33.867 ns |   0.7350 ns |   0.6876 ns |     34.013 ns |  1.42 |    0.04 |  0.0331 |     - |     - |     104 B |
    |           |       |               |             |             |               |       |         |         |       |       |           |
    | ToArray() |     6 |     28.242 ns |   0.5071 ns |   0.4234 ns |     28.196 ns |  1.00 |    0.00 |  0.0280 |     - |     - |      88 B |
    |  ToList() |     6 |     43.516 ns |   0.9448 ns |   1.1949 ns |     42.896 ns |  1.56 |    0.06 |  0.0382 |     - |     - |     120 B |
    |           |       |               |             |             |               |       |         |         |       |       |           |
    | ToArray() |    10 |     31.636 ns |   0.5408 ns |   0.4516 ns |     31.657 ns |  1.00 |    0.00 |  0.0331 |     - |     - |     104 B |
    |  ToList() |    10 |     53.870 ns |   1.2988 ns |   2.2403 ns |     53.415 ns |  1.77 |    0.07 |  0.0433 |     - |     - |     136 B |
    |           |       |               |             |             |               |       |         |         |       |       |           |
    | ToArray() |    39 |     58.896 ns |   0.9441 ns |   0.8369 ns |     58.548 ns |  1.00 |    0.00 |  0.0713 |     - |     - |     224 B |
    |  ToList() |    39 |    138.054 ns |   2.8185 ns |   3.2458 ns |    138.937 ns |  2.35 |    0.08 |  0.0815 |     - |     - |     256 B |
    |           |       |               |             |             |               |       |         |         |       |       |           |
    | ToArray() |   100 |    119.167 ns |   1.6195 ns |   1.4357 ns |    119.120 ns |  1.00 |    0.00 |  0.1478 |     - |     - |     464 B |
    |  ToList() |   100 |    274.053 ns |   5.1073 ns |   4.7774 ns |    272.242 ns |  2.30 |    0.06 |  0.1578 |     - |     - |     496 B |
    |           |       |               |             |             |               |       |         |         |       |       |           |
    | ToArray() |   666 |    569.920 ns |  11.4496 ns |  11.2450 ns |    571.647 ns |  1.00 |    0.00 |  0.8688 |     - |     - |    2728 B |
    |  ToList() |   666 |  1,621.752 ns |  17.1176 ns |  16.0118 ns |  1,623.566 ns |  2.85 |    0.05 |  0.8793 |     - |     - |    2760 B |
    |           |       |               |             |             |               |       |         |         |       |       |           |
    | ToArray() |  1000 |    796.705 ns |  16.7091 ns |  19.8910 ns |    796.610 ns |  1.00 |    0.00 |  1.2951 |     - |     - |    4064 B |
    |  ToList() |  1000 |  2,453.110 ns |  48.1121 ns |  65.8563 ns |  2,460.190 ns |  3.09 |    0.10 |  1.3046 |     - |     - |    4096 B |
    |           |       |               |             |             |               |       |         |         |       |       |           |
    | ToArray() |  1337 |  1,057.983 ns |  20.9810 ns |  41.4145 ns |  1,041.028 ns |  1.00 |    0.00 |  1.7223 |     - |     - |    5416 B |
    |  ToList() |  1337 |  3,217.550 ns |  62.3777 ns |  61.2633 ns |  3,203.928 ns |  2.98 |    0.13 |  1.7357 |     - |     - |    5448 B |
    |           |       |               |             |             |               |       |         |         |       |       |           |
    | ToArray() | 10000 |  7,309.844 ns | 160.0343 ns | 141.8662 ns |  7,279.387 ns |  1.00 |    0.00 | 12.6572 |     - |     - |   40064 B |
    |  ToList() | 10000 | 23,858.032 ns | 389.6592 ns | 364.4874 ns | 23,759.001 ns |  3.26 |    0.08 | 12.6343 |     - |     - |   40096 B |
    
    // * Hints *
    Outliers
      Benchmarks.ToArray(): Default -> 2 outliers were removed (35.20 ns, 35.29 ns)
      Benchmarks.ToArray(): Default -> 2 outliers were removed (38.51 ns, 38.88 ns)
      Benchmarks.ToList(): Default  -> 1 outlier  was  removed (64.69 ns)
      Benchmarks.ToArray(): Default -> 1 outlier  was  removed (67.02 ns)
      Benchmarks.ToArray(): Default -> 1 outlier  was  removed (130.08 ns)
      Benchmarks.ToArray(): Default -> 1 outlier  was  detected (541.82 ns)
      Benchmarks.ToArray(): Default -> 1 outlier  was  removed (7.82 us)
    
    // * Legends *
      Count     : Value of the 'Count' parameter
      Mean      : Arithmetic mean of all measurements
      Error     : Half of 99.9% confidence interval
      StdDev    : Standard deviation of all measurements
      Median    : Value separating the higher half of all measurements (50th percentile)
      Ratio     : Mean of the ratio distribution ([Current]/[Baseline])
      RatioSD   : Standard deviation of the ratio distribution ([Current]/[Baseline])
      Gen 0     : GC Generation 0 collects per 1000 operations
      Gen 1     : GC Generation 1 collects per 1000 operations
      Gen 2     : GC Generation 2 collects per 1000 operations
      Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B)
      1 ns      : 1 Nanosecond (0.000000001 sec)


7
如果您不打算改变集合,我认为更好的方式是使用ToImmutableArray()(来自System.Collections.Immutable包),以显示意图。 - Arturo Torres Sánchez
6
谢谢。所选答案只是一个论点,并假设结果遵循该论点。要以科学的方式并作为奖励知道差异有多大,只有一种真正的方法可以知道。 - Jonas
5
这个测试会不会因为重复使用相同长度的数组而受到影响?这意味着在LargeArrayBuilder中可用的正确大小的缓冲区可能会发生变化。 - Erik Ovegård
10
这里没有考虑到RangeIterator.ToArrayRangeIterator.ToList的实现已知集合的最终大小。在使用更复杂的LINQ表达式(例如带有Where)时,大小无法确定,结果集合将需要动态调整大小。因此,这个答案只适用于现实世界中一小部分情况。 - neonblitzer
7
我把基准代码修改成了这个样子:public IEnumerable<int> Items => Enumerable.Range(0, Count).Where(i => i % 17 == 0); 在大多数情况下,使用 ToList 方法会快约 10%,并且分配更少的内存。@ErikOvegård @Tyrrrz @Jonas @neonblitzer - Andrei Bozantan
显示剩余4条评论

73
(seven years later...)
另外一些(好的)答案集中在可能出现的微小性能差异上。
本文只是为了补充提到在数组(T[])产生的IEnumerator<T>与由List<T>返回的IEnumerator<T>之间存在的语义差异。
最好通过以下示例进行说明:
IList<int> source = Enumerable.Range(1, 10).ToArray();  // try changing to .ToList()

foreach (var x in source)
{
  if (x == 5)
    source[8] *= 100;
  Console.WriteLine(x);
}

上述代码将不会抛出异常,并输出以下结果:
1 2 3 4 5 6 7 8 900 10
这表明由 int[] 返回的 IEnumarator<int> 并没有跟踪数组自创建枚举器以来是否已被修改。
请注意,我将本地变量 source 声明为 IList<int>。通过这种方式,我确保 C# 编译器不会将 foreach 语句优化为等效于 for (var idx = 0; idx < source.Length; idx++) { /* ... */ } 的循环。如果我使用 var source = ...;,C# 编译器可能会这样做。在我当前版本的 .NET Framework 中,此处使用的实际枚举器是一个非公共引用类型 System.SZArrayHelper+SZGenericArrayEnumerator`1[System.Int32],但这当然是一个实现细节。
现在,如果我将 .ToArray() 更改为 .ToList(),则只会得到以下内容:
1 2 3 4 5

紧接着是一个System.InvalidOperationException 异常,错误信息如下:

集合已修改;可能无法执行枚举操作。

在这种情况下,底层的枚举器是公共可变值类型 System.Collections.Generic.List`1+Enumerator[System.Int32](在此示例中包含在 IEnumerator<int> 盒子中,因为我使用了 IList<int>)。

总之,List<T> 生成的枚举器会跟踪列表是否在枚举期间发生更改,而由 T[] 生成的枚举器则不会。因此,在选择 .ToList().ToArray() 之间时,请考虑这种差异。

人们经常添加一个额外的 .ToArray().ToList() 来规避在枚举器的生命周期内集合是否被修改的问题。

如果有人想知道 List<> 如何跟踪集合是否被修改,那么这个类中有一个私有字段 _version,每次更新 List<> 时都会更改它。事实上,通过简单地删除索引器 public T this[int index]set 访问器中递增 _version 的行即可更改 List<> 的这种行为,就像最近在 Dictionary<,> 中所做的那样,如另一个答案中所述。


2
非常有信息量,但这只是数组和列表之间的区别,不是特定于ToArray与ToList实现的。并不是要批评,只是以防对其他人有帮助。 - nawfal

29

我同意@mquander的观点,性能差异应该是微不足道的。然而,我想进行基准测试以确保,所以我这样做了——结果证明确实如此,微不足道。

Testing with List<T> source:
ToArray time: 1934 ms (0.01934 ms/call), memory used: 4021 bytes/array
ToList  time: 1902 ms (0.01902 ms/call), memory used: 4045 bytes/List

Testing with array source:
ToArray time: 1957 ms (0.01957 ms/call), memory used: 4021 bytes/array
ToList  time: 2022 ms (0.02022 ms/call), memory used: 4045 bytes/List

每个源数组/列表都有1000个元素。因此,您可以看到时间和内存差异是可以忽略不计的。

我的结论:您可以使用 ToList(),因为 List<T> 提供了比数组更多的功能,除非您真的很在意几个字节的内存。


1
我想知道如果你使用一个大的 struct 而不是一个原始类型或类,这个结果会不会有所不同。 - Scott Rippey
13
List<T>.ToList是将一个List<T>转换为另一个List<T>的方法。建议使用一个实现IEnumerable接口而不是ICollection接口的对象作为输入参数。 - Grigory
9
我想确保我只测量了 ToListToArray 调用的时间,而没有枚举任何 IEnumerable。List<T>.ToList() 仍然会创建一个新的 List<T> - 它不仅仅是“返回此”。 - EMP
28
当使用 ICollection<T> 参数调用 ToArray()ToList() 时,它们的行为差异很大 - 它们只执行单个分配和单个复制操作。List<T>Array 都实现了 ICollection<T> 接口,因此你的基准测试结果根本无效。 - Mohammad Dehghan
1
对于任何感兴趣的人,我发布了我的独立答案基准。它使用.Select(i => i)来避免ICollection<T>实现问题,并包括一个控制组,以查看有多少时间仅用于首先迭代源IEnumerable<> - StriplingWarrior
1
除非你真的在意几个字节的内存,否则最好使用ToList(),因为List<T>提供比数组更多的功能。 - Tyrrrz

23

ToList()通常在使用IEnumerable<T>(例如从ORM中)时更受欢迎。如果序列长度在开始时未知,则ToArray()会创建动态长度集合,例如List,然后将其转换为数组,这需要额外的时间。


28
在这种情况下,我已经决定可读性比性能更重要。现在只有在预期继续添加元素时才使用ToList。在所有其他情况(大多数情况)中,我使用ToArray。但是感谢你的建议! - Frank Krueger
7
在ILSpy中查看,Enumerable.ToArray() 调用 new Buffer<TSource>(source).ToArray()。在 Buffer 构造函数中,如果 source 实现了 ICollection,则调用 source.CopyTo(items, 0),然后 .ToArray() 直接返回内部 items 数组。因此,在这种情况下没有额外花费时间的转换。如果 source 没有实现 ICollection,则 ToArray 将导致一个数组拷贝,以便从数组末尾修剪未使用的额外位置,正如 Scott Rippey 的评论所述。 - BrandonAGr

22

内存将始终被分配两次,或者接近于这个数字。由于无法调整数组的大小,因此两种方法都使用某种机制来收集增长的数据集合。(嗯,List本身就是一个增长的集合。)

List使用数组作为内部存储,并在需要时将其容量加倍。这意味着平均有2/3的项目已经至少重新分配了一次,其中一半至少重新分配了两次,其中一半至少重新分配了三次,依此类推。这意味着每个项目平均被重新分配了1.3次,这并不是很大的开销。

另外要记住,如果你正在收集字符串,则集合本身只包含对字符串的引用,字符串本身不会被重新分配。


这可能是一个愚蠢的问题,但是你所描述的2/3, 1/3, 1/6的逻辑是否假设列表的数组可以直接扩展?也就是说,数组的末尾有空闲空间,因此不需要移动现有的分配吗? - user565869
@JonofAllTrades:不,数组从未在原地扩展,.NET中的内存管理根本不会这样做。如果它会在原地扩展,就不需要重新分配项目了。 - Guffa
1
啊,我明白了:那些没有重新分配的项目之所以不需要重新分配是因为它们已经在最终的分配中了。所有在之前分配中分配的项目都会被移动,但由于数组长度的对数增长,这只是一个可计算的部分。谢谢澄清! - user565869

18

我发现其他人做的基准测试都不够好,所以我自己来试试。如果你发现我的方法有问题,请告诉我。

/* This is a benchmarking template I use in LINQPad when I want to do a
 * quick performance test. Just give it a couple of actions to test and
 * it will give you a pretty good idea of how long they take compared
 * to one another. It's not perfect: You can expect a 3% error margin
 * under ideal circumstances. But if you're not going to improve
 * performance by more than 3%, you probably don't care anyway.*/
void Main()
{
    // Enter setup code here
    var values = Enumerable.Range(1, 100000)
        .Select(i => i.ToString())
        .ToArray()
        .Select(i => i);
    values.GetType().Dump();
    var actions = new[]
    {
        new TimedAction("ToList", () =>
        {
            values.ToList();
        }),
        new TimedAction("ToArray", () =>
        {
            values.ToArray();
        }),
        new TimedAction("Control", () =>
        {
            foreach (var element in values)
            {
                // do nothing
            }
        }),
        // Add tests as desired
    };
    const int TimesToRun = 1000; // Tweak this as necessary
    TimeActions(TimesToRun, actions);
}


#region timer helper methods
// Define other methods and classes here
public void TimeActions(int iterations, params TimedAction[] actions)
{
    Stopwatch s = new Stopwatch();
    int length = actions.Length;
    var results = new ActionResult[actions.Length];
    // Perform the actions in their initial order.
    for (int i = 0; i < length; i++)
    {
        var action = actions[i];
        var result = results[i] = new ActionResult { Message = action.Message };
        // Do a dry run to get things ramped up/cached
        result.DryRun1 = s.Time(action.Action, 10);
        result.FullRun1 = s.Time(action.Action, iterations);
    }
    // Perform the actions in reverse order.
    for (int i = length - 1; i >= 0; i--)
    {
        var action = actions[i];
        var result = results[i];
        // Do a dry run to get things ramped up/cached
        result.DryRun2 = s.Time(action.Action, 10);
        result.FullRun2 = s.Time(action.Action, iterations);
    }
    results.Dump();
}

public class ActionResult
{
    public string Message { get; set; }
    public double DryRun1 { get; set; }
    public double DryRun2 { get; set; }
    public double FullRun1 { get; set; }
    public double FullRun2 { get; set; }
}

public class TimedAction
{
    public TimedAction(string message, Action action)
    {
        Message = message;
        Action = action;
    }
    public string Message { get; private set; }
    public Action Action { get; private set; }
}

public static class StopwatchExtensions
{
    public static double Time(this Stopwatch sw, Action action, int iterations)
    {
        sw.Restart();
        for (int i = 0; i < iterations; i++)
        {
            action();
        }
        sw.Stop();

        return sw.Elapsed.TotalMilliseconds;
    }
}
#endregion

您可以在此处下载LINQPad脚本

结果: ToArray vs ToList performance

通过调整上面的代码,您将发现:

  1. 处理较小的数组时,差异不那么显著。 More iterations, but smaller arrays
  2. 与使用string相比,使用int时差别不那么显著。
  3. 使用大型struct而不是string通常需要更长的时间,但实际上并没有太大改变。

这与得票最多的答案的结论相符:

  1. 除非您的代码经常生成许多大型数据列表,否则您不太可能注意到性能差异。(创建1000个包含100K字符串的列表时只有200ms的差异。)
  2. ToList()始终运行更快,并且如果您不打算长时间保留结果,则是更好的选择。

更新

@JonHanna指出,根据Select的实现方式,ToList()ToArray()的实现可以预测结果集合的大小。将上述代码中的.Select(i => i)替换为Where(i => true) 在目前情况下产生非常相似的结果,而且无论.NET的实现方式如何,这种做法更有可能产生相似的结果。

Benchmark using Where instead of Select


1
在.NET Core中,这两种情况应该比netfx更好,因为它会意识到大小将是100000并使用它来优化ToList()ToArray(),其中ToArray()非常轻巧,因为它不需要收缩操作,否则就需要,这是ToList()具有优势的唯一地方。问题中的示例仍然会失败,因为“Where”意味着无法进行这样的大小预测。 - Jon Hanna
@JonHanna:感谢您的快速反馈。我不知道.NET Core正在进行这种优化。这很酷。在我的代码中,.Select(i => i)可以替换为.Where(i => true)来纠正这个问题。 - StriplingWarrior
是的,这将阻止优化对corefx的影响。拥有一个大小为2的幂次方(这应该会给ToArray()带来优势)和一个不是的大小可能很有趣,然后比较结果。 - Jon Hanna
@JonHanna:有趣的是,在最佳情况下,ToArray()仍然会输掉。使用Math.Pow(2, 15)个元素时,ToList: 700ms,ToArray: 900ms。在添加一个元素后,ToList: 925,ToArray: 1350。我想知道即使数组已经是完美大小,ToArray是否仍在复制数组?他们可能认为这种情况很少发生,不值得额外的条件判断。 - StriplingWarrior
甚至在我们开始在 corefx 中进行优化之前,它就没有精确地匹配尺寸了,所以它是最容易出错的情况。 - Jon Hanna

17

我知道这是一篇旧文章,但在提出相同问题并进行了一些研究后,我发现了一些有趣的东西可能值得分享。

首先,我赞同@mquander及其回答。就性能而言,两者是相同的。

然而,我一直在使用Reflector查看System.Linq.Enumerable扩展命名空间中的方法,并注意到一种非常常见的优化。
只要可能,IEnumerable<T>源就会被转换为IList<T>或ICollection<T>以优化该方法。 例如,看一下ElementAt(int)。

有趣的是,微软选择仅优化IList<T>,而不是IList。 看起来微软更喜欢使用IList<T>接口。


17
我做了一个测试,发现了一件令人惊讶的事情。数组确实实现了IList<T>!使用Reflector分析System.Array只会显示IList、ICollection和IEnumerable的继承链,但是使用运行时反射,我发现string[]的继承链包括IList、ICollection、IEnumerable、IList<string>、ICollection<string>和IEnumerable<string>。因此,我没有比@mquander更好的答案! - Scott Rippey
@ScottRippey 是的。你注意到的奇怪现象实际上是一个“hack”的一部分 - 它还涉及到“固定大小”和类似属性的一些相当奇怪的含义(取决于你如何转换它,可能存在一些不一致性)。在 .net 源代码中有一些涉及这个主题的相当大的注释。抱歉没有链接,但如果我记得正确的话,很容易找到(在数组类内部)。 (还有一个大的 SO 问题讨论不一致性.... 在某个地方... >__>) - AnorZaken
@ScottRippey,顺便说一下,我找到了这个答案,与你的评论有关: https://dev59.com/mG855IYBdhLWcg3wMRRV#4482567 - David Klempfner

15
你应该根据设计选择来决定使用 ToList 还是 ToArray 。如果你想要一个只能通过索引迭代和访问的集合,请选择 ToArray。如果你想要稍后添加和删除集合中的元素而不费什么力气,则使用 ToList (虽然你也可以向数组中添加元素,但通常这不是正确的工具)。
如果性能很重要,你还应该考虑哪种操作更快。实际上,你不会调用 ToListToArray 一百万次,但可能会对获取的集合进行一百万次操作。在这个方面,[] 更好,因为 List<> 就是带有一些开销的 []。请参见此线程以获取一些效率比较:Which one is more efficient : List<int> or int[] 在我自己之前的一些测试中,我发现 ToArray 更快。但我不确定测试结果是否有偏差。尽管存在如此微不足道的性能差异,但只有在你将这些查询在循环中运行数百万次时才会注意到。

2
是的 - 如果编译器知道您正在迭代数组(而不是 IEnumerable <>),它可以显着优化迭代。 - RobSiklos

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接