harveyAJ
harveyAJ

Reputation: 917

Memory usage of concatenating strings using interpolated vs "+" operator

I see the benefit of using interpolated strings, in terms of readability:

string myString = $"Hello { person.FirstName } { person.LastName }!"

over a concatenation done this way:

string myString = "Hello " + person.FirstName + " " person.LastName + "!";

The author of this video tutorial claims that the first one makes better use of memory.

How come?

Upvotes: 43

Views: 50807

Answers (5)

Slipoch
Slipoch

Reputation: 785

I created a memory test program, I had a bug in one of the benchmarks earlier on so I have fixed that and I have posted the source below the results. A note, this is using C# 7 if you use .,net core you will be using a different version of C# and these results will change.

Further to the immutable arguments above, the allocation is at the point of assignation. so the var output = "something"+"something else"+" "+"something other" contains 2 assignations, the variable assign on the left and the final string on the right (as it is optimised this way by the compiler when a fixed number of vars is used).

As shown below, these assignations happen every time you use this method (string.format and stringbuilder differ here, format uses less memory and builder has extra overhead initially).

Simple

So if you are simply adding vars into a single string yes Interp and Inline Concat use the same amount of RAM, string.format uses the least RAM though so there is obviously some extra allocations occurring with concat & interp that string format avoids.

Using the 1 var and assigning to it multiple times

Interestingly, in the multiline assigns (where you assign the same value to the var multiple times) even with 3 clears and appendformats added to the stringbuilder it is the most efficient in the multiline assigns and is still faster in CPU time than format, easiest on cpu is interp and concat, however the memory is nearing 1MB.

Appending to the var

When constructing a string over successive lines (appending separately in the builtbylines tests as you may for error code messages) String format slips behind the others when using += to append to the output var. Stringbuilder in this instance is the clear winner.

Multiple runs of the functions Here we can see the difference in a very simple 20x run of the line concat that could be found in a function if you wanted to track progress or the part of the function you are attempting to do. The difference between using a builder vs a string is nearly 25%. If you had even a small amount of strings assigned inside a loop of lots of records then the potential memory impact could be quite high by using interp/+=. For example if I were importing a relatively small file of records into a database and was using strings then the number of records could easily exceed 50000 in a very short period of time (let alone the 4gb compressed files I used to have to import), which means the system as a whole could easily crash or end up very slow as it is forced the GC repeatedly within a very short period of time. In those cases I would se a stringbuilder ref and simply refresh and re-assign that OR use a Span.

Results

