176

Is there a noticeable performance difference between using string interpolation:

myString += $"{x:x2}";

vs String.Format()?

myString += String.Format("{0:x2}", x);

I am only asking because ReSharper is prompting the fix, and I have been fooled before.

Pang
  • 9,564
  • 146
  • 81
  • 122
Krythic
  • 4,184
  • 5
  • 26
  • 67
  • Something can help:[Performance: string concatenation vs String.Format vs interpolated string](https://www.meziantou.net/performance-string-concatenation-vs-string-format-vs-interpolated-string.htm) and [Interpolated strings: advanced usages](https://www.meziantou.net/interpolated-strings-advanced-usages.htm) – marbel82 Aug 23 '21 at 13:48

7 Answers7

77

Noticable is relative. However: string interpolation is turned into string.Format() at compile-time so they should end up with the same result.

There are subtle differences though: as we can tell from this question, string concatenation in the format specifier results in an additional string.Concat() call.

Community
  • 1
  • 1
Jeroen Vannevel
  • 43,651
  • 22
  • 107
  • 170
  • 6
    Actually, string interpolation could compile into string concatenation in some cases (e.g. when a `int` is used). `var a = "hello"; var b = $"{a} world";` compiles to string concatenation. `var a = "hello"; var b = $"{a} world {1}";` compiles to string format. – Omar Muscatello Nov 23 '18 at 17:18
56

The answer is both yes and no. ReSharper is fooling you by not showing a third variant, which is also the most performant. The two listed variants produce equal IL code, but the following will indeed give a boost:

myString += $"{x.ToString("x2")}";

Full test code

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Diagnosers;
using BenchmarkDotNet.Diagnostics.Windows;
using BenchmarkDotNet.Running;

namespace StringFormatPerformanceTest
{
    [Config(typeof(Config))]
    public class StringTests
    {
        private class Config : ManualConfig
        {
            public Config() => AddDiagnoser(MemoryDiagnoser.Default, new EtwProfiler());
        }

        [Params(42, 1337)]
        public int Data;

        [Benchmark] public string Format() => string.Format("{0:x2}", Data);
        [Benchmark] public string Interpolate() => $"{Data:x2}";
        [Benchmark] public string InterpolateExplicit() => $"{Data.ToString("x2")}";
    }

    class Program
    {
        static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<StringTests>();
        }
    }
}

Test results

|              Method | Data |      Mean |  Gen 0 | Allocated |
|-------------------- |----- |----------:|-------:|----------:|
|              Format |   42 | 118.03 ns | 0.0178 |      56 B |
|         Interpolate |   42 | 118.36 ns | 0.0178 |      56 B |
| InterpolateExplicit |   42 |  37.01 ns | 0.0102 |      32 B |
|              Format | 1337 | 117.46 ns | 0.0176 |      56 B |
|         Interpolate | 1337 | 113.86 ns | 0.0178 |      56 B |
| InterpolateExplicit | 1337 |  38.73 ns | 0.0102 |      32 B |

New test results (.NET 6)

Re-ran the test on .NET 6.0.9.41905, X64 RyuJIT AVX2.

|              Method | Data |      Mean |   Gen0 | Allocated |
|-------------------- |----- |----------:|-------:|----------:|
|              Format |   42 |  37.47 ns | 0.0089 |      56 B |
|         Interpolate |   42 |  57.61 ns | 0.0050 |      32 B |
| InterpolateExplicit |   42 |  11.46 ns | 0.0051 |      32 B |
|              Format | 1337 |  39.49 ns | 0.0089 |      56 B |
|         Interpolate | 1337 |  59.98 ns | 0.0050 |      32 B |
| InterpolateExplicit | 1337 |  12.85 ns | 0.0051 |      32 B |

The InterpolateExplicit() method is faster since we now explicitly tell the compiler to use a string. No need to box the object to be formatted. Boxing is indeed very costly. Also note that NET 6 reduced both CPU and memory allocations - for all methods.

New test results (.NET 7)

Re-ran the test on .NET 7.0.122.56804, X64 RyuJIT AVX2.

|              Method | Data |      Mean |   Gen0 | Allocated |
|-------------------- |----- |----------:|-------:|----------:|
|              Format |   42 |  41.04 ns | 0.0089 |      56 B |
|         Interpolate |   42 |  65.82 ns | 0.0050 |      32 B |
| InterpolateExplicit |   42 |  12.19 ns | 0.0051 |      32 B |
|              Format | 1337 |  41.02 ns | 0.0089 |      56 B |
|         Interpolate | 1337 |  59.61 ns | 0.0050 |      32 B |
| InterpolateExplicit | 1337 |  13.28 ns | 0.0051 |      32 B |

No significant changes since .NET 6.

