System.Collections.Generic.List may be the most used collection in .NET. It is a generic collection that can be used to store any type of data. In this post, we'll see the different ways to enumerate a List .
There are multiple ways to enumerate a List in .NET. Not all of them are equal to each other in term of behavior and performance. You can enumerate the list faster if you accept tradeoffs! #Using the foreach statementList list = new List();
foreach (var item in list)
{
// Do something
}
This is the most common way to iterate on a collection. The compiler will replace this loop with a call to GetEnumerator and MoveNext() methods: List list = new List();
List.Enumerator enumerator = list.GetEnumerator();
try
{
while (enumerator.MoveNext())
{
T current = enumerator.Current;
}
}
finally
{
((IDisposable)enumerator).Dispose();
}
The MoveNext method implementation ensure the list is not modified during the enumeration. If the list is modified, an InvalidOperationException with the message "Collection was modified after the enumerator was instantiated" is thrown by the MoveNext method. This mainly helps detecting multi-threading issues. However, this feature comes with a cost. Indeed, there is a check when accessing each item of the collection. #Using the List.Foreach methodList list = new List();
list.ForEach(item =>
{
// Do something
});
The ForEach method knows internals of the List and is able to optimize the enumeration. Indeed, it can iterate over the private array and the JIT may be able to remove bound checks. This method gives the same guarantee as when using the foreach keyword. So, it will throw an exception if the list is modified during the enumeration. This method needs to call the delegate for each item. Calling a method has a low overhead, but it may be significant in a micro-benchmark compared to a foreach loop which would have its code inlined. #Using the for statementList list = new List();
for (int i = 0; i < list.Count; i++)
{
var item = list[i];
// Do something
}
Using a for loop, you don't ensure the collection is not modified during the enumeration. However, the indexer still need to check you don't access an invalid index. So, the performance should be a little bit better than the foreach loop as there is less checks. #Using the CollectionsMarshal.AsSpan methodList list = new List();
foreach (var item in CollectionsMarshal.AsSpan(list))
{
// Do something
}
The CollectionsMarshal.AsSpan unsafe and should be used only if you know what you're doing. CollectionsMarshal.AsSpan returns a Span on the private array of List . Iterating over a Span is fast as the JIT uses the same tricks as for optimizing arrays. Using this method, it won't check the list is not modified during the enumeration. As said, this method is unsafe and you may get weird behaviors if you don't know what you are doing. For instance, you may iterate over items that are not part of the list anymore in some cases. Let's see an example: var list = new List<int>(capacity: 1);
list.Add(0); // list: [0]
var span = CollectionsMarshal.AsSpan(list);
span[0] = 1; // list: [1], span: [1]
// To add a new item, the list needs to increase its capacity
// => A new array is created and replace the existing List._items
// But the span still references the old array
// => ⚠️ Accessing the span won't access the actual list items!
list.Add(3); // list: [1, 3], span: [1]
list[0] = 2; // ⚠️ list: [2, 3], span: [1]
span[0] = 0; // ⚠️ list: [2, 3], span: [0]
As long as you don't modify the list while enumerating it, you should be fine. #BenchmarkLet's benchmark the 4 methods using BenchmarkDotnet: BenchmarkRunner.Run();
public class ListBenchmark
{
private List<int> _list = default!;
[Params(10, 1_000, 10_000, 100_000, 1_000_000)]
public int Size { get; set; }
[GlobalSetup]
public void Setup()
{
_list = new List<int>(Size);
for (var i = 0; i < Size; i++)
{
_list.Add(i);
}
}
[Benchmark(Baseline = true)]
public void Foreach()
{
foreach (var item in _list)
{
}
}
[Benchmark]
public void List_Foreach()
{
_list.ForEach(_ => { });
}
[Benchmark]
public void For()
{
for (var i = 0; i < _list.Count; i++)
{
_ = _list[i];
}
}
[Benchmark]
public void Foreach_Span()
{
foreach (var item in CollectionsMarshal.AsSpan(_list))
{
}
}
}
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK=6.0.100
[Host] : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT
DefaultJob : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT
METHOD | SIZE | MEAN | ERROR | STDDEV | MEDIAN | RATIO |
---|
Foreach | 10 | 6.538 ns | 0.1515 ns | 0.2313 ns | 6.473 ns | 1.00 | List_Foreach | 10 | 17.250 ns | 0.1969 ns | 0.1841 ns | 17.207 ns | 2.60 | For | 10 | 3.972 ns | 0.1016 ns | 0.2144 ns | 3.915 ns | 0.61 | Foreach_Span | 10 | 2.404 ns | 0.0675 ns | 0.0778 ns | 2.402 ns | 0.36 | | | | | | | | Foreach | 1000 | 652.441 ns | 13.0433 ns | 13.9562 ns | 645.224 ns | 1.00 | List_Foreach | 1000 | 1,532.082 ns | 29.8052 ns | 33.1284 ns | 1,513.077 ns | 2.35 | For | 1000 | 335.335 ns | 6.4555 ns | 8.6179 ns | 337.084 ns | 0.52 | Foreach_Span | 1000 | 216.830 ns | 4.2158 ns | 4.1405 ns | 214.779 ns | 0.33 | | | | | | | | Foreach | 10000 | 6,525.978 ns | 128.4690 ns | 200.0107 ns | 6,424.406 ns | 1.00 | List_Foreach | 10000 | 15,729.575 ns | 306.7648 ns | 537.2739 ns | 15,578.696 ns | 2.42 | For | 10000 | 3,260.233 ns | 63.2148 ns | 79.9465 ns | 3,208.650 ns | 0.50 | Foreach_Span | 10000 | 2,149.916 ns | 41.9624 ns | 61.5079 ns | 2,110.078 ns | 0.33 | | | | | | | | Foreach | 100000 | 64,740.749 ns | 1,236.3309 ns | 1,156.4647 ns | 64,548.645 ns | 1.00 | List_Foreach | 100000 | 161,580.108 ns | 3,214.2802 ns | 8,689.9934 ns | 159,376.184 ns | 2.49 | For | 100000 | 32,876.553 ns | 632.3321 ns | 822.2103 ns | 32,999.438 ns | 0.51 | Foreach_Span | 100000 | 21,528.151 ns | 424.5114 ns | 536.8710 ns | 21,562.567 ns | 0.33 | | | | | | | | Foreach | 1000000 | 649,103.544 ns | 12,557.2372 ns | 11,131.6637 ns | 645,519.531 ns | 1.00 | List_Foreach | 1000000 | 1,539,384.342 ns | 30,577.8956 ns | 32,718.0058 ns | 1,546,489.551 ns | 2.37 | For | 1000000 | 325,415.127 ns | 6,401.9765 ns | 8,324.3778 ns | 320,766.748 ns | 0.50 | Foreach_Span | 1000000 | 213,845.618 ns | 4,275.2697 ns | 4,390.3872 ns | 213,240.796 ns | 0.33 |
Unsurprisingly CollectionsMarshal.AsSpan is the fatest way to enumerate a List when it's applicable. I was a bit surprised with the performance of List.Foreach which is about 2.5 times slower than a foreach , but it makes sense as it has to call the delegate. In this benchmark there is no operation applied on list items, but in real scenario, this overhead should be negligible. note: For large collections, you may need to investigate Parallel.ForEach to improve the performance. As always, be sure to measure the performance before and after the change.
|