A story about boxing / unboxing and string interpolation in C#

This article should shed some light on what string interpolation has to do with boxing and unboxing. Furthermore I want to demystify the performance aspect.

String-Interpolation

Imagine the following snippet:

int myNumber = 2;
var myString = $"My number is {myNumber}";

String interpolation allows you to create an "inline" string. The main advantage is, that it's super readable.

The feature itself is just syntactic sugar. That means one step in the compiler chain will lower this term to something understandable. And most of you think "Yeah of course. It will get lowered to string.Format" but that is not always the case.

And here boxing and unboxing comes into play. Boxing means converting a value to the type object. Unboxing is the other way around.

The compiler will do different things depending whether or not boxing is involved.

Let's have a look at this:

string World = "World";
int AgeInBillionYears = 4;
Console.WriteLine($"Hello {World}. You are {AgeInBillionYears} billion years old");

This term will become as suspected:

Console.WriteLine(string.Format("Hello {0}. You are {1} billion years old", World, AgeInBillionYears));

Let's have a look into example two:

Console.WriteLine($"Hello {World}. You are {AgeInBillionYears.ToString()} years old"); 

This term will become

string[] array = new string[5];
array[0] = "Hello ";
array[1] = World;
array[2] = ". You are ";
array[3] = AgeInBillionYears.ToString();
array[4] = " billion years old";
Console.WriteLine(string.Concat(array));

We can see that the compiler uses string.Concat. Why? Well it's quite easy. In our first example we have an integer and in our second example we have a string and if you look at both method definitions it should become clear what happens.

string.Format(string, object) and string.Concat(string[]).

We see that Concat will not use any boxing at all. As the compiler know nows that everything is from type string, it can use the faster version.

Everytime Boxing/Unboxing is involved in string-interpolation the compiler will use string.Format. If you want to play around and want to see it in action head over to Sharplab.io

Let's have a look at performance.

I created a benchmark, check the code here:

public class StringBuilderBenchmark
{
    [Params(2, 10, 50, 100)] public int ConcatSize { get; set; }

    [Benchmark]
    public string ConcatViaStringBuilder()
    {
        var sb = new StringBuilder();
        for (var i = 0; i < ConcatSize; i++)
        {
            sb.Append(i);
        }

        return sb.ToString();
    }

    [Benchmark]
    public string ConcatViaConcatBoxed()
    {
        var output = string.Empty;
        for (var i = 0; i < ConcatSize; i++)
        {
            output += output + i;
        }

        return output;
    }
    
    [Benchmark]
    public string ConcatViaConcatUnboxed()
    {
        var output = string.Empty;
        for (var i = 0; i < ConcatSize; i++)
        {
            output += output + i.ToString();
        }

        return output;
    }
    
    [Benchmark]
    public string ConcatViaStringFormatBoxed()
    {
        var output = string.Empty;
        for (var i = 0; i < ConcatSize; i++)
        {
            output = $"{output}{i}";
        }

        return output;
    }
    
    [Benchmark]
    public string ConcatViaStringFormatUnboxed()
    {
        var output = string.Empty;
        for (var i = 0; i < ConcatSize; i++)
        {
            output = $"{output}{i.ToString()}";
        }

        return output;
    }
}

We have mutliple scenarios: string.Format, string.Concat and a stringbuilder. We will always concatenate them. But once with and once without boxing/unboxing. Plus we'll check for only 2 string and 10 string, 50 and 100.

Method ConcatSize Mean Error StdDev Median
ConcatViaStringBuilder 2 45.09 ns 1.168 ns 3.407 ns 44.31 ns
ConcatViaConcatBoxed 2 36.14 ns 0.339 ns 0.265 ns 36.15 ns
ConcatViaConcatUnboxed 2 36.60 ns 0.380 ns 0.481 ns 36.57 ns
ConcatViaStringFormatBoxed 2 185.35 ns 4.655 ns 13.356 ns 181.92 ns
ConcatViaStringFormatUnboxed 2 30.29 ns 0.668 ns 0.957 ns 30.28 ns
ConcatViaStringBuilder 10 155.17 ns 4.753 ns 13.790 ns 152.19 ns
ConcatViaConcatBoxed 10 541.45 ns 17.286 ns 49.319 ns 534.95 ns
ConcatViaConcatUnboxed 10 553.73 ns 21.764 ns 61.386 ns 538.91 ns
ConcatViaStringFormatBoxed 10 938.73 ns 18.525 ns 31.955 ns 931.03 ns
ConcatViaStringFormatUnboxed 10 208.84 ns 3.827 ns 6.180 ns 208.31 ns
ConcatViaStringBuilder 50 811.73 ns 13.508 ns 12.635 ns 809.94 ns
ConcatViaConcatBoxed 50 NA NA NA NA
ConcatViaConcatUnboxed 50 NA NA NA NA
ConcatViaStringFormatBoxed 50 5,012.96 ns 99.171 ns 139.024 ns 4,979.66 ns
ConcatViaStringFormatUnboxed 50 1,596.57 ns 30.620 ns 28.642 ns 1,602.53 ns
ConcatViaStringBuilder 100 1,479.16 ns 28.212 ns 27.708 ns 1,476.77 ns
ConcatViaConcatBoxed 100 NA NA NA NA
ConcatViaConcatUnboxed 100 NA NA NA NA
ConcatViaStringFormatBoxed 100 10,787.23 ns 155.462 ns 263.986 ns 10,723.98 ns
ConcatViaStringFormatUnboxed 100 4,382.53 ns 208.454 ns 591.349 ns 4,174.07 ns

What we can see is, that StringBuilder makes no sense for a small amount of strings. Please keep in my that we are talking about nanoseconds. But we can see that boxing and unboxing can play a major role in the performance of string concatenation.

1
An error has occurred. This application may no longer respond until reloaded. Reload x