DEV Community

Dominika Sikorska
Dominika Sikorska

Posted on • Originally published at towardsdev.com on

ToList() vs ToArray() in C#: The Performance Guide Every Developer Needs

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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

  1. Are you building the collection incrementally with .Add()?

├─ Yes → Use Array (or pre-allocate List capacity if you know the size)

└─ No → Continue…

  1. 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…

  1. Do you know the final size upfront?

├─ Yes → Pre-allocate List capacity: new List(size)

└─ No → Use ToArray() for exact allocation

Quick Reference Guide

  1. 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();
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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" };
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Calling **.ToList() or ***.ToArray() on a LINQ query?* → Choose based on whether you need mutability (performance is identical)

  2. Building incrementally with **.Add()?** → Pre-allocate capacity or use arrays

  3. 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:

What’s your experience with ToList() vs ToArray()? Have you encountered scenarios where the choice made a significant difference? Share in the comments!

CSharp #LINQ #DotNet #Performance #SoftwareEngineering


Top comments (0)