C# List Capacity Management and Expansion
This article provides a comprehensive overview of how the generic
List<T> collection manages its internal memory in C#.
It explores the critical distinction between the Count and
Capacity properties, breaks down the internal array
reallocation process during capacity expansion, analyzes the performance
and garbage collection impacts of dynamic resizing, and details best
practices to optimize list operations.
The Underlying Structure of List<T>
Although List<T> behaves like an elastic data
structure, it is internally backed by a flat, contiguous array
(T[]). Because standard arrays in .NET have a fixed size
once allocated, List<T> handles dynamic growth by
automatically creating new arrays behind the scenes when space runs
out.
Count vs. Capacity
Understanding internal memory management requires distinguishing between two fundamental properties:
Count: The actual number of elements currently stored in the list. This value increases by one each time an element is added and decreases when an element is removed.Capacity: The total number of elements that the internal array can hold without needing to resize. This value is always greater than or equal toCount.
When a list is initialized using the default parameterless
constructor (new List<T>()), its initial
Capacity is 0, and the internal array is set
to an empty array singleton.
How Capacity Expansion Works
When an element is added using methods like Add() or
AddRange(), the list checks whether Count + 1
exceeds Capacity. If the internal array has sufficient
room, the element is placed at index Count, and
Count is incremented. If the array is full, an expansion
process is triggered.
The expansion follows a specific sequence:
- Calculate New Capacity: If the current capacity is
0, the runtime sets the new capacity to a default minimum, which is4. If the capacity is already greater than0, the runtime doubles the current capacity (Capacity * 2). - Handle Upper Limits: If doubling the capacity
exceeds the maximum allowed array size (
Array.MaxLength), the capacity is clamped toArray.MaxLengthorint.MaxValue. - Allocate New Array: A brand-new contiguous array of the calculated capacity is allocated on the managed heap.
- Copy Elements: All existing elements from the old
internal array are copied to the new array using
Array.Copy. - Reassign Reference: The internal reference pointer is updated to point to the new array. The previous array is abandoned and becomes eligible for Garbage Collection (GC).
- Insert Element: The new item is placed into the first empty slot.
Performance Implications
Understanding this growth cycle is critical for writing high-performance C# code due to two primary factors:
Time Complexity
- Amortized O(1): While inserting an element into an
existing array slot is an \(O(1)\)
constant-time operation, resizing requires an \(O(N)\) operation because every existing
element must be copied to the new array. Because the capacity doubles
each time, expansions become less frequent as the list grows,
maintaining an amortized \(O(1)\) time
complexity for
Add. - Latency Spikes: In time-critical paths, an unexpected resize introduces CPU pauses due to memory allocation and large-block copying.
Garbage Collection and Memory Overhead
Frequent resizes generate multiple orphaned arrays that must be cleaned up by the Garbage Collector. For lists containing large numbers of elements or large value types, the internal array can exceed 85,000 bytes, which places it directly onto the Large Object Heap (LOH). Repeated expansions on the LOH can cause heap fragmentation and trigger costly full Gen 2 GC sweeps.
Best Practices for Capacity Management
1. Pre-allocate Capacity
If the number of items is known or can be estimated in advance, define the capacity during instantiation to eliminate all reallocation overhead:
// Allocates an internal array of size 1,000 immediately
List<int> numbers = new List<int>(1000);2. Manual Capacity Adjustments
You can manually adjust the capacity at any point during runtime before performing bulk operations:
List<string> items = new List<string>();
items.Capacity = expectedItemCount;3. Reclaiming Memory with TrimExcess
If a list grows significantly and then sheds elements without expecting further growth, you can release unused capacity back to the system:
items.TrimExcess();TrimExcess() checks if the list is utilizing less than
90% of its allocated capacity. If so, it allocates a new array sized
precisely to the current Count, copies the active items
over, and frees the remaining buffer.