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