使用插值与“+”运算符连接字符串的内存使用情况
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.Format
和 String.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);
}
}
我看到了使用内插字符串在可读性方面的好处:
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.Format
和 String.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);
}
}