Introduction
In my previous articles on [deferred execution], [multiple enumerations], and [5 performance patterns], we covered *when* to materialize LINQ queries. But once you’ve decided to materialize, which method should you choose: .ToList() or .ToArray()?
The answer surprised me. After running comprehensive benchmarks using BenchmarkDotNet, I discovered the performance difference depends entirely on how you’re building the collection. For most LINQ queries, the difference is negligible (< 1%). But for incremental building with .Add(), List can waste 162% more memory and run 2.4x slower than arrays.
This article provides:
- Performance benchmarks comparing both methods
- Memory allocation patterns and their impact
- A decision framework for choosing the right method
- Common mistakes and how to avoid them
Let’s dive into the differences that actually matter in production.
Understanding the Two Methods
Before we compare performance, let’s understand what each method actually does.
ToList() — Dynamic Array with Flexibility
IEnumerable<int> numbers = GetNumbers();
List<int> list = numbers.ToList();
What it does:
- Enumerates the source sequence once
- Creates a List with dynamic capacity (starts at 4, doubles when full)
- Allows adding/removing items after creation
- Provides indexed access with list[index]
Key characteristics:
- Initial capacity: 4 elements (or you can specify)
- Growth strategy: Doubles capacity when full (4 → 8 → 16 → 32…)
- Extra memory overhead: ~40 bytes for the List object + array backing
- Resizable: Yes
ToArray() — Fixed-Size Array
IEnumerable<int> numbers = GetNumbers();
int[] array = numbers.ToArray();
What it does:
- Enumerates the source sequence once (or twice if size unknown)
- Creates a fixed-size array with exact capacity
- Immutable size (can’t add/remove, only modify existing elements)
- Provides indexed access with array[index]
Key characteristics:
- Exact size allocation (no wasted capacity)
- No resizing overhead during creation (if size known)
- Minimal memory overhead: Just the array
- Immutable size: Cannot add/remove elements
📌 What About AsEnumerable()?
AsEnumerable() is NOT a materialization method — it doesn’t create a collection at all. It’s a type cast from IQueryable to IEnumerable that changes how LINQ operators are resolved.
Use it when: You need to switch from database queries (IQueryable) to in-memory operations (IEnumerable) — for example, when you have a custom method that can’t translate to SQL.
Don’t confuse it with ToList()/ToArray() — they serve completely different purposes. If you need to materialize your query results, use ToList() or ToArray(), not AsEnumerable().
Performance Benchmarks
I benchmarked both methods using BenchmarkDotNet on .NET 9.0.11, Windows 11, Intel Core Ultra 7 165H across four different scenarios. The results reveal when the performance difference actually matters.
Scenario 1: LINQ Queries with Known Size (Most Common)
This represents typical LINQ usage — calling .ToList() or .ToArray() on a query result where the count is known (like most Entity Framework queries, Enumerable.Range(), or materialized collections).
- 1,000 items:
ToList(): 164.5 ns | 3.96 KB allocated
ToArray(): 168.2 ns | 3.93 KB allocated
- 100,000 items:
ToList(): 108.9 μs | 390.72 KB allocated
ToArray(): 108.0 μs | 390.69 KB allocated
Result: Virtually Identical (< 1% difference)
When the source collection has a known size, both methods perform nearly identically:
- Speed difference: 0.8% (negligible)
- Memory difference: 0.008% (essentially zero)
Key insight: For most LINQ queries, choose based on mutability needs , not performance.
Scenario 2: Incremental Building with Add() (Where It Matters!)
This represents building collections incrementally without pre-allocating capacity — the scenario where List’s doubling strategy causes significant waste.
- 1,000 items (built incrementally):
List (via Add()): 1,001 ns | 8.23 KB allocated
Array (via indexing): 422 ns | 3.93 KB allocated
- 100,000 items (built incrementally):
List (via Add()): 293.5 μs | 1,024.48 KB allocated
Array (via indexing): 120.1 μs | 390.69 KB allocated
Result: Array Wins Dramatically
- Memory: Array uses 2.6x less memory (List wastes 162% more!)
- Speed: Array is 2.4x faster
Why? When you build a List incrementally with .Add(), it grows capacity in powers of 2:
- For 100,000 items: 4 → 8 → 16 → 32 → 64 → … → 131,072
- Final capacity: 131,072
- Actual items: 100,000
- Wasted slots: 31,072 (23.7% waste)
Scenario 3: Pre-Allocated List (Best Practice)
What happens when you pre-allocate List capacity upfront with new List(capacity)?
- 100,000 items:
List (pre-allocated capacity): 139.1 μs | 390.72 KB allocated
Array: 120.1 μs | 390.69 KB allocated
Result: Memory Matches, Speed Still Favors Array
- Memory: Identical (no waste when pre-allocated!)
- Speed: Array still 16% faster
Key insight: If you know the size upfront, pre-allocate your List to avoid waste.
Decision Framework: Which Method to Choose?
After seeing the benchmark results, here’s the practical decision framework:
Decision Tree
- Are you building the collection incrementally with .Add()?
├─ Yes → Use Array (or pre-allocate List capacity if you know the size)
└─ No → Continue…
- Are you calling .ToList() or .ToArray() on a LINQ query?
├─ Yes → Choose based on mutability needs (performance difference < 1%)
│ ├─ Need to add/remove items? → ToList()
│ └─ Want immutability? → ToArray()
└─ No → Continue…
- Do you know the final size upfront?
├─ Yes → Pre-allocate List capacity: new List(size)
└─ No → Use ToArray() for exact allocation
Quick Reference Guide
- When to use either (< 1% performance difference):
- Calling .ToList() or .ToArray() on LINQ queries
2. When to use ToList():
- Need to add/remove items after creation (only List is resizable)
- Know size upfront: use new List(capacity) — matches Array memory, allows mutation
3. When to use ToArray():
- Want immutable return values (prevents accidental modification)
- Building incrementally with .Add() — Array is 2.4x faster, uses 2.6x less memory
- Large collections (100K+) built incrementally — avoids 23.7% capacity waste
General tip:
- Passing to method expecting specific type? Match the signature to avoid unnecessary conversions
Real-World Scenarios
Let’s look at practical examples:
Scenario 1: API Response DTOs (Performance: Identical)
// Either works - performance difference < 1%
public async Task<List<DocumentDto>> GetDocumentsAsync(int userId)
{
return await dbContext.Documents
.Where(d => d.UserId == userId)
.Select(d => new DocumentDto { … })
.ToList();
}
// Prefer array for immutability (not performance)
public async Task<DocumentDto[]> GetDocumentsAsync(int userId)
{
return await dbContext.Documents
.Where(d => d.UserId == userId)
.Select(d => new DocumentDto { … })
.ToArray();
}
Why prefer array:
- Response DTOs shouldn’t be modified by callers (immutability)
- Signals intent clearly (“this won’t change”)
- Performance difference is negligible (< 1%)
- Not because of memory savings (they’re identical for LINQ queries)
Scenario 2: Incremental Building (Performance: Array Wins!)
This is where the performance difference actually matters — building collections with .Add() without pre-allocating.
// BAD: Building without pre-allocation wastes memory
var results = new List<Document>();
foreach (var item in items)
{
if (item.IsValid)
{
results.Add(item); // Causes multiple resizes, wastes 162% more memory!
}
}
return results; // Capacity might be 131,072 for 100,000 items
// BETTER: Pre-allocate if you know approximate size
var results = new List<Document>(items.Count);
foreach (var item in items)
{
if (item.IsValid)
{
results.Add(item); // No resize waste!
}
}
return results;
// BEST: Use array if size is known
var results = new Document[items.Count];
int index = 0;
foreach (var item in items)
{
if (item.IsValid)
{
results[index++] = item; // Fastest, minimal memory
}
}
return results.Take(index).ToArray(); // Trim to actual size
Scenario 3: Need Mutability After Creation
// Use List when you need to modify after creation
var results = query.ToList(); // < 1% performance difference vs ToArray()
if (includeDefaults)
{
results.AddRange(GetDefaultItems()); // Can't do this with arrays!
}
return results;
Scenario 4: Processing Large Datasets
// For LINQ queries, ToList vs ToArray makes minimal difference
var allRecords = dbContext.Records.ToList(); // 500K records
// vs
var allRecords = dbContext.Records.ToArray(); // 500K records
// Memory difference: < 0.01% (essentially identical)
// The real optimization: don't materialize at all if possible
await foreach (var record in dbContext.Records.AsAsyncEnumerable())
{
ProcessRecord(record); // Processes one at a time, minimal memory
}
Common Mistakes
Mistake 1: Using ToList() for Immutable Return Values
// BAD: Caller can modify the returned list
public List<string> GetApprovedStatuses()
{
return new[] { "Approved", "Verified", "Published" }.ToList();
}
// Caller can do this:
var statuses = GetApprovedStatuses();
statuses.Add("Hacked"); // Modifies "immutable" constant!
// GOOD: Return array to prevent modification
public string[] GetApprovedStatuses()
{
return new[] { "Approved", "Verified", "Published" };
}
Mistake 2: ToArray() with Unknown Size Source
// SLOWER: ToArray with unknown size enumerates twice
public int[] ProcessResults(IEnumerable<int> source)
{
// First enumeration: Count items
// Second enumeration: Fill array
return source.ToArray();
}
// If you know approximate size, ToList with capacity is better
public List<int> ProcessResults(IEnumerable<int> source, int estimatedSize)
{
var result = new List<int>(estimatedSize); // Pre-allocate
result.AddRange(source);
return result;
}
Performance Tips
Tip 1: Pre-Allocate List Capacity When Known
// SLOW: List resizes multiple times
var results = new List<Document>();
foreach (var item in items)
{
results.Add(ProcessItem(item));
}
// FAST: Pre-allocate capacity
var results = new List<Document>(items.Count);
foreach (var item in items)
{
results.Add(ProcessItem(item));
}
With known capacity, ToList() can match ToArray() performance.
Tip 2: Pre-Allocate for Incremental Building
For incremental building over 10K items, the difference is dramatic:
// BAD: Incremental building without pre-allocation
var list = new List<Document>();
foreach (var item in items) // 100K items
{
list.Add(item); // Wastes 162% more memory!
}
// GOOD: Pre-allocate capacity
var list = new List<Document>(100000);
foreach (var item in items)
{
list.Add(item); // No waste!
}
Tip 3: Return Arrays from Public APIs
// GOOD: Immutable return type
public DocumentDto[] GetRecentDocuments() => /* … */
// Prevents:
var docs = GetRecentDocuments();
docs[0] = null; // Can still modify elements
docs = docs.Concat(newDoc).ToArray(); // But can't add/remove
When Performance Doesn’t Matter
For LINQ queries, the performance difference is negligible regardless of collection size:
// Both perform identically for LINQ queries
var userIds = query.ToList(); // 3.96 KB, 164.5 ns
var userIds = query.ToArray(); // 3.93 KB, 168.2 ns
// Difference: < 1% - won't impact your app
Choose based on intent, not performance:
- Use ToList() if you might modify the collection
- Use ToArray() for immutable return values
- Don’t worry about the performance difference for LINQ queries!
Summary
ToList():
- ✅ When you need to add/remove items
- ✅ When flexibility matters more than immutability
- ✅ Performance identical to ToArray() for LINQ queries (< 1%)
- ✅ Familiar API with many helper methods
- ❌ For incremental building without pre-allocation (wastes 162% more memory!)
ToArray():
- ✅ For immutable return values (signals intent)
- ✅ For incremental building scenarios (2.4x faster than List without pre-allocation)
- ✅ Clear intent: “This collection won’t change”
- ✅ Performance identical to ToList() for LINQ queries (< 1%)
- ❌ When you need to modify collection after creation
Pre-allocated List:
- ✅ When you know the size and need mutability
- ✅ Matches Array memory, allows modification
- ✅ Best of both worlds: new List(capacity)
Conclusion
The choice between ToList() and ToArray() isn’t about micro-optimization — it’s about context and intent.
The surprising truth from benchmarks:
- For LINQ queries: Performance difference < 1% → Choose based on mutability needs
- For incremental building: List without pre-allocation wastes 162% more memory → Use Array or pre-allocate
Practical decision:
Calling **.ToList() or ***.ToArray() on a LINQ query?* → Choose based on whether you need mutability (performance is identical)
Building incrementally with **.Add()?** → Pre-allocate capacity or use arrays
Need immutable return values? → Use ToArray() to signal intent
Start by choosing based on mutability and intent, not performance myths. The real performance difference only matters when building collections incrementally without pre-allocation.
Further Reading
Previous articles in this series:
- [Part 1: Deferred Execution — The Essence of LINQ in C#]
- [Part 2: Multiple Enumerations in LINQ Expressions]
- [Part 3: 5 Performance Patterns Every C# Developer Should Know]
- [Part 4: Howyield return Reduces Memory by 90% in C#]
What’s your experience with ToList() vs ToArray()? Have you encountered scenarios where the choice made a significant difference? Share in the comments!

Top comments (0)