Back to Blog
C#

C# Span vs String: When to Use Each for Performance

c# span vs string: Compare Span<T> and string in C# for memory efficiency and performance. Learn when to use each, how they differ, and practical examples.

Span<T>stringmemory allocationperformanceC#
Visual comparison of C# string and Span<T> showing a string as a contiguous block and a span as a view into that block.

c# span vs string requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When working with text in C#, string is the default choice because it is immutable, easy to use, and well integrated with the language. But when you need to process large text buffers, parse substrings, or avoid allocations in hot paths, Span<T> becomes a serious alternative. The key difference is that string always owns its character data, while Span<T> is a view over existing memory. Understanding this distinction helps you decide which one fits a given scenario.

What Span<T> Actually Is

Span<T> is a stack-only value type that provides a type-safe view over a contiguous region of memory. That memory can be an array, a string, a native buffer, or a stack-allocated block. A Span<char> can point into a string without copying the characters. It exposes methods like Slice, IndexOf, and SequenceEqual that work directly on the underlying memory.

string text = "Hello, world"; ReadOnlySpan<char> span = text.AsSpan(); ReadOnlySpan<char> world = span.Slice(7, 5); Console.WriteLine(world.ToString()); // "world"

The AsSpan method creates a span that references the string's internal buffer. No new string is allocated. This is the core advantage: you can read and process parts of a string without creating new string objects.

How string Differs from Span<T>

A string is a reference type that stores characters in a contiguous block on the managed heap. It is immutable by design, so any operation that appears to modify a string actually creates a new string. Substring, Replace, Trim, and concatenation all produce new allocations. Those allocations add up in performance-sensitive code.

Span<T> is a ref struct that lives on the stack. It cannot be stored in a field, boxed, or used as a generic type argument. This restriction exists because a span can point to stack memory, which is not valid after the stack frame returns. The compiler enforces this to prevent dangling references.

AspectstringSpan<T> (ReadOnlySpan<char>)
Memory locationManaged heapStack (ref struct)
ImmutabilityImmutableMutable for Span<T>, read-only for ReadOnlySpan<T>
AllocationAlways allocatedNo allocation when viewing existing memory
Use in fieldsYesNo
Async supportYesNot allowed in async methods

This table highlights the practical differences. The most important for performance is allocation behavior.

When string Substring Creates Unnecessary Allocations

Consider a method that extracts a portion of a string and then passes it to another method. Using Substring copies the characters into a new string. If the extracted portion is only needed for a short time, that copy is wasteful.

public static string GetDomain(string email) { int atIndex = email.IndexOf('@'); return email.Substring(atIndex + 1); }

Each call allocates a new string for the domain. If this method is called frequently, the allocations can cause pressure on the garbage collector. Using ReadOnlySpan<char> avoids that allocation:

public static ReadOnlySpan<char> GetDomainSpan(string email) { int atIndex = email.IndexOf('@'); return email.AsSpan(atIndex + 1); }

The caller now receives a view into the original string. No new memory is allocated. But the caller must be aware that the span is only valid as long as the original string is alive and not modified (strings are immutable, so that is not a concern).

Performance: Where Span<T> Helps and Where It Doesn't

Span<T> shines when you need to process many small pieces of a large buffer. Parsing CSV lines, extracting tokens from a log file, or validating user input are common examples. Because you avoid allocating substrings, you reduce GC pressure and improve cache locality.

However, Span<T> is not always faster. If you need to store the extracted text for later use, you eventually have to convert it to a string. That conversion allocates. The benefit only appears when the span is used transiently within a narrow scope.

Also, Span<T> cannot be used in async methods because async can suspend and resume the method, and the span's stack memory would be invalid. If you need to process text asynchronously, you must convert to a string first.

Practical Example: Parsing a Key-Value Pair

Suppose you have a string like "key=value" and you want to extract both parts without allocations. With string you might write:

string input = "key=value"; int eq = input.IndexOf('='); string key = input.Substring(0, eq); string value = input.Substring(eq + 1);

With ReadOnlySpan<char> you can avoid both allocations:

ReadOnlySpan<char> inputSpan = input; int eq = inputSpan.IndexOf('='); ReadOnlySpan<char> keySpan = inputSpan.Slice(0, eq); ReadOnlySpan<char> valueSpan = inputSpan.Slice(eq + 1);

Now keySpan and valueSpan are views into the original string. If you need to use them as strings, you can call ToString() at the point of use, but only if you actually need a string. For operations like comparison, SequenceEqual or MemoryExtensions methods work directly on spans.

Limitations and Compatibility Concerns

Span<T> is a ref struct, so it cannot be used in many places where string is allowed. For example, you cannot store a span in a class field, use it as a lambda capture, or use it in an iterator method. This limits its use in APIs that require heap storage.

Additionally, Span<T> is not supported in .NET Framework without additional packages. It is fully supported in .NET Core 2.1+ and .NET 5+. If you are targeting older frameworks, you may need the System.Memory NuGet package.

Another limitation is that ReadOnlySpan<char> does not have all the methods that string has. For instance, you cannot use Split directly on a span. You need to manually iterate or use MemoryExtensions methods. This can make code more verbose, but the performance benefit often justifies it in hot paths.

Decision Criteria: Which One Should You Use?

Use string when:

  • You need to store the text in a field, return it from a method, or pass it to an async method.
  • The text is short and allocation pressure is not a concern.
  • You rely on the rich string API like Split, Replace, or Regex.

Use ReadOnlySpan<char> when:

  • You are processing a large buffer and need to extract temporary slices.
  • You are writing a parser or a high-performance library where GC pressure matters.
  • The extracted data is used only within the current method or a small scope.

A common pattern is to use ReadOnlySpan<char> for the parsing logic and only convert to string when you need to return or store the result. This gives you the best of both worlds: low allocation during processing and the flexibility of string at the boundaries.

Handling the Case Where You Need a String Later

If you have a span and you eventually need a string, you call ToString() on the span. This creates a new string that copies the characters. That is the same cost as Substring, so you should only do it when necessary. The advantage of using span is that you can delay the allocation until the last possible moment, and in many cases you can avoid it altogether.

public static string ExtractValue(string input) { int eq = input.IndexOf('='); if (eq < 0) return string.Empty; ReadOnlySpan<char> valueSpan = input.AsSpan(eq + 1); return valueSpan.ToString(); // allocation happens here }

If the caller only needs to inspect the value, you could return the span instead. But returning a span from a method is only safe if the original string is guaranteed to outlive the span. In practice, many APIs return ReadOnlySpan<char> and document that the caller must not store it beyond the source's lifetime.

Advanced Usage: Stack-Allocated Buffers

Span<T> can also point to stack-allocated memory using stackalloc. This is useful for temporary buffers that are too large for a single stack frame but still avoid heap allocation. For example, you might format a number into a stack buffer and then use a span to read it.

Span<char> buffer = stackalloc char[32]; int written = value.TryFormat(buffer, out int charsWritten); ReadOnlySpan<char> formatted = buffer.Slice(0, charsWritten);

This pattern is common in high-performance logging or serialization code. It avoids any heap allocation for the formatted output. The tradeoff is that stack memory is limited, so you must be careful with buffer sizes.

Final Consideration: Readability and Maintainability

While Span<T> can improve performance, it also makes code harder to read because you lose the convenience of string methods. You often have to write manual loops or use extension methods that are less familiar. For most application code, string is the right choice. Reserve Span<T> for code that is proven to be a bottleneck through profiling.

When you do use spans, document the lifetime constraints clearly. A span that outlives its source is a bug waiting to happen. By keeping spans local and short-lived, you get the performance benefits without compromising correctness.

c# span vs string: Practical Usage and Code Examples | RYUSLOG DEV