本文始發于:https://www.cnblogs.com/wildmelon/p/16180980.html
一、參考資料
- .Net源代碼,https://referencesource.microsoft.com/#mscorlib/system/string.cs
- 字串和文本,https://docs.unity.cn/cn/current/Manual/BestPracticeUnderstandingPerformanceInUnity5.html
- Concatenating Strings Efficiently,https://jonskeet.uk/csharp/stringbuilder.html
二、序數比對
根據 官方檔案優化建議 中提到的:
在與字串相關的代碼中經常出現的核心性能問題之一,是無意間使用了緩慢的默認字串 API,這些 API 是為商業應用程式構建的,可根據與文本字符有關的多種不同區域性和語言規則來處理字串,
這里提到的區域性,即指與 CultureInfo 有關的處理,在 string.cs 中,大約有以下方法與 CultureInfo 有關:
- Equals
- Compare
- StartsWith
- EndsWith
- ToUpper
- ToLower
除了 ToUpper 和 ToLower 之外,都是與字串對比有關的,絕大多數情況下我們不需要認定字符'e'與'?'之類的字符是相同的,所以需要改用帶 StringComparison 形參的方法,切換到 StringComparison.Ordinal,直接按序數進行比對,否則可能產生數十到上百倍的性能差距:
// 耗時對比
public void TestString1() {
System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
int loopCount = 1000000;
string strA = "test1";
string strB = "test2";
stopwatch.Start();
for (int i = 0; i < loopCount; i++) {
string.Compare(strA, strB);
//strA.Equals(strB, StringComparison.CurrentCulture);
}
stopwatch.Stop();
Debug.Log("Compare method 1: " + stopwatch.Elapsed.TotalMilliseconds);
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < loopCount; i++) {
string.CompareOrdinal(strA, strB);
//strA.Equals(strB, StringComparison.Ordinal);
}
stopwatch.Stop();
Debug.Log("Compare method 2: " + stopwatch.Elapsed.TotalMilliseconds);
}
// 某次不嚴謹的測驗結果:
// Compare method 1: 574.2263
// Compare method 2: 7.5011
不過與 Unity檔案講的不同,Equals 介面似乎默認就是使用的 StringComparison.Ordinal 序數比對,
另外類似 a.StartsWith("b", System.StringComparison.Ordinal) 本身效率已經足夠,似乎也沒有必要自己重新實作一個 CustomStartsWith,有可能會忽略掉某些提前退出的情況,反而使得性能下降,
三、字串拼接
(一)加號運算子
基本上在 StringBuilder 的相關教程上,都能看到類似如下的示例:
public string TestString2() {
// 會產生多個臨時物件,產生 GC
string str = "";
str = str + "a";
str = str + "b";
str = str + "c";
return str;
}
使用 ILSpy 工具打開編譯之后的 dll 檔案,可見 C# 原始碼被編譯為以下 IL 代碼,可以看到 string 的加號運算子,實際上呼叫的是 string.Concat 方法:
.method public hidebysig
instance string TestString2 () cil managed
{
IL_0000: ldstr ""
IL_0005: ldstr "a"
IL_000a: call string [mscorlib]System.String::Concat(string, string)
IL_000f: ldstr "b"
IL_0014: call string [mscorlib]System.String::Concat(string, string)
IL_0019: ldstr "c"
IL_001e: call string [mscorlib]System.String::Concat(string, string)
IL_0023: ret
} // end of metho
閱讀 Concat 方法的原始碼,會發現底層是在統計所需長度后,呼叫 FastAllocateString 一次性分配記憶體,然后 FillStringChecked 對源字串進行拷貝,實際上是一個非常效率的介面:
[System.Security.SecuritySafeCritical] // auto-generated
private static String ConcatArray(String[] values, int totalLength) {
String result = FastAllocateString(totalLength);
int currPos=0;
for (int i=0; i<values.Length; i++) {
Contract.Assert((currPos <= totalLength - values[i].Length),
"[String.ConcatArray](currPos <= totalLength - values[i].Length)");
FillStringChecked(result, currPos, values[i]);
currPos+=values[i].Length;
}
return result;
}
(二)拼接優化
那么對于剛才的示例,是否有必要改成如下代碼呢:
StringBuilder builder = new StringBuilder();
builder.Append ("a");
builder.Append ("b");
builder.Append ("c");
string result = builder.ToString();
對于已知數量的字串拼接,其實直接改成 string str = "a"+"b"+"c"; 即可,單行的連續加號運算會編譯成 String Concat(params String[] values) 方法呼叫,既保持可讀性又不會生成多個中間字串,
嚴格來講上述說明不太準確,string str = "a"+"b"+"c"; 生成的 IL 代碼如下所示:
.method public hidebysig
instance string TestString2 () cil managed
{
.maxstack 8
IL_0000: ldstr "abc"
IL_0005: ret
}
可見如果代碼里用的是常量運算式,在編譯時就能確認字串內容的話,會進行優化直接跳過拼接的呼叫,但使用 StringBuilder 的話則是無法提前確認進行優化的,
(三)使用總結
StringBuilder 和 string.Concat 效率是相近的,
對于預先組織好的、固定數量的字串拼接(能直接確認總長度),string.Concat 效率稍高一些,可讀性也更高,
對于不確定數量的或者需要快取中間結果的字串拼接,則使用 StringBuilder,
順帶一提,String.Format 是效率更低的字串介面,看似只用了一個額外字串,實際上會逐字符遍歷決議占位符,如果只是用作拼接目的,先考慮是否能用其他介面替代,
四、C# 0GC 字串方案
StringBuilder 和 Concat,每次呼叫都會分配新的記憶體來保存新字串,在專案中如果存在大量的字串拼接,就會導致頻繁的 GC Alloc,
可以考慮使用 https://github.com/871041532/zstring
參考以下文章說明:
- Unity中的string gc優化,https://www.cnblogs.com/zhaoqingqing/p/13928469.html
- ZString — Zero Allocation StringBuilder for .NET Core and Unity.
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/462917.html
標籤:其他
上一篇:Redis主從同步
下一篇:Unity制作一個小星球
