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.