使用插值与“+”运算符连接字符串的内存使用情况

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

我看到了使用内插字符串在可读性方面的好处:

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

通过这种方式完成的串联:

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

this video tutorial 的作者声称第一个可以更好地利用内存。

怎么会?

作者实际上并没有说一个比另一个更好地利用内存。它说摘要中的一种方法"makes good use of memory",就其本身而言,并没有什么实际意义。

但是不管他们怎么说,这两种方法在实现上不会有明显的不同。两者在内存或时间方面都不会与另一个有明显不同。

字符串是不可变的。这意味着它们无法更改。

当您使用 + 号连接字符串时,您最终会创建多个字符串以形成最终字符串。

当您使用插值方法(或 StringBuilder)时,.NET 运行时会优化您的字符串使用,因此它(理论上)具有更好的内存使用率。

综上所述,这通常取决于您在做什么,以及您做这件事的频率。

一组串联并没有提供很多 performance/memory 改进。

在循环中进行这些连接可以有很大的改进。

因为 c# 中的字符串是不可变的,所以它会一次又一次地使用相同的内存,所以它不会对内存产生太大影响,但就性能而言,您实际上是在 String.FormatString.Concat 之间进行区分,因为在编译时你的代码会像这样

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

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

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

如果您有兴趣,请参阅关于这两者之间性能的完整讨论帖String output: format or concat in C#

我做了一个简单的测试,见下文。如果您连接常量,请不要使用 "string.Concat",因为编译器无法在编译时连接您的字符串。如果使用变量,结果实际上是相同的。

时间测量结果:

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

代码:

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}"; 

}

我创建了一个内存测试程序,我之前在其中一个基准测试中遇到了一个错误,所以我已经修复了它,并且我已经在结果下方发布了源代码。请注意,如果您使用 .,net core,这是使用 C# 7,您将使用不同版本的 C#,这些结果将会改变。

除了上面的不可变参数之外,分配是在赋值点。所以 var output = "something"+"something else"+" "+"something other" 包含 2 个赋值,左边是变量赋值,右边是最终字符串(因为当使用固定数量的变量时,编译器会以这种方式优化它)。

如下所示,每次您使用此方法时都会发生这些分配(string.format 和 stringbuilder 在这里不同,格式使用较少的内存,而构建器有额外的开销)。

简单

因此,如果您只是将 vars 添加到单个字符串中,是的,Interp 和 Inline Concat 使用相同数量的 RAM,string.format 使用最少的 RAM,因此显然 concat 和 interp 会发生一些额外的分配该字符串格式避免了。

多次使用 1 变量

有趣的是,在多行赋值中 (您多次将相同的值赋给 var) 即使在 stringbuilder 中添加了 3 个清除和追加格式,它也是最有效的多行分配并且在 CPU 时间内仍然比格式快 2.5 倍。

附加到变量

在连续的行上构建字符串时(在builtbylines 测试中单独追加) 当使用+= 追加到输出变量时,字符串格式落后于其他格式。在这种情况下,Stringbuilder 无疑是赢家。

这是来源:

[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}", 1, 2, 3, 4);
    }

    [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}", 1, 2, 3, 4);
        output = String.Format("{0} {1} {2} {3}", 1, 2, 3, 4);
        output = String.Format("{0} {1} {2} {3}", 1, 2, 3, 4);
        output = String.Format("{0} {1} {2} {3}", 1, 2, 3, 4);
    }

    [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);
    }

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