l33t
  • 18,692
  • 16
  • 103
  • 180
  • 2
    The *third* variant will crash if `x` is `null`, though. – Pang Jan 03 '21 at 08:21
  • 1
    Why using interpolation in this case, instead of just `myString += x.ToString("x2");`? – Alexandre May 12 '21 at 15:42
  • Obviously, you wouldn't. For illustrative purposes, I chose to have very simple string formatting. – l33t May 13 '21 at 09:12
  • @Pang, we could change it to `$"{Data?.ToString("x2")}`. It might cost a few nanoseconds, but allocations/boxing will not be affected. – l33t May 13 '21 at 09:13
  • 2
    Look at the decompiled [IL code sharplab.io/...](https://sharplab.io/#v2:EYLgtghglgdgPgAQMwAIECYUGMCwAoAb3xRLVVgBcUARCCiAbn2NOTQEYAGFAMQHsATpAoAKAJQoAvAD4OnAHT8hdEQCICnEAA90AX1UAaGnQhimeUmTkoAkjAoBTAQAc+AGzoPxU2QBJ1tPTaeqrmlmwIXLb2Tq4ejgCiWs5uUFhQohIyKP4EgRDyACp8AMoUArAA5mo6qmL65rpAA=). Format and Interpolate have exactly same content. It uses the string.Format function. The string interpolation in InterpolateExplicit is redundant, it simple calls Data.ToString("x2")... and checks if null. Weird... Data.ToString("x2") can return null? – marbel82 Aug 19 '21 at 12:53
  • Yes, that's the whole idea with this interpolation syntax - it compiles into a `string.Format()` call. No, the interpolation is not redundant. Maybe add some padding to make it clearer? E.g. `$"foobar{Data?.ToString()}"`). The point is that having an explicit call to `ToString()` avoids **boxing**. Finally, *Data* could be null, so we should add that question mark (but it will cost a few nanoseconds). – l33t Aug 19 '21 at 20:35
  • Ok, I see your point, but... :) In that case, you should add `"foobar"` in your tests. Interesting... `$"foobar{Data?.ToString()}"` is compiled to `string.Concat("foobar", Data.ToString("x2"))` – marbel82 Aug 23 '21 at 12:25
  • 2
    I found interesting blog post @meziantou [Interpolated strings: advanced usages](https://www.meziantou.net/interpolated-strings-advanced-usages.htm) – marbel82 Aug 23 '21 at 13:39
  • Is this still true with the latest .NET 6 optimisations? – Haighstrom Sep 27 '22 at 13:58
  • @Haighstrom See update. Both yes and no. In `.NET 6` they managed to get rid of that extra allocation, but it seems the boxing still occurs (which is more or less expected). Writing out `ToString()` yields better performance for interpolated *value types*. – l33t Sep 28 '22 at 19:40
  • Sweet "1337" Result. This is the one I want. Somewhat offtopic, but I've been using a "FastConcat" method for numbers, if I remember correctly, it was faster. (THIS IS FOR LONGS, INTS, ETC). It goes something like this. -----> { if (b < 10U) return 10UL * a + b; if (b < 100U) return 100UL * a + b; if (b < 1000U) return 1000UL * a + b; if (b < 10000U) return 10000UL * a + b; ... etc – layer07 Nov 19 '22 at 10:05
  • Unfortunately, this is not the case when it comes to DateTime types. I don't the technicality but the ToString seems to perform better when it comes to DateTime types. – It's actually me Jul 25 '23 at 03:15
14

You should note that there have been significant optimizations on String Interpolation in C#10 and .NET 6 - String Interpolation in C# 10 and .NET 6.

I've been migrating all of my usage of not only string formatting, but also my usage of string concatenation, to use String Interpolation.

I'd be just as concerned, if not more, with memory allocation differences between the different methods. I've found that String Interpolation almost always wins for both speed and memory allocation when working with a smallish number of strings. If you have an undetermined (not known at design-time) number of strings, you should always use System.Text.StringBuilder.Append(xxx) or System.Text.StringBuilder.AppendFormat(xxx)

Also, I'd callout your usage of += for string concatenation. Be very careful and only do so for a small number of small strings.

Dave Black
  • 7,305
  • 2
  • 52
  • 41
5

String interpolation is turned into string.Format() at compile-time.

Also, with string.Format(), you can specify several outputs for a single argument, and different output formats for single a argument.

But string interpolation is more readable, I guess. So, it's up to you.

a = string.Format("Due date is {0:M/d/yy} at {0:h:mm}", someComplexObject.someObject.someProperty);

b = $"Due date is {someComplexObject.someObject.someProperty:M/d/yy} at {someComplexObject.someObject.someProperty:h:mm}";

There are some performance test results available:

https://koukia.ca/string-interpolation-vs-string-format-string-concat-and-string-builder-performance-benchmarks-c1dad38032a

J. Scott Elblein
  • 4,013
  • 15
  • 58
  • 94
Paul
  • 115
  • 1
  • 3
  • 3
    string interpolation is **just sometimes** turned into `String::Format`. and sometimes into `String::Concat`. And the performance-test on that page is imho not really meaningful: the amount of arguments you pass to each of those methods is dependent. concat is not always the fastest, stringbuilder is not always the slowest. – Matthias Burger Apr 09 '19 at 13:11
5

The question was about performance, however the title just says "vs", so I feel like having to add a few more points, some of them are opinionated though.

  • Localization

    • String interpolation cannot be localized due to its inline code nature. Before localization, it has to be turned into string.Format. However, there is tooling for that (e.g. ReSharper).
  • Maintainability (my opinion)

    • string.Format is far more readable, as it focuses on the sentence what I'd like to phrase, for example when constructing a nice and meaningful error message. Using the {N} placeholders give me more flexibility and it's easier to modify it later.
    • Also, the inlined format specifier in interpolation is easy to misread, and easy to delete together with the expression during a change.
    • When using complex and long expressions, interpolation quickly gets even more hard to read and maintain, so in this sense, it doesn't scale well when code is evolving and gets more complex. string.Format is much less prone to this.
    • At the end of the day, it's all about separation of concerns: I don't like to mix the how it should present with the what should be presented.

So based on these, I decided to stick with string.Format in most of my code. However, I've prepared an extension method to have a more fluent way of coding which I like much more. The extension's implementation is a one-liner, and it looks simply like this in use.

var myErrorMessage = "Value must be less than {0:0.00} for field {1}".FormatWith(maximum, fieldName);

Interpolation is a great feature, don't get me wrong. But IMO, it shines the best in those languages which miss the string.Format-like feature, for example JavaScript.

Pang
  • 9,564
  • 146
  • 81
  • 122
Zoltán Tamási
  • 12,249
  • 8
  • 65
  • 93
  • 6
    I'd disagree on maintainability; granted ReSharper makes it somewhat easier to match up the inserted values with their corresponding indices (and vice versa) but I think it's still more cognitive load to figure out if `{3}` is X or Y especially if you start rearranging your format. Madlibs example: `$"It was a {adjective} day in {month} when I {didSomething}"` vs `string.Format("It was a {0} day in {1} when I {2}", adjective, month, didSomething)` --> `$"I {didSomething} on a {adjective} {month} day"` vs `string.Format("I {2} on a {0} {1} day", adjective, month, didSomething)` – drzaus Jul 22 '20 at 19:50
  • 1
    @drzaus Thanks for sharing your thoughts. You have good points, however it's true only if we use only simple, well-named local variables. What I've seen quite many times is complex expressions, function calls, whatever put into interpolated string. With `string.Format` I think you are much less prone to this issue. But anyway, this is why I emphasized that it's my opinion :) – Zoltán Tamási Jul 23 '20 at 06:51
3

Maybe too late to mention but didn't found others mentioned it: I noticed the += operator in your question. Looks like you are creating some hex output of something executing this operation in cycle.

Using concat on strings (+=), especially in cycles, may result in hardly discoverable problem: an OutOfMemoryException while analysing the dump will show tons of free memory inside!

What happens?

  1. The memory management will look for a continuous space enough for the result string.
  2. The concatenated string written there.
  3. The space used for storing value for original left hand side variable freed.

Note that the space allocated in step #1 is certainly bigger than the space freed in step #3.

In the next cycle the same happens and so on. How our memory will look like assuming 10 bytes long string was added in each cycle to an originally 20 bytes long string 3 times?

[20 bytes free]X1[30 bytes free]X2[40 bytes free]X2[50 bytes allocated]

(Because almost sure there are other commands using memory during the cycle I placed the Xn-s to demonstrate their memory allocations. These may be freed or still allocated, follow me.)

If at the next allocation MM founds no enough big continuous memory (60 bytes) then it tries to get it from OS or by restructuring free spaces in its outlet. The X1 and X2 will be moved somewhere (if possible) and a 20+30+40 continuous block become available. Time taking but available.

BUT if the block sizes reach 88kb (google for it why 88kb) they will be allocated on Large Object Heap. Free blocks here wont be compacted anymore.

So if your string += operation results are going past this size (e.g. you are building a CSV file or rendering something in memory this way), the above cycle will result in chunks of free memory of continuously growing sizes, the sum of them can be gigabytes, but your app will terminate with OOM because it won't be able to allocate a block of maybe as small as 1Mb because none of the chunks are big enough for it :)

Sorry for long explanation, but it happened some years ago and it was a hard lesson. I am fighting against inappropriate use of string concats since then.

Pang
  • 9,564
  • 146
  • 81
  • 122
cly
  • 661
  • 3
  • 13
1

There is an important note about String.Format() on Microsoft's site:

https://learn.microsoft.com/en-us/dotnet/api/system.string.format?view=net-6.0#remarks

Instead of calling the String.Format method or using composite format strings, you can use interpolated strings if your language supports them. An interpolated string is a string that contains interpolated expressions. Each interpolated expression is resolved with the expression's value and included in the result string when the string is assigned.

J. Scott Elblein
  • 4,013
  • 15
  • 58
  • 94
ozan.yarci
  • 89
  • 1
  • 5