C#连接字符串6种方法速度比较:StringBuilder/Format/Join/Concat等
作者:admin 时间:2023-5-14 1:46:55 浏览:C#连接字符串,我们通常简单地用加号“+”来实现,这当然是C#连接字符串的一种方法。不过,C#连接字符串的方法有很多种,本文将介绍C#连接字符串的6种方法,并通过实例比较它们性能的优劣,比较它们速度的快慢。
- 使用加号“+”,包括“+=”
- String.Concat
- String.Join
- StringBuilder
- String.Format
- 使用字符串插值(例如 $”My string {variable}”)。
我想现在每个人都知道使用 “+” 来连接大字符串(据说)是不行的。但这让我开始思考性能影响到底是什么?如果你有两个要连接的字符串,是否值得启动一个 StringBuilder
实例?为此,我想做一些基准测试,比较一下C#连接字符串的各种方法的性能。
我使用带有 .NET Core SDK的 AMD Ryzen CPU作为我的运行时。详细信息在这里:
BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
AMD Ryzen 7 2700X, 1 CPU, 16 logical and 8 physical cores
.NET Core SDK=3.1.100
[Host] : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
DefaultJob : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
初始基准
我将使用 BenchmarkDotNet 来完成我的基准测试,BenchmarkDotNet可帮助你将方法转化为基准、跟踪其性能的测量实验。你不需要是一位经验丰富的性能工程师,你可以使用简单的 API 以声明式的方式设计非常复杂的性能实验。参阅文章:
对于我的基准测试,我将尝试执行“单行连接”。我所说的“单行连接”的意思是我说了 5 个变量,我想将它们全部连接成一个长字符串,它们之间有一个空格。我不是在循环内执行此操作,而且我手头有所有 5 个变量。
我的基准看起来像这样:
public class SingleLineJoin
{
public string string1 = "a";
public string string2 = "b";
public string string3 = "c";
public string string4 = "d";
public string string5 = "e";
[Benchmark]
public string Interpolation()
{
return $"{string1} {string2} {string3} {string4} {string5}";
}
[Benchmark]
public string PlusOperator()
{
return string1 + " " + string2 + " " + string3 + " " + string4 + " " + string5;
}
[Benchmark]
public string StringConcatenate()
{
return string.Concat(string1, " ", string2, " ", string3, " ", string4, " ", string5);
}
[Benchmark]
public string StringJoin()
{
return string.Join(" ", string1, string2, string3, string4, string5);
}
[Benchmark]
public string StringFormat()
{
return string.Format("{0} {1} {2} {3} {4}", string1, string2, string3, string4, string5);
}
[Benchmark]
public string StringBuilderAppend()
{
StringBuilder builder = new StringBuilder();
builder.Append(string1);
builder.Append(" ");
builder.Append(string2);
builder.Append(" ");
builder.Append(string3);
builder.Append(" ");
builder.Append(string4);
builder.Append(" ");
builder.Append(string5);
return builder.ToString();
}
}
结果在这里:
方法 | 平均 | 错误 | 偏差 |
---|---|---|---|
Interpolation | 98.58 ns | 1.310 ns | 1.225 ns |
PlusOperator | 98.35 ns | 0.729 ns | 0.646 ns |
StringConcatenate | 94.65 ns | 0.929 ns | 0.869 ns |
StringJoin | 78.52 ns | 0.846 ns | 0.750 ns |
StringFormat | 233.67 ns | 3.262 ns | 2.892 ns |
StringBuilderAppend | 51.13 ns | 0.237 ns | 0.210 ns |
可以看到,Interpolation
、PlusOperator
和 Concat
大致相同。String.Join
速度很快,StringBuilder
明显领先。String.Format
最慢。这里发生了什么?我们将不得不深入挖掘幕后发生的事情。
String.Format
为什么 String.Format
这么慢?原来,String.Format
也在幕后使用 StringBuilder
,但它归结为一个名为“AppendFormatHelper
”的方法:
https://github.com/microsoft/referencesource/blob/master/mscorlib/system/text/stringbuilder.cs#L1322
现在这有点说得通了,因为你必须记住,String.Format
可以做这样的事情:
String.Format("Price : {0:C2}", 14.00M); //打印 $14.00 (货币格式)
因此,它必须做更多的工作来尝试格式化字符串,同时考虑到正确格式化货币等问题,即使检查这些格式类型也需要一点额外的时间。
String.Join
String.Join
是一个有趣的代码,因为在我看来幕后代码并没有太大意义。如果你传入一个 IEnumerable
或一个对象的参数列表,那么它只使用一个 StringBuilder
而不会做太多其他事情:
https://github.com/microsoft/referencesource/blob/master/mscorlib/system/string.cs#L161
但是,如果你传入字符串参数,它会使用一个字符数组并执行一些非常低级的操作:
https://github.com/microsoft/referencesource/blob/master/mscorlib/system/string.cs#L204
所以我马上想到……有区别吗?那么这个基准:
public class StringJoinComparison
{
public string string1 = "a";
public string string2 = "b";
public string string3 = "c";
public string string4 = "d";
public string string5 = "e";
public List<string> stringList;
[GlobalSetup]
public void Setup()
{
stringList = new List<string> { string1, string2, string3, string4, string5 };
}
[Benchmark]
public string StringJoin()
{
return string.Join(" ", string1, string2, string3, string4, string5);
}
[Benchmark]
public string StringJoinList()
{
return string.Join(" ", stringList);
}
}
结果
方法 | 平均 | 错误 | 偏差 |
---|---|---|---|
StringJoin | 80.32 ns | 0.730 ns | 0.683 ns |
StringJoinList | 141.16 ns | 1.109 ns | 1.038 ns |
巨大差距。事实上,它要慢得多。
String.Concat
Concat
与 Join
非常相似。例如,如果我们传入一个 IEnumerable
,它会使用一个 StringBuilder
:
https://github.com/microsoft/referencesource/blob/master/mscorlib/system/string.cs#L3145
但是如果我们传入字符串的参数列表,它就会下降到 ConcatArray
方法:
https://github.com/microsoft/referencesource/blob/master/mscorlib/system/string.cs#L3292
你可能会开始注意到很多方法都调用了“FastAllocateString
”,从用法而非我所拥有的特殊知识推断,这似乎为字符串的完整大小分配了内存,然后在稍后“填充”。例如,给定一个字符串列表,你已经提前知道该字符串的大小,因此你可以预先分配该内存,然后在稍后简单地填充字节。
加号(+) 运算符
加号运算符连接字符串,它的性能如何?我写了一个小基准测试:
[MemoryDiagnoser]
public class OperatorTest
{
public string string1 = "a";
public string string2 = "b";
public string string3 = "c";
public string string4 = "d";
public string string5 = "e";
[Benchmark]
public string PlusOperatorWithResult()
{
var result = string1 + " ";
result += string2 + " ";
result += string3 + " ";
result += string4 + " ";
result += string5 + " ";
return result;
}
[Benchmark]
public string PlusOperator()
{
var result = string1 + " " + string2 + " " + string3 + " " + string4 + " " + string5;
return result;
}
}
每个字符串连接都在它自己的行上,理论上它应该每次都创建一个新字符串。
结果:
方法 | 平均 | 错误 | 偏差 |
---|---|---|---|
PlusOperatorWithResult | 106.52 ns | 0.560 ns | 0.497 ns |
PlusOperator | 95.10 ns | 1.818 ns | 1.701 ns |
显然,随着时间的推移,随着更大的字符串和更多的连接,这可能会变得更成问题,我认为这是人们在尖叫“一切都使用 StringBuilder
!”时试图指出的。
另请注意,我将 MemoryDiagnoser
添加到该基准测试中以表明是的,当您使用 +=
运算符进行处理时会分配更多内存,因为它必须在内存中创建一个全新的字符串来处理此问题。
StringBuilder
StringBuilder
的源代码可以在这里找到:
https://github.com/microsoft/referencesource/blob/master/mscorlib/system/text/stringbuilder.cs
它相对简单,因为它持有一个 char
数组直到最后一刻,然后在最后将所有内容连接起来。它如此之快的原因是因为在真正需要它之前你不会分配字符串。
使用 StringBuilder
最让我感到惊讶的是,即使追加 5 个(或者如果我们计算空格我猜更多),它也比仅使用 +
运算符快得多。
在前文,我展示了 String
如何比 StringBuilder
消耗更多的资源,参阅文章:
Interpolation(插补)
我找不到 C# 中插值的源代码。事实上,我甚至不确定要搜索什么。因为它类似于加号运算符,所以我假设它可能只是同一段代码,在代码深处连接字符串。
总结
那么,这会告诉我们什么?通过这些基准测试,我们知道了 StringBuilder
是构建字符串的最佳实践,我们还发现,即使在构建较小的字符串时,StringBuilder
也能完成其余的工作。这是否意味着立即重写你的代码以在所有地方使用 StringBuilder
?我个人对此表示怀疑。仅基于可读性,几纳秒对你来说可能不值得。
我们还可看到,即使我们没有进行任何特殊格式化,String.Format
的性能也非常差。事实上,我们可以使用任何其他方法将字符串连接在一起并使其更快。
最后,我们还发现字符串连接仍然是一个奇怪的事情,使用 String.Concat
和 String.Join
之类的东西,根据你传入的内容做不同的事情。10 次中有 9 次你可能甚至不认为传入 IEnumerable
和 Params
之间存在差异,但确实存在差异。
相关文章
- 站长推荐