We're all used to write new List<int> { 1, 2, 3, 4 } or new int[] { 1, 2, 3, 4} to initialize collections with
values. Syntactically, they look similar, but the behavior is different, and you should be careful if you are worried
about
performance.
Array
As we know, arrays contain a sequence of fixed-size elements. Once created, their size cannot be changed for the entire lifetime of the array.
var array = new int[] { 1, 2, 3, 4};
List<T>
When we don't know the final size of a collection, or we need to add/remove elements during its life cycle, using List<T> type is appropriate.
var list = new List<int>();
list.Add(1);
list.Add(2);
At first glance, it might seem like we can always use a list instead of an array — it has all the capabilities of an array, but it can also be dynamically resized. But to decide whether to use a list, we need to understand more about its internal structure.
Part of the source code:
public class List<T> : IList<T>, IList, IReadOnlyList<T>
{
private const int DefaultCapacity = 4;
internal T[] _items;
internal int _size;
private static readonly T[] s_emptyArray = new T[0];
// Constructs a List. The list is initially empty and has a capacity
// of zero. Upon adding the first element to the list the capacity is
// increased to DefaultCapacity, and then increased in multiples of two
// as required.
public List()
{
_items = s_emptyArray;
}
...
public int Capacity
{
get => _items.Length;
set {...}
}
public int Count => _size;
}
_items- internal array for storing elements;_size- the number of elements in the array and the total size of the list;Capacity- the size of the_itemsarray and the maximum number of elements that can fit into it without resizing.
Resizing a list
In simple terms, we can say that a list is an array that can be resized as needed.
Every time we try to add another item, the list checks if there is enough free space in _items, otherwise it sets the new capacity:
internal void Grow(int capacity)
{
...
int newCapacity = _items.Length == 0 ? DefaultCapacity : 2 * _items.Length;
...
Capacity = newCapacity;
}
Let's take a closer look at the property Capacity:
// Gets and sets the capacity of this list. The capacity is the size of
// the internal array used to hold items. When set, the internal
// array of the list is reallocated to the given capacity.
public int Capacity
{
get => _items.Length;
set
{
if (value < _size)
{
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity);
}
if (value != _items.Length)
{
if (value > 0)
{
T[] newItems = new T[value];
if (_size > 0)
{
Array.Copy(_items, newItems, _size);
}
_items = newItems;
}
else
{
_items = s_emptyArray;
}
}
}
}
So, what do we see? Initially, each list is created with an empty internal array. After the first element is added,
the list creates a new array with 4 elements (DefaultCapacity is 4). And when the current array is exhausted, a new one is created
with double the size, and all elements are copied.
Performance
What happens when we create a new list and initialize it with values?
var list = new List<int> { 1, 2, 3, 4, 5 };
This looks like array initialization, but it works completely differently.
According to the documentation, any type that implements IEnumerable and has an Add method can be used with a collection initializer.
So, the previous example is just a shorthand form of successive calls to the Add method:
var list = new List<int>();
list.Add(1);
list.Add(2);
list.Add(3);
list.Add(4);
list.Add(5);
The compiler simply transforms the short collection initializer and automatically adds the necessary calls. This can result in a performance penalty.
Let's take a closer look at the process of adding 5 elements:
- Before the first call to
Addthe list is empty, the internal array is empty. - The first element added creates a new internal array with 4 elements.
- Elements 2, 3, 4 do not change anything when added.
- When we add the fifth element, the internal array is full and needs to be resized. A new array of size 8 is created, all elements are copied from the previous array, and the fifth element is added.
So, we end up with a list with 5 elements and an internal array with 8 elements. We've created two arrays, and the final one is wasting 37.5% of its space. As you might have guessed, creating new arrays and copying elements results in memory allocation and takes additional time.
This could be an unpleasant surprise in critical areas. Do we have a solution? Yes!
Capacity
If we know or guess the finite size of the list, we can create it with an initial capacity (Capacity).
var list = new List<int>(5);
list.Add(1);
list.Add(2);
list.Add(3);
list.Add(4);
list.Add(5);
Or
var list = new List<int>(5) { 1, 2, 3, 4, 5 };
Now we immediately create one internal array of 5 elements, and there are no more unnecessary memory allocations.
Benchmark
Let's compare creating lists with and without initial capacity.
Full benchmark code. Click to expand.
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
namespace ListBenchmark
{
[ShortRunJob(RuntimeMoniker.Net48)]
[ShortRunJob(RuntimeMoniker.NetCoreApp31)]
[ShortRunJob(RuntimeMoniker.Net80)]
[ShortRunJob(RuntimeMoniker.Net10_0)]
[MemoryDiagnoser]
[HideColumns("Job", "Error", "StdDev", "Gen0")]
public class InitListBenchmark
{
[BenchmarkCategory("One")]
[Benchmark]
public List<int> InitList1()
{
return new List<int> {1};
}
[BenchmarkCategory("One")]
[Benchmark]
public List<int> InitListWithSize1()
{
return new List<int>(1) {1};
}
[BenchmarkCategory("Five")]
[Benchmark]
public List<int> InitList5()
{
return new List<int> {1, 2, 3, 4, 5};
}
[BenchmarkCategory("Five")]
[Benchmark]
public List<int> InitListWithSize5()
{
return new List<int>(5) {1, 2, 3, 4, 5};
}
[BenchmarkCategory("Ten")]
[Benchmark]
public List<int> InitList10()
{
return new List<int> {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
}
[BenchmarkCategory("Ten")]
[Benchmark]
public List<int> InitListWithSize10()
{
return new List<int>(10) {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
}
}
}
BenchmarkDotNet v0.15.6, Windows 10 (10.0.19045.6456/22H2/2022Update)
AMD Ryzen 7 7840H with Radeon 780M Graphics 3.80GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 10.0.100
[Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4
ShortRun-.NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v4
ShortRun-.NET 8.0 : .NET 8.0.22 (8.0.22, 8.0.2225.52707), X64 RyuJIT x86-64-v4
ShortRun-.NET Core 3.1 : .NET Core 3.1.32 (3.1.32, 4.700.22.55902), X64 RyuJIT VectorSize=256
ShortRun-.NET Framework 4.8 : .NET Framework 4.8.1 (4.8.9310.0), X64 RyuJIT VectorSize=256
IterationCount=3 LaunchCount=1 WarmupCount=3
| Method | Runtime | Mean | Allocated |
|--------------------|--------------------|----------:|----------:|
| InitList1 | .NET Framework 4.8 | 13.904 ns | 80 B |
| InitListWithSize1 | .NET Framework 4.8 | 6.658 ns | 72 B |
| InitList1 | .NET Core 3.1 | 11.091 ns | 72 B |
| InitListWithSize1 | .NET Core 3.1 | 7.407 ns | 64 B |
| InitList1 | .NET 8.0 | 10.084 ns | 72 B |
| InitListWithSize1 | .NET 8.0 | 6.838 ns | 64 B |
| InitList1 | .NET 10.0 | 8.170 ns | 72 B |
| InitListWithSize1 | .NET 10.0 | 6.638 ns | 64 B |
| InitList5 | .NET Framework 4.8 | 31.298 ns | 136 B |
| InitListWithSize5 | .NET Framework 4.8 | 12.013 ns | 88 B |
| InitList5 | .NET Core 3.1 | 26.466 ns | 128 B |
| InitListWithSize5 | .NET Core 3.1 | 9.446 ns | 80 B |
| InitList5 | .NET 8.0 | 23.714 ns | 128 B |
| InitListWithSize5 | .NET 8.0 | 15.587 ns | 80 B |
| InitList5 | .NET 10.0 | 20.002 ns | 128 B |
| InitListWithSize5 | .NET 10.0 | 8.712 ns | 80 B |
| InitList10 | .NET Framework 4.8 | 53.488 ns | 225 B |
| InitListWithSize10 | .NET Framework 4.8 | 18.185 ns | 104 B |
| InitList10 | .NET Core 3.1 | 44.371 ns | 216 B |
| InitListWithSize10 | .NET Core 3.1 | 12.496 ns | 96 B |
| InitList10 | .NET 8.0 | 38.707 ns | 216 B |
| InitListWithSize10 | .NET 8.0 | 12.024 ns | 96 B |
| InitList10 | .NET 10.0 | 33.854 ns | 216 B |
| InitListWithSize10 | .NET 10.0 | 15.822 ns | 96 B |
Setting the initial capacity prevents unnecessary memory allocations and results in better performance. And there's no excess memory traffic. The more elements we add to the list, the greater the difference we see in the benchmarks.

Analyzer
If performance is critical for your project, you should pay attention to these situations. Automatic diagnostics can help. I maintain a set of diagnostic tools based on Roslyn – Collections.Analyzer, and now it can detect lists with a collection initializer and without an initial capacity.

It's important to understand that the analyzer only sets the initial collection size to avoid unnecessary allocations during the initial collection phase. He cannot predict how the size of the collection will change in the future.
Recommendations
- If you need a static collection that you won't modify (add or remove elements), use arrays.
- If you are creating a list and exactly know its future size, set the initial capacity to that size.
- If you are creating a list and don't know its future size, set the expected size.