|
|
|
|
|
C#連接字符串,我們通常簡(jiǎn)單地用加號(hào)“+”來實(shí)現(xiàn),這當(dāng)然是C#連接字符串的一種方法。不過,C#連接字符串的方法有很多種,本文將介紹C#連接字符串的6種方法,并通過實(shí)例比較它們性能的優(yōu)劣,比較它們速度的快慢。
我想現(xiàn)在每個(gè)人都知道使用 “+” 來連接大字符串(據(jù)說)是不行的。但這讓我開始思考性能影響到底是什么?如果你有兩個(gè)要連接的字符串,是否值得啟動(dòng)一個(gè) StringBuilder
實(shí)例?為此,我想做一些基準(zhǔn)測(cè)試,比較一下C#連接字符串的各種方法的性能。
我使用帶有 .NET Core SDK的 AMD Ryzen CPU作為我的運(yùn)行時(shí)。詳細(xì)信息在這里:
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
初始基準(zhǔn)
我將使用 BenchmarkDotNet 來完成我的基準(zhǔn)測(cè)試,BenchmarkDotNet可幫助你將方法轉(zhuǎn)化為基準(zhǔn)、跟蹤其性能的測(cè)量實(shí)驗(yàn)。你不需要是一位經(jīng)驗(yàn)豐富的性能工程師,你可以使用簡(jiǎn)單的 API 以聲明式的方式設(shè)計(jì)非常復(fù)雜的性能實(shí)驗(yàn)。參閱文章:
對(duì)于我的基準(zhǔn)測(cè)試,我將嘗試執(zhí)行“單行連接”。我所說的“單行連接”的意思是我說了 5 個(gè)變量,我想將它們?nèi)窟B接成一個(gè)長(zhǎng)字符串,它們之間有一個(gè)空格。我不是在循環(huán)內(nèi)執(zhí)行此操作,而且我手頭有所有 5 個(gè)變量。
我的基準(zhǔn)看起來像這樣:
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();
}
}
結(jié)果在這里:
方法 | 平均 | 錯(cuò)誤 | 偏差 |
---|---|---|---|
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
明顯領(lǐng)先。String.Format
最慢。這里發(fā)生了什么?我們將不得不深入挖掘幕后發(fā)生的事情。
String.Format
為什么 String.Format
這么慢?原來,String.Format
也在幕后使用 StringBuilder
,但它歸結(jié)為一個(gè)名為“AppendFormatHelper
”的方法:
https://github.com/microsoft/referencesource/blob/master/mscorlib/system/text/stringbuilder.cs#L1322
現(xiàn)在這有點(diǎn)說得通了,因?yàn)槟惚仨氂涀。?code>String.Format 可以做這樣的事情:
String.Format("Price : {0:C2}", 14.00M); //打印 $14.00 (貨幣格式)
因此,它必須做更多的工作來嘗試格式化字符串,同時(shí)考慮到正確格式化貨幣等問題,即使檢查這些格式類型也需要一點(diǎn)額外的時(shí)間。
String.Join
String.Join
是一個(gè)有趣的代碼,因?yàn)樵谖铱磥砟缓蟠a并沒有太大意義。如果你傳入一個(gè) IEnumerable
或一個(gè)對(duì)象的參數(shù)列表,那么它只使用一個(gè) StringBuilder
而不會(huì)做太多其他事情:
https://github.com/microsoft/referencesource/blob/master/mscorlib/system/string.cs#L161
但是,如果你傳入字符串參數(shù),它會(huì)使用一個(gè)字符數(shù)組并執(zhí)行一些非常低級(jí)的操作:
https://github.com/microsoft/referencesource/blob/master/mscorlib/system/string.cs#L204
所以我馬上想到……有區(qū)別嗎?那么這個(gè)基準(zhǔn):
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);
}
}
結(jié)果
方法 | 平均 | 錯(cuò)誤 | 偏差 |
---|---|---|---|
StringJoin | 80.32 ns | 0.730 ns | 0.683 ns |
StringJoinList | 141.16 ns | 1.109 ns | 1.038 ns |
巨大差距。事實(shí)上,它要慢得多。
String.Concat
Concat
與 Join
非常相似。例如,如果我們傳入一個(gè) IEnumerable
,它會(huì)使用一個(gè) StringBuilder
:
https://github.com/microsoft/referencesource/blob/master/mscorlib/system/string.cs#L3145
但是如果我們傳入字符串的參數(shù)列表,它就會(huì)下降到 ConcatArray
方法:
https://github.com/microsoft/referencesource/blob/master/mscorlib/system/string.cs#L3292
你可能會(huì)開始注意到很多方法都調(diào)用了“FastAllocateString
”,從用法而非我所擁有的特殊知識(shí)推斷,這似乎為字符串的完整大小分配了內(nèi)存,然后在稍后“填充”。例如,給定一個(gè)字符串列表,你已經(jīng)提前知道該字符串的大小,因此你可以預(yù)先分配該內(nèi)存,然后在稍后簡(jiǎn)單地填充字節(jié)。
加號(hào)(+) 運(yùn)算符
加號(hào)運(yùn)算符連接字符串,它的性能如何?我寫了一個(gè)小基準(zhǔn)測(cè)試:
[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;
}
}
每個(gè)字符串連接都在它自己的行上,理論上它應(yīng)該每次都創(chuàng)建一個(gè)新字符串。
結(jié)果:
方法 | 平均 | 錯(cuò)誤 | 偏差 |
---|---|---|---|
PlusOperatorWithResult | 106.52 ns | 0.560 ns | 0.497 ns |
PlusOperator | 95.10 ns | 1.818 ns | 1.701 ns |
顯然,隨著時(shí)間的推移,隨著更大的字符串和更多的連接,這可能會(huì)變得更成問題,我認(rèn)為這是人們?cè)诩饨?ldquo;一切都使用 StringBuilder
!”時(shí)試圖指出的。
另請(qǐng)注意,我將 MemoryDiagnoser
添加到該基準(zhǔn)測(cè)試中以表明是的,當(dāng)您使用 +=
運(yùn)算符進(jìn)行處理時(shí)會(huì)分配更多內(nèi)存,因?yàn)樗仨氃趦?nèi)存中創(chuàng)建一個(gè)全新的字符串來處理此問題。
StringBuilder
StringBuilder
的源代碼可以在這里找到:
https://github.com/microsoft/referencesource/blob/master/mscorlib/system/text/stringbuilder.cs
它相對(duì)簡(jiǎn)單,因?yàn)樗钟幸粋€(gè) char
數(shù)組直到最后一刻,然后在最后將所有內(nèi)容連接起來。它如此之快的原因是因?yàn)樵谡嬲枰澳悴粫?huì)分配字符串。
使用 StringBuilder
最讓我感到驚訝的是,即使追加 5 個(gè)(或者如果我們計(jì)算空格我猜更多),它也比僅使用 +
運(yùn)算符快得多。
在前文,我展示了 String
如何比 StringBuilder
消耗更多的資源,參閱文章:
Interpolation(插補(bǔ))
我找不到 C# 中插值的源代碼。事實(shí)上,我甚至不確定要搜索什么。因?yàn)樗愃朴诩犹?hào)運(yùn)算符,所以我假設(shè)它可能只是同一段代碼,在代碼深處連接字符串。
總結(jié)
那么,這會(huì)告訴我們什么?通過這些基準(zhǔn)測(cè)試,我們知道了 StringBuilder
是構(gòu)建字符串的最佳實(shí)踐,我們還發(fā)現(xiàn),即使在構(gòu)建較小的字符串時(shí),StringBuilder
也能完成其余的工作。這是否意味著立即重寫你的代碼以在所有地方使用 StringBuilder
?我個(gè)人對(duì)此表示懷疑。僅基于可讀性,幾納秒對(duì)你來說可能不值得。
我們還可看到,即使我們沒有進(jìn)行任何特殊格式化,String.Format
的性能也非常差。事實(shí)上,我們可以使用任何其他方法將字符串連接在一起并使其更快。
最后,我們還發(fā)現(xiàn)字符串連接仍然是一個(gè)奇怪的事情,使用 String.Concat
和 String.Join
之類的東西,根據(jù)你傳入的內(nèi)容做不同的事情。10 次中有 9 次你可能甚至不認(rèn)為傳入 IEnumerable
和 Params
之間存在差異,但確實(shí)存在差異。
相關(guān)文章