Measure Twice Allocate Once: Faster Lists in .NET
Lists grow by reallocating and copying, which adds hidden cost as data scales. This post shows why setting List capacity up front reduces allocations and improves cache behavior. You will see small runnable examples and practical patterns you can drop into production.
I once tried to pack a moving truck by tossing boxes in as they came down the stairs. Ten minutes later I was playing Tetris on hard mode, sweating, and learning a life lesson. Planning the space up front makes the whole day easier. Lists in .NET feel the same. If you know how many items you will add, telling the List about that size will save time, memory, and a few GC-induced sighs.
Why size matters more than we think
A fresh List<T> starts small. When you keep adding items and the internal array fills, the runtime allocates a bigger array, copies the data, and keeps going. That copy happens multiple times as the list grows. Each resize is like unpacking and repacking the truck. You will get there, but it costs time and extra lifting.
The fix is simple. If you know the ballpark count, set the capacity up front. When you do, the List allocates once and avoids a chain of resizes. Fewer allocations means less GC work and better cache locality, which is a fancy way of saying your CPU stays happier.
A peek under the hood
List<T> grows by jumping to a larger internal array when adding would exceed the current capacity. The growth strategy is roughly double on each resize after the first allocation. That is fast in big O terms, but it still copies everything on each jump. Capacity is the size of the internal array. Count is how many elements you actually have.
Let us watch capacity change in a tiny demo.
using System;using System.Collections.Generic;
var hobbits = new List<string>();for (int i = 1; i <= 9; i++){ hobbits.Add($"Hobbit-{i}"); Console.WriteLine($"Count={hobbits.Count}, Capacity={hobbits.Capacity}");}You will see the capacity jump at certain adds. Each jump is an allocation plus a copy of existing elements.
A micro test with and without a preset capacity
Let’s compare two short runs that add ten thousand integers. We are not doing a full benchmark suite here, just enough to show the trend.
using System;using System.Collections.Generic;using System.Diagnostics;
int n = 10_000;var sw = Stopwatch.StartNew();var defaultList = new List<int>();for (int i = 0; i < n; i++) defaultList.Add(i);sw.Stop();Console.WriteLine($"Default capacity: {sw.ElapsedMilliseconds} ms");using System;using System.Collections.Generic;using System.Diagnostics;
int n = 10_000;var sw = Stopwatch.StartNew();var preSized = new List<int>(n);for (int i = 0; i < n; i++) preSized.Add(i);sw.Stop();Console.WriteLine($"Pre-sized: {sw.ElapsedMilliseconds} ms");On most machines the second run is noticeably faster and allocates less. The bigger the n, the bigger the gap.
Practical patterns that pay off
Think of these as your packing strategies so you do not end up with a box of cables labeled maybe important.
- If you know the count, set it
var stormtroopers = new List<int>(5_000);for (int id = 0; id < 5_000; id++) stormtroopers.Add(id);- If you can estimate, ensure it
var attendees = new List<string>();attendees.EnsureCapacity(1_000);foreach (var name in GetEarlyBirds()) attendees.Add(name);- If you already have a collection, AddRange is your friend
var rebels = new List<string>();var recruits = GetRecruits().ToList();rebels.AddRange(recruits);- Done building and want to trim memory
var snacks = new List<string>(10_000);Fill(snacks);snacks.TrimExcess();- Streaming unknown size with a rolling guess
var dwarves = new List<string>();dwarves.EnsureCapacity(64);foreach (var d in StreamDwarves()) dwarves.Add(d);Real world scenarios
- Web APIs that buffer results before writing the response. If you know the page size or an upper bound, set capacity when you create the buffer list.
- Data import jobs. Row counts are often known or can be read from metadata. Use that number to pre-size working lists.
- Query composition with LINQ. If you materialize with
ToList()and you started from a collection that knows itsCount, the framework already optimizes the allocation. If the source is a stream or iterator that does not know the count, considerEnsureCapacitybefore you iterate.
Quick Q and A
-
What if I overestimate and allocate too much
- You will hold a larger backing array than needed. That increases peak memory but avoids resizes. If memory is tight, call
TrimExcess()after building or dial down the estimate.
- You will hold a larger backing array than needed. That increases peak memory but avoids resizes. If memory is tight, call
-
What if I underestimate
- The list will still grow, but you reintroduce some resizing. A reasonable guess still reduces the number of jumps.
-
Does this advice apply to
Dictionary<TKey,TValue>andHashSet<T>- Yes. Both have constructors that accept capacity, which help avoid rehashing and resizing.
-
Is this micro optimization or material in production
- Both. For hot paths or large data sets, the impact is easy to measure. For small lists you will not notice much, but the habit keeps you out of trouble when the stakes grow.
Wrap up
Lists are the lunchbox of C#. Pack them with a plan. A small change in how you initialize a List<T> can cut allocations, calm the GC, and speed up tight loops. Measure on your workload, pick a sensible capacity, and let your code cruise instead of stop and go.
Sign in to join in. Reading needs nothing.