String Interpolation and Performance in C#

String interpolation with the $"" syntax is one of C#'s most-used features. It replaced string.Format calls with something far more readable. But readability is only half the story — C# 10 introduced the interpolated string handler pattern, transforming what was once a convenience feature into a genuine performance tool.

The Basics

String interpolation lets you embed expressions directly in string literals:

Example.cs
var name = "Alice";
var age = 30;
var message = $"Hello, {name}! You are {age} years old.";

Prior to C# 10, this compiled into a string.Format call, which meant allocating an object[] array and boxing any value-type arguments. For a feature used in hot paths and logging, that overhead added up.

C# 10: Interpolated String Handlers

C# 10 changed the game with DefaultInterpolatedStringHandler. The compiler now lowers interpolated strings into a series of Append calls on a stack-allocated handler:

Example.cs
// What you write:
var message = $"Hello, {name}! You are {age} years old.";

// What the compiler generates (approximately):
var handler = new DefaultInterpolatedStringHandler(28, 2);
handler.AppendLiteral("Hello, ");
handler.AppendFormatted(name);
handler.AppendLiteral("! You are ");
handler.AppendFormatted(age);
handler.AppendLiteral(" years old.");
var message = handler.ToStringAndClear();

The handler uses Span<char> and stack-allocated buffers internally. Value types are formatted directly without boxing. The result is significantly less allocation than the old string.Format path.

Constant Folding

When all parts of an interpolated string are compile-time constants, the compiler folds them into a single constant string:

Example.cs
const string greeting = "Hello";
const string target = "World";

// This is a compile-time constant — no runtime allocation
string message = $"{greeting}, {target}!";

This means you can use interpolation for readability without any runtime cost when the values are known at compile time.

Custom Interpolated String Handlers

You can write your own handlers to control how interpolated strings are processed. This is how high-performance logging avoids allocations when a log level is disabled:

Example.cs
[InterpolatedStringHandler]
public ref struct LogInterpolatedStringHandler
{
    private DefaultInterpolatedStringHandler _inner;
    private readonly bool _isEnabled;

    public LogInterpolatedStringHandler(
        int literalLength, int formattedCount, ILogger logger, LogLevel level,
        out bool handlerIsValid)
    {
        _isEnabled = logger.IsEnabled(level);
        handlerIsValid = _isEnabled;

        if (_isEnabled)
            _inner = new DefaultInterpolatedStringHandler(literalLength, formattedCount);
    }

    public void AppendLiteral(string s)
    {
        if (_isEnabled) _inner.AppendLiteral(s);
    }

    public void AppendFormatted<T>(T value)
    {
        if (_isEnabled) _inner.AppendFormatted(value);
    }

    public string ToStringAndClear() =>
        _isEnabled ? _inner.ToStringAndClear() : string.Empty;
}

When handlerIsValid is set to false, the compiler skips evaluating the interpolation holes entirely. No formatting, no allocations, no side effects from computing the values.

Format Specifiers and Alignment

Interpolation supports the same format specifiers as string.Format:

Example.cs
var price = 42.5m;
var date = DateTime.Now;

Console.WriteLine($"Price: {price:C2}");        // Price: £42.50
Console.WriteLine($"Date: {date:yyyy-MM-dd}");   // Date: 2025-06-16
Console.WriteLine($"{"Name",-20}{"Score",10}");   // Left and right alignment

These format specifiers are passed directly to the AppendFormatted overload that accepts a ReadOnlySpan<char> format string, maintaining the zero-boxing benefit.

String.Create for Maximum Control

For scenarios where you know the exact length of the output string, string.Create avoids even the handler overhead:

Example.cs
// Create a string of known length, writing directly into its buffer
string result = string.Create(10, 42, (span, value) =>
{
    "Value: ".AsSpan().CopyTo(span);
    value.TryFormat(span[7..], out _);
});

This is a niche optimisation, but it demonstrates the performance-conscious design that pervades modern .NET string handling.

Practical Guidelines

For most code, simply use $"" interpolation and trust the compiler to generate efficient code. The handler pattern means you no longer need to choose between readable code and performant code.

Where performance matters:

The days of avoiding string interpolation for performance reasons are behind us. The compiler does the heavy lifting, and in many cases produces code that is faster than what you would write by hand.