.NET 9 — ToList vs ToArray

Performance comparison between ToList and ToArray

Last year I made an article comparing the performance of ToList versus ToArray when creating short lived collections that won’t be mutated, usually used to prevent multiple enumerations when iterating over a temporary LINQ transformation or to ensure mapping exceptions will be thrown inside the corresponding application layer.

The tests were performed with .NET Framework 4.8, .NET 7 and .NET 8, which concluded that ToArray is significantly faster and more memory efficient for almost all collection sizes, with the only exception with very large collections in .NET 8 were ToList was faster - but still uses more memory).

Assuming everything goes as planed, Microsoft should release .NET 9 by the end of 2024. This is the next major version of their most popular development framework that will bring a lot of new features (C# 13 is one of them) and performance improvements.

Since we already have .NET 9 preview 5 available, which contains an even more optimized SegmentedArrayBuilder that is used internally by ToArray, I think it is a good time to compare the performance of both these methods in .NET 9 while using .NET 8 as the baseline.

Performance Test

Once again, I’m going to use the well known C# library BenchmarkDotNet to run the tests and the environment will be the following:

1
2
3
4
5
6
7
BenchmarkDotNet v0.13.10, Windows 11 (10.0.22631.3737/23H2/2023Update/SunValley3)
AMD Ryzen 7 3700X, 1 CPU, 16 logical and 8 physical cores
.NET SDK 9.0.100-preview.5.24307.3
[Host] : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
.NET 8.0 : .NET 8.0.6 (8.0.624.26715), X64 RyuJIT AVX2
.NET 9.0 : .NET 9.0.0 (9.0.24.30607), X64 RyuJIT AVX2
.NET Framework 4.8.1 : .NET Framework 4.8.1 (4.8.9241.0), X64 RyuJIT VectorSize=256

The test consists in the creation of a collection that will hold random integers with a size defined by a test parameter. To ensure the collection initialization doesn’t affect performance, it will be created and cached during test setup, but converted to a new IEnumerable before invoking either ToArray or ToList.

Keep in mind we want to test the performance of iterating over an IEnumerable and create either an array or a list so, to prevent .NET internal optimizations (like using a SelectArrayIterator), the method that converts the cached array to an IEnumerable will use the yield return keyword. This is different than the previous article were I was using the Select method which would return an optimized enumerable for arrays and I want to test the worst case scenario.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[SimpleJob(RuntimeMoniker.Net80, baseline: true)]
[SimpleJob(RuntimeMoniker.Net90)]
[MemoryDiagnoser]
public class ToListVsToArray
{
[Params(10, 100, 1000, 10000, 100000)]
public int Size;

private int[] _items;

[GlobalSetup]
public void Setup()
{
var random = new Random(123);

_items = Enumerable.Range(0, Size).Select(_ => random.Next()).ToArray();
}

[Benchmark]
public int[] ToArray() => CreateItemsEnumerable().ToArray();

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

private IEnumerable<int> CreateItemsEnumerable()
{
foreach (var item in _items)
yield return item;
}
}

Performance results

Since this article is a continuation of my previous one, were I concluded using ToArray is faster and more memory efficient than ToList, let’s compare if that statement still holds true.

1
2
3
4
5
6
7
8
9
10
11
12
| Method  | Size   | Mean          | Error         | StdDev        | Gen0     | Gen1     | Gen2     | Allocated |
|-------- |------- |--------------:|--------------:|--------------:|---------:|---------:|---------:|----------:|
| ToArray | 10 | 70.39 ns | 0.366 ns | 0.342 ns | 0.0134 | - | - | 112 B |
| ToList | 10 | 72.85 ns | 0.744 ns | 0.696 ns | 0.0315 | - | - | 264 B |
| ToArray | 100 | 322.65 ns | 1.816 ns | 1.610 ns | 0.0563 | - | - | 472 B |
| ToList | 100 | 368.11 ns | 4.283 ns | 4.006 ns | 0.1469 | - | - | 1232 B |
| ToArray | 1000 | 2,451.62 ns | 19.687 ns | 16.439 ns | 0.4845 | - | - | 4072 B |
| ToList | 1000 | 2,854.28 ns | 24.286 ns | 22.717 ns | 1.0109 | 0.0153 | - | 8472 B |
| ToArray | 10000 | 22,275.27 ns | 163.363 ns | 152.810 ns | 4.7607 | - | - | 40072 B |
| ToList | 10000 | 26,944.65 ns | 293.685 ns | 260.344 ns | 15.6250 | - | - | 131448 B |
| ToArray | 100000 | 328,160.90 ns | 1,874.673 ns | 1,753.570 ns | 124.5117 | 124.5117 | 124.5117 | 400156 B |
| ToList | 100000 | 410,583.73 ns | 2,298.854 ns | 2,037.874 ns | 285.6445 | 285.6445 | 285.6445 | 1049120 B |

Using ToList as the baseline, we can see the ToArray method is, on average, 15% faster and uses 60% less memory.

Keep in mind that ToArray is a better choice than ToList even on larger collections, something that wasn’t true in .NET 8, were it was 4% slower despite using less memory.

The winner: .NET 9.0

.NET performance evolution

Because we also want to compare the performance of .NET 9 over .NET 8, let’s analyze each method individually on each framework and see if anything has changed.

ToArray

1
2
3
4
5
6
7
8
9
10
11
12
| Runtime  | Size   | Mean          | Error         | StdDev        | Ratio | RatioSD | Gen0     | Gen1     | Gen2     | Allocated | Alloc Ratio |
|----------|------- |--------------:|--------------:|--------------:|------:|--------:|---------:|---------:|---------:|----------:|------------:|
| .NET 8.0 | 10 | 107.51 ns | 1.585 ns | 1.238 ns | 1.00 | 0.00 | 0.0315 | - | - | 264 B | 1.00 |
| .NET 9.0 | 10 | 70.39 ns | 0.366 ns | 0.342 ns | 0.65 | 0.01 | 0.0134 | - | - | 112 B | 0.42 |
| .NET 8.0 | 100 | 442.33 ns | 3.788 ns | 3.543 ns | 1.00 | 0.00 | 0.1431 | - | - | 1200 B | 1.00 |
| .NET 9.0 | 100 | 322.65 ns | 1.816 ns | 1.610 ns | 0.73 | 0.01 | 0.0563 | - | - | 472 B | 0.39 |
| .NET 8.0 | 1000 | 3,186.13 ns | 31.530 ns | 29.493 ns | 1.00 | 0.00 | 1.0185 | - | - | 8544 B | 1.00 |
| .NET 9.0 | 1000 | 2,451.62 ns | 19.687 ns | 16.439 ns | 0.77 | 0.00 | 0.4845 | - | - | 4072 B | 0.48 |
| .NET 8.0 | 10000 | 30,659.83 ns | 292.167 ns | 273.293 ns | 1.00 | 0.00 | 12.6343 | - | - | 106232 B | 1.00 |
| .NET 9.0 | 10000 | 22,275.27 ns | 163.363 ns | 152.810 ns | 0.73 | 0.00 | 4.7607 | - | - | 40072 B | 0.38 |
| .NET 8.0 | 100000 | 482,397.96 ns | 1,499.949 ns | 1,403.053 ns | 1.00 | 0.00 | 249.5117 | 249.5117 | 249.5117 | 925140 B | 1.00 |
| .NET 9.0 | 100000 | 328,160.90 ns | 1,874.673 ns | 1,753.570 ns | 0.68 | 0.00 | 124.5117 | 124.5117 | 124.5117 | 400156 B | 0.43 |

Using .NET 8 as the baseline, we can see the ToArray method is, on average, 30% faster and uses 55% less memory on .NET 9. 

The winner: .NET 9.0

ToList

1
2
3
4
5
6
7
8
9
10
11
12
| Runtime  | Size   | Mean          | Error         | StdDev        | Ratio | RatioSD | Gen0     | Gen1     | Gen2     | Allocated | Alloc Ratio |
|----------|------- |--------------:|--------------:|--------------:|------:|--------:|---------:|---------:|---------:|----------:|------------:|
| .NET 8.0 | 10 | 87.44 ns | 1.803 ns | 1.929 ns | 1.00 | 0.00 | 0.0315 | - | - | 264 B | 1.00 |
| .NET 9.0 | 10 | 72.85 ns | 0.744 ns | 0.696 ns | 0.84 | 0.02 | 0.0315 | - | - | 264 B | 1.00 |
| .NET 8.0 | 100 | 420.90 ns | 3.654 ns | 2.853 ns | 1.00 | 0.00 | 0.1469 | - | - | 1232 B | 1.00 |
| .NET 9.0 | 100 | 368.11 ns | 4.283 ns | 4.006 ns | 0.87 | 0.01 | 0.1469 | - | - | 1232 B | 1.00 |
| .NET 8.0 | 1000 | 3,448.37 ns | 67.905 ns | 78.199 ns | 1.00 | 0.00 | 1.0109 | 0.0153 | - | 8472 B | 1.00 |
| .NET 9.0 | 1000 | 2,854.28 ns | 24.286 ns | 22.717 ns | 0.82 | 0.01 | 1.0109 | 0.0153 | - | 8472 B | 1.00 |
| .NET 8.0 | 10000 | 35,650.35 ns | 707.370 ns | 1,537.764 ns | 1.00 | 0.00 | 15.6250 | - | - | 131448 B | 1.00 |
| .NET 9.0 | 10000 | 26,944.65 ns | 293.685 ns | 260.344 ns | 0.77 | 0.05 | 15.6250 | - | - | 131448 B | 1.00 |
| .NET 8.0 | 100000 | 462,317.72 ns | 1,686.365 ns | 1,577.427 ns | 1.00 | 0.00 | 285.6445 | 285.6445 | 285.6445 | 1049120 B | 1.00 |
| .NET 9.0 | 100000 | 410,583.73 ns | 2,298.854 ns | 2,037.874 ns | 0.89 | 0.01 | 285.6445 | 285.6445 | 285.6445 | 1049120 B | 1.00 |

Using .NET 8 as the baseline, we can see the ToList method is, on average, 15% faster while having exactly the same memory footprint on .NET 9.

The winner: .NET 9.0

Conclusion

In this article we compared the performance of ToArray versus ToList on .NET 9 and concluded, once again, if you need to create a temporary collection in memory to prevent multiple enumerations of an IEnumerable, using ToArray is more performant in all scenarios independent of collection size, something that wasn’t true in .NET 8.

This is also a clear statement that Microsoft made a good decision to introduce classes and structures dedicated to performance, like ArrayPool or ReadOnlySpan, making it easier to share or reuse resources without constantly (de)allocating memory. This has been specially important for our performance tests since SegmentedArrayBuilder makes heavy use of these functionalities, now more than ever.