Source code

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Diagnostics.Windows.Configs;
using BenchmarkDotNet.Running;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    [AsciiDocExporter]
    [MemoryDiagnoser]
    public class Program
    {
        private string str1 = "test string";
        private string str2 = "this is another string";
        private string str3 = "helo string 3";
        private string str4 = "a fourth string";
                
        [Benchmark]
        public void TestStringConcatStringsConst()
        {
            var output = str1 + " " + str2 + " " + str3 + " " + str4;
        }


        [Benchmark]
        public void TestStringInterp()
        {
            var output = $"{str1} {str2} {str3} {str4}";
        }

        [Benchmark]
        public void TestStringFormat()
        {            
            var output = String.Format("{0} {1} {2} {3}", str1, str2, str3, str4);
        }

        [Benchmark]
        public void TestStringBuilder()
        {
            var output = new StringBuilder().AppendFormat("{0} {1} {2} {3}", str1, str2, str3, str4);
        }

        [Benchmark]
        public void TestStringConcatStrings_FourMultiLineAssigns()
        {
            var output = str1 + " " + str2 + " " + str3 + " " + str4;
            output = str1 + " " + str2 + " " + str3 + " " + str4;
            output = str1 + " " + str2 + " " + str3 + " " + str4;
            output = str1 + " " + str2 + " " + str3 + " " + str4;
        }

        [Benchmark]
        public void TestStringInterp_FourMultiLineAssigns()
        {
            var output = $"{str1} {str2} {str3} {str4}";
            output = $"{str1} {str2} {str3} {str4}";
            output = $"{str1} {str2} {str3} {str4}";
            output = $"{str1} {str2} {str3} {str4}";
        }

        [Benchmark]
        public void TestStringFormat_FourMultiLineAssigns()
        {
            var output = String.Format("{0} {1} {2} {3}", str1, str2, str3, str4);
            output = String.Format("{0} {1} {2} {3}", str1, str2, str3, str4);
            output = String.Format("{0} {1} {2} {3}", str1, str2, str3, str4);
            output = String.Format("{0} {1} {2} {3}", str1, str2, str3, str4);
        }

        [Benchmark]
        //This also clears and re-assigns the data, I used the stringbuilder until the last line as if you are doing multiple assigns with stringbuilder you do not pull out a string until you need it.
        public void TestStringBuilder_FourMultilineAssigns()
        {
            var output = new StringBuilder().AppendFormat("{0} {1} {2} {3}", str1, str2, str3, str4);
            output = output.Clear().AppendFormat("{0} {1} {2} {3}", str1, str2, str3, str4);
            output = output.Clear().AppendFormat("{0} {1} {2} {3}", str1, str2, str3, str4);
            output = output.Clear().AppendFormat("{0} {1} {2} {3}", str1, str2, str3, str4);
        }

        [Benchmark]
        public void TestStringConcat_BuiltByLine()
        {
            var output = str1;
            output += " " + str2;
            output += " " + str3;
            output += " " + str4;
        }

        [Benchmark]
        public void TestStringInterp_BuiltByLine1()
        {
            var output = str1;
            output = $"{output} {str2}";
            output = $"{output} {str3}";
            output = $"{output} {str4}";
        }

        [Benchmark]
        public void TestStringInterp_BuiltByLine2()
        {
            var output = str1;
            output += $" {str2}";
            output += $" {str3}";
            output += $" {str4}";
        }

        [Benchmark]
        public void TestStringFormat_BuiltByLine1()
        {
            var output = str1;
            output = String.Format("{0} {1}", output, str2);
            output = String.Format("{0} {1}", output, str3);
            output = String.Format("{0} {1}", output, str4);
        }

        [Benchmark]
        public void TestStringFormat_BuiltByLine2()
        {
            var output = str1;
            output += String.Format(" {0}", str2);
            output += String.Format(" {0}", str3);
            output += String.Format(" {0}", str4);
        }

        [Benchmark]
        public void TestStringBuilder_BuiltByLine()
        {
            var output = new StringBuilder(str1);
            output.AppendFormat(" {0}", str2);
            output.AppendFormat(" {0}", str3);
            output.AppendFormat(" {0}", str4);
        }

        [Benchmark]
        public void TestConcatLine20x()
        {
            for (int i = 0; i < 20; i++) {
                TestStringConcat_BuiltByLine();
            }
        }

        [Benchmark]
        public void TestInterpLine20x()
        {
            for (int i = 0; i < 20; i++)
            {
                TestStringInterp_BuiltByLine2();
            }
        }

        [Benchmark]
        public void TestBuilderLine20x()
        {
            for (int i = 0; i < 20; i++)
            {
                TestStringBuilder_BuiltByLine();
            }
        }

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

Upvotes: 4

Georg
Georg

Reputation: 2096

I made a simple test, see below. If you concatenate constants, don't use "string.Concat" because the compiler can't concatenate your strings at compile time. If you use variables, the results are effectively the same.

time measure results:

const string interpolation : 4
const string concatenation : 58
const string addition      : 3
var string interpolation   : 53
var string concatenation   : 55
var string addition        : 55
mixed string interpolation : 47
mixed string concatenation : 53
mixed string addition      : 42

the code:

void Main()
{

const int repetitions = 1000000; 
const string part1 = "Part 1"; 
const string part2 = "Part 2"; 
const string part3 = "Part 3"; 
var vPart1 = GetPart(1); 
var vPart2 = GetPart(2); 
var vPart3 = GetPart(3); 

Test("const string interpolation ", () => $"{part1}{part2}{part3}"); 
Test("const string concatenation ", () => string.Concat(part1, part2, part3)); 
Test("const string addition      ", () => part1 + part2 + part3); 
Test("var string interpolation   ", () => $"{vPart1}{vPart2}{vPart3}"); 
Test("var string concatenation   ", () => string.Concat(vPart1, vPart2, vPart3)); 
Test("var string addition        ", () => vPart1 + vPart2 + vPart3); 
Test("mixed string interpolation ", () => $"{vPart1}{part2}{part3}");
Test("mixed string concatenation ", () => string.Concat(vPart1, part2, part3));
Test("mixed string addition      ", () => vPart1 + part2 + part3);

void Test(string info, Func<string> action) 
{ 
    var watch = Stopwatch.StartNew(); 
    for (var i = 0; i < repetitions; i++) 
    { 
        action(); 
    } 
    watch.Stop(); 
    Trace.WriteLine($"{info}: {watch.ElapsedMilliseconds}"); 
} 

string GetPart(int index) 
    => $"Part{index}"; 

}

Upvotes: 24

Usman
Usman

Reputation: 4703

Because strings in c# are immutable that's why same memory is used again and again so it does not impact memory much but in terms of performance you are actually differentiating between String.Format and String.Concat because at compile time your code will be like this

  string a = "abc";
  string b = "def";

  string.Format("Hello {0} {1}!", a, b);

  string.Concat(new string[] { "Hello ", a, " ", b, "!" });

there is a whole thread about performance between these two if you are interested String output: format or concat in C#

Upvotes: 2

Eric Burdo
Eric Burdo

Reputation: 814

Strings are immutable. That means they can't be changed.

When you concatenate strings with a + sign, you end up creating multiple strings to get to the final string.

When you use the interpolation method (or StringBuilder), the .NET runtime optimizes your string use, so it (in theory) has better memory usage.

All that being said, it often depends on WHAT you are doing, and HOW OFTEN you are doing it.

One set of concatenations doesn't offer a lot of performance/memory improvements.

Doing those concatenations in a loop can have a lot of improvement.

Upvotes: 11

Servy
Servy

Reputation: 203817

The author doesn't actually say that one makes better use of memory than the other. It says that the one method "makes good use of memory" in the abstract, which, by itself, doesn't really mean much of anything.

But regardless of what they said, the two methods aren't going to be meaningfully different in their implementation. Neither is going to be meaningfully different from the other in terms of memory or time.

Upvotes: 26

Related Questions