Strings in .NET are immutable. Every concatenation, substring, or format operation allocates a new string. In hot paths, these allocations compound into serious GC pressure. The framework provides several tools to reduce this cost, and string.Create is one of the most powerful.
The Cost of String Operations
Consider building a formatted identifier:
// Three allocations: prefix.ToUpper(), id.ToString("X8"), and the concatenation
public string FormatId(string prefix, int id)
{
return prefix.ToUpperInvariant() + "-" + id.ToString("X8");
}
Each intermediate result allocates a new string. For high-throughput code, this is wasteful.
string.Create to the Rescue
string.Create allocates the final string once and lets you write directly into its character buffer via a Span<char>. No intermediate strings needed.
public string FormatId(string prefix, int id)
{
// prefix length + 1 (hyphen) + 8 (hex digits)
int length = prefix.Length + 1 + 8;
return string.Create(length, (prefix, id), static (span, state) =>
{
var (prefix, id) = state;
// Copy the prefix in uppercase
prefix.AsSpan().ToUpperInvariant(span);
// Add the hyphen
span[prefix.Length] = '-';
// Format the ID directly into the remaining span
id.TryFormat(span[(prefix.Length + 1)..], out _, "X8");
});
}
The string.Create method allocates a single string of the specified length, then invokes your SpanAction<char, TState> delegate to fill it. The delegate receives a writable Span<char> backed by the string's internal buffer — something normally impossible due to string immutability. Once the delegate completes, the string is sealed and returned.
Interpolated String Handlers (C# 10+)
C# 10 introduced interpolated string handlers, which the compiler uses to optimise string interpolation. When you write:
string result = $"User {name} logged in at {timestamp:O}";
The compiler can use DefaultInterpolatedStringHandler, which writes into a stack-allocated or pooled buffer rather than creating intermediate strings. This happens automatically — you do not need to change your code.
For custom scenarios, you can use the handler explicitly:
public string BuildGreeting(string name, int visits)
{
var handler = new DefaultInterpolatedStringHandler(
literalLength: 20,
formattedCount: 2);
handler.AppendLiteral("Welcome back, ");
handler.AppendFormatted(name);
handler.AppendLiteral("! Visit #");
handler.AppendFormatted(visits);
return handler.ToStringAndClear();
}
Avoiding String Allocations Entirely
Sometimes you can avoid creating strings altogether. Several .NET APIs now accept ReadOnlySpan<char> instead of string:
// Instead of creating a substring
string header = line.Substring(0, colonIndex);
// Work with a span — zero allocation
ReadOnlySpan<char> header = line.AsSpan(0, colonIndex);
// Many APIs accept spans directly
if (header.SequenceEqual("Content-Type"))
{
// ...
}
int.TryParse, DateTime.TryParse, Guid.TryParse, and many other parsing methods accept ReadOnlySpan<char>.
String Comparison Performance
String comparison method matters for performance:
// Slowest — culture-aware by default
string.Equals(a, b);
// Fast — ordinal byte-by-byte comparison
string.Equals(a, b, StringComparison.Ordinal);
// Fast case-insensitive — avoids culture overhead
string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
For dictionary keys and hash sets, always use StringComparer.Ordinal or StringComparer.OrdinalIgnoreCase. Culture-aware comparisons are significantly slower and rarely needed for programmatic string matching.
SearchValues (NET 8+)
.NET 8 introduced SearchValues<T> for optimised character searching:
private static readonly SearchValues<char> _vowels =
SearchValues.Create("aeiouAEIOU");
public int CountVowels(ReadOnlySpan<char> text)
{
int count = 0;
int index;
while ((index = text.IndexOfAny(_vowels)) >= 0)
{
count++;
text = text[(index + 1)..];
}
return count;
}
SearchValues selects an optimised search strategy at construction time — potentially using SIMD instructions — making it significantly faster than repeated IndexOfAny(char[]) calls.
Summary
String performance in .NET is a solved problem if you know which tools to use. string.Create eliminates intermediate allocations. Interpolated string handlers optimise formatting automatically. ReadOnlySpan<char> avoids allocations entirely. And ordinal comparisons skip expensive culture-aware processing. Start with the simplest approach and reach for string.Create when profiling shows string allocations dominating a hot path.