Selecting the appropriate data structure is frequently the key to C# performance improvement. Sequences of values can be represented by T[], List<T>, and Span<T>, although they differ significantly in terms of allocation, memory access, slicing, and iteration.
This comparison is made much more intriguing with.NET 10. JIT optimization, loop cloning, devirtualization, stack allocation, and Span<T> handling are all improved by the runtime. Microsoft particularly points out that.NET 10 extends significant JIT improvements to span-based loops and that more code is being developed around spans.
In order to compare arrays, lists, and spans, this article constructs a useful BenchmarkDotNet test and discusses when each strategy makes sense.
Why Compare Span, Array, and List?
At first glance, these types appear interchangeable:
int[] array = new int[1000];
List<int> list = new List<int>(1000);
Span<int> span = array;
All three allow indexed access:
value = array[index];
value = list[index];
value = span[index];
But their underlying behavior is different.
An array is a fixed-size managed object with contiguous elements.
List<T> is a dynamically sized collection backed internally by an array.
Span<T> is a lightweight ref struct representing a contiguous region of memory. It can provide a view over an array without creating another collection.
That distinction becomes important in hot loops, parsers, serializers, networking code, image processing, and other performance-sensitive workloads.
What Changed With .NET 10?
.NET 10 is an LTS release and introduces several runtime performance improvements, including better JIT code generation, devirtualization, stack allocation, and loop optimizations.
One particularly relevant improvement is that JIT loop cloning can now apply more effectively to span-based code.
Consider:
static int Sum(Span<int> values)
{
int total = 0;
for (int i = 0; i < values.Length; i++)
{
total += values[i];
}
return total;
}
The runtime can optimize this kind of loop aggressively.
Microsoft’s .NET 10 performance work specifically demonstrates improvements around span loops and bounds-check optimization.
C# 14 also introduces first-class span conversions, making interactions between arrays, Span<T>, and ReadOnlySpan<T> more natural.
Benchmark Setup
To make the comparison meaningful, use BenchmarkDotNet rather than relying on Stopwatch.
Create a console application:
dotnet new console -n SpanPerformance
cd SpanPerformance
dotnet add package BenchmarkDotNet
Then use the following benchmark:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
namespace SpanPerformance;
[MemoryDiagnoser]
public class CollectionBenchmarks
{
private int[] _array = null!;
private List<int> _list = null!;
[GlobalSetup]
public void Setup()
{
_array = Enumerable.Range(0, 10_000).ToArray();
_list = _array.ToList();
}
[Benchmark]
public int ArrayLoop()
{
int sum = 0;
for (int i = 0; i < _array.Length; i++)
{
sum += _array[i];
}
return sum;
}
[Benchmark]
public int ListLoop()
{
int sum = 0;
for (int i = 0; i < _list.Count; i++)
{
sum += _list[i];
}
return sum;
}
[Benchmark]
public int SpanLoop()
{
int sum = 0;
Span<int> span = _array;
for (int i = 0; i < span.Length; i++)
{
sum += span[i];
}
return sum;
}
}
Run the benchmark in Release mode:
dotnet run -c Release
BenchmarkDotNet executes multiple iterations and reports statistics such as mean execution time, error, standard deviation, and memory allocation.
Do not treat a single Stopwatch measurement as a reliable benchmark. JIT compilation, CPU frequency changes, garbage collection, OS scheduling, and other processes can distort short measurements.
Benchmark 1: Sequential Iteration
The first test asks a simple question:
How efficiently can each type be traversed?
The three implementations are conceptually equivalent:
for (int i = 0; i < array.Length; i++)
{
sum += array[i];
}
for (int i = 0; i < list.Count; i++)
{
sum += list[i];
}
Span<int> span = array;
for (int i = 0; i < span.Length; i++)
{
sum += span[i];
}
For this workload, you should generally expect array and span performance to be very close, particularly when the span is simply a view over the same array.
The important point is that Span<T> isn’t automatically a faster replacement for every array.
If the underlying data is already an array and your loop is straightforward, the JIT can optimize array access extremely well.
.NET 10 further improves these optimizations, including bounds-check handling and loop cloning for spans.
Benchmark 2: Slicing Without Allocation
This is where Span<T> becomes more interesting.
Suppose you only need elements 2,000 through 5,000 from an array.
A traditional approach might create a new array:
int[] subset = new int[3000];
Array.Copy(
_array,
2000,
subset,
0,
3000);
That creates additional storage and copies the data.
With a span:
Span<int> subset = _array.AsSpan(2000, 3000);
No new element array is created.
The span simply represents a view over the existing memory.
You can then process it:
int sum = 0;
foreach (int value in subset)
{
sum += value;
}
This is one of the strongest practical use cases for Span<T>.
Instead of:
Original array
↓
Copy data
↓
New array
↓
Process
you can use:
Original array
↓
Span view
↓
Process
That can reduce both allocation and copying.
Benchmark 3: List and Span
An interesting optimization appears when the source data is a List<T>.
Modern .NET provides:
CollectionsMarshal.AsSpan(list)
This exposes a span over the list’s backing storage. Microsoft documents this API as returning a Span<T> view over the list’s data.
Example:
using System.Runtime.InteropServices;
Span<int> span = CollectionsMarshal.AsSpan(_list);
int sum = 0;
for (int i = 0; i < span.Length; i++)
{
sum += span[i];
}
This can eliminate some abstraction overhead in performance-critical code.
However, there is an important safety rule.
You should not add or remove elements from the list while the span is being used. Microsoft explicitly documents this restriction for CollectionsMarshal.AsSpan.
Therefore, this technique should be reserved for controlled hot paths rather than becoming the default way you work with every List<T>.
Benchmark 4: Allocation Matters
Execution time isn’t the only metric.
Use:
[MemoryDiagnoser]
in BenchmarkDotNet to track allocations.
For example, consider this:
int[] source = GetData();
int[] copy = source[1000..5000];
The range operation creates a new array.
Compare that with:
ReadOnlySpan<int> view = source.AsSpan(1000, 4000);
The second operation creates a span view rather than copying the elements into another array.
This distinction can matter enormously inside high-throughput applications.
For example,
- JSON parsing
- HTTP processing
- binary protocols
- serialization
- image processing
- log processing
- file parsing
- network buffers
These workloads can process millions of small pieces of data, making unnecessary allocations expensive.
Span Does Not Own Memory
One of the most important concepts developers need to understand is that Span<T> isn’t an alternative collection in the same sense as List<T>.
It is better to think of it as a window over memory.
For example:
int[] numbers = { 10, 20, 30, 40, 50 };
Span<int> span = numbers.AsSpan(1, 3);
span[0] = 200;
The original array changes:
Console.WriteLine(numbers[1]);
Output:
200
The span didn’t create an independent copy.
It referenced the same memory.
This is one reason spans are powerful for high-performance APIs, but it is also why developers need to understand their lifetime and mutation behavior.
Array vs List vs Span
Here’s the practical comparison.
| Feature | Array | List | Span |
|---|---|---|---|
| Fixed size | Yes | No | View only |
| Dynamic resizing | No | Yes | No |
| Owns storage | Yes | Yes | No |
| Can avoid copying | Sometimes | Sometimes | Yes |
| Stack-friendly | No | No | Yes |
| Works over arrays | Natively | N/A | Yes |
| Works over list storage | N/A | Natively | Via CollectionsMarshal |
| Allocation-free view | No | No | Yes |
| Best use | Fixed collections | Dynamic collections | Hot-path memory access |
The key takeaway is that these aren’t competing abstractions in every scenario.
They solve different problems.
When Should You Use Array?
Use an array when:
- The collection size is known.
- You need simple indexed access.
- You own the data.
- The data needs to live on the managed heap.
- You don’t need dynamic resizing.
Example:
byte[] buffer = new byte[4096];
Arrays are also an excellent input for span-based APIs:
Process(buffer.AsSpan());
This allows an API to accept a span without forcing callers to copy their arrays.
When Should You Use List?
List<T> is still the right choice for many ordinary application scenarios.
Use it when:
- Elements need to be added or removed.
- Collection size changes dynamically.
- Developer productivity matters more than micro-optimization.
- You need normal collection APIs.
- You don’t have a demonstrated hot-path performance problem.
Example:
var users = new List<User>();
users.Add(user);
users.Remove(user);
Replacing every List<T> with a span would be a design mistake.
A span cannot replace the dynamic collection behavior that makes List<T> useful.
When Should You Use Span?
Span<T> becomes valuable when you need:
- Low-allocation processing
- Array slicing
- Buffer manipulation
- Parsing
- Memory-efficient APIs
- High-frequency loops
- Stack-based temporary storage
- Zero-copy processing
For example:
static int ParseFirstFourDigits(ReadOnlySpan<char> value)
{
return int.Parse(value[..4]);
}
The method can operate directly on a portion of existing character data rather than requiring a new string.
This style is particularly valuable in parsers and high-throughput infrastructure.
A More Interesting .NET 10 Benchmark
To test .NET 10 specifically, BenchmarkDotNet can compare multiple runtimes.
For example:
dotnet run -c Release \
--runtimes net9.0 net10.0
Your benchmark should then report separate results for each runtime.
This is more useful than simply asking which collection is faster because it demonstrates how the runtime itself affects the generated machine code.
Microsoft’s own .NET 10 performance investigation uses BenchmarkDotNet and reports improvements across areas including JIT optimization, spans, allocations, and collection processing.
Don’t Optimize Based on Assumptions
One of the biggest mistakes in C# performance engineering is assuming:
Span is always faster.
That isn’t true.
For a simple loop:
for (int i = 0; i < array.Length; i++)
{
sum += array[i];
}
an array can already be highly optimized by the JIT.
.NET 10 has specifically improved array interface devirtualization and array iteration optimization.
The right question isn’t:
Which type is fastest?
It is:
Which representation produces the least unnecessary work for this workload?
That distinction matters.
Practical Optimization Strategy
A good progression for production C# code is:
Step 1: Start with the simplest correct data structure
Use:
List<T>
when you need a dynamic collection.
Use:
T[]
when you need fixed-size storage.
Step 2: Profile
Identify the actual hot path.
Step 3: Benchmark
Use BenchmarkDotNet rather than intuition.
Step 4: Reduce allocations
Look for:
- unnecessary arrays
- string creation
- LINQ allocations
- temporary objects
- repeated conversions
- unnecessary copies
Step 5: Introduce Span
Use spans where they solve a demonstrated performance problem.
Step 6: Re-run the benchmark
Optimization isn’t complete until the benchmark demonstrates an improvement.
Final Verdict
There is no universal winner in the Span<T> vs array vs List<T> performance battle.
For straightforward sequential processing, arrays and spans can be extremely close, because a span may simply be a view over the same underlying array.
For dynamic collections, List<T> remains the practical choice.
For slicing, parsing, buffer manipulation, and allocation-sensitive hot paths, Span<T> becomes particularly powerful because it can provide a view over existing memory without copying the underlying elements.
.NET 10 makes this area even more compelling. Its JIT improvements extend optimization opportunities for spans, arrays, collections, and generated machine code, while C# 14 adds more natural span conversions.
The best performance strategy is therefore not to replace every collection with Span<T>. Instead, benchmark the actual workload, understand where allocations and bounds checks occur, and use the lowest-overhead representation that fits the ownership and lifetime requirements of the data.
Summary
For developers working on parsers, serializers, networking, high-throughput APIs, or other performance-critical .NET applications, that distinction can be far more important than the raw benchmark number from a single micro-test.
Best ASP.NET Core 10.0 Hosting
The feature and reliability are the most important things when choosing a good ASP.NET Core 10.0 hosting. HostForLIFE is the leading provider of Windows hosting and affordable ASP.NET Core , their servers are optimized for PHP web applications such as the latest ASP.NET Core 10.0 version. The performance and the uptime of the ASP.NET Core hosting service are excellent, and the features of the web hosting plan are even greater than what many hosting providers ask you to pay for. At HostForLIFE.eu, customers can also experience fast ASP.NET Core hosting. The company invested a lot of money to ensure the best and fastest performance of the datacenters, servers, network and other facilities. Its data centers are equipped with top equipment like cooling system, fire detection, high-speed Internet connection, and so on. That is why HostForLIFE.eu guarantees 99.9% uptime for ASP.NET Core . And the engineers do regular maintenance and monitoring works to assure its ASP.NET Core hosting are security and always up.

