如何提高C# StringBuilder的性能
本文探討使用C# StringBuilder 的最佳實踐,用於減少記憶體分配,提高字元串操作的性能。
在 .NET 中,字元串是不可變的類型。每當你在 .NET 中修改一個字元串對象時,就會在記憶體中創建一個新的字元串對象來保存新的數據。相比之下,StringBuilder 對象代表了一個可變的字元串,並隨著字元串大小的增長動態地擴展其記憶體分配。
String 和 StringBuilder 類是你在 .NET Framework 和 .NET Core 中處理字元串時經常使用的兩個流行類。然而,每個類都有其優點和缺點。
BenchmarkDotNet 是一個輕量級的開源庫,用於對 .NET 程式碼進行基準測試。BenchmarkDotNet 可以將你的方法轉化為基準,跟蹤這些方法,然後提供對捕獲的性能數據的洞察力。在這篇文章中,我們將利用 BenchmarkDotNet 為我們的 StringBuilder 操作進行基準測試。
要使用本文提供的程式碼示例,你的系統中應該安裝有 Visual Studio 2019 或者以上版本。
1. 在Visual Studio中創建一個控制台應用程式項目
首先讓我們在 Visual Studio中 創建一個 .NET Core 控制台應用程式項目。假設你的系統中已經安裝了 Visual Studio 2019,請按照下面的步驟創建一個新的 .NET Core 控制台應用程式項目。
- 1. 啟動 Visual Studio IDE。
- 2. 點擊 “創建新項目”。
- 3. 在 “創建新項目 “窗口中,從顯示的模板列表中選擇 “控制台應用程式(.NET核心)”。
- 4. 點擊 “下一步”。
- 5. 在接下來顯示的 “配置你的新項目 “窗口中,指定新項目的名稱和位置。
- 6. 點擊創建。
這將在 Visual Studio 2019 中創建一個新的 .NET Core 控制台應用程式項目。我們將在本文的後續章節中使用這個項目來處理 StringBuilder。
2. 安裝 BenchmarkDotNet NuGet包
要使用 BenchmarkDotNet,你必須安裝 BenchmarkDotNet 軟體包。你可以通過 Visual Studio 2019 IDE 內的 NuGet 軟體包管理器,或在 NuGet 軟體包管理器控制台執行以下命令來完成。
Install-Package BenchmarkDotNet
3. 使用 StringBuilderCache 來減少分配
StringBuilderCache 是一個內部類,在 .NET 和 .NET Core 中可用。每當你需要創建多個 StringBuilder 的實例時,你可以使用 StringBuilderCache 來大大減少分配的成本。
StringBuilderCache 的工作原理是快取一個 StringBuilder 實例,然後在需要一個新的 StringBuilder 實例時重新使用它。這減少了分配,因為你只需要在記憶體中擁有一個 StringBuilder 實例。
讓我們用一些程式碼來說明這一點。在 Program.cs 文件中創建一個名為 StringBuilderBenchmarkDemo 的類。創建一個名為 AppendStringUsingStringBuilder 的方法,程式碼如下。
public string AppendStringUsingStringBuilder() { var stringBuilder = new StringBuilder(); stringBuilder.Append("First String"); stringBuilder.Append("Second String"); stringBuilder.Append("Third String"); return stringBuilder.ToString(); }
上面的程式碼片段顯示了如何使用 StringBuilder 對象來追加字元串。接下來創建一個名為 AppendStringUsingStringBuilderCache 的方法,程式碼如下。
public string AppendStringUsingStringBuilderCache() { var stringBuilder = StringBuilderCache.Acquire(); stringBuilder.Append("First String"); stringBuilder.Append("Second String"); stringBuilder.Append("Third String"); return StringBuilderCache.GetStringAndRelease(stringBuilder); }
上面的程式碼片段說明了如何使用 StringBuilderCache 類的 Acquire 方法創建一個 StringBuilder 實例,然後用它來追加字元串。
下面是 StringBuilderBenchmarkDemo 類的完整源程式碼供你參考。
[MemoryDiagnoser] public class StringBuilderBenchmarkDemo { [Benchmark] public string AppendStringUsingStringBuilder() { var stringBuilder = new StringBuilder(); stringBuilder.Append("First String"); stringBuilder.Append("Second String"); stringBuilder.Append("Third String"); return stringBuilder.ToString(); } [Benchmark] public string AppendStringUsingStringBuilderCache() { var stringBuilder = StringBuilderCache.Acquire(); stringBuilder.Append("First String"); stringBuilder.Append("Second String"); stringBuilder.Append("Third String"); return StringBuilderCache.GetStringAndRelease(stringBuilder); } }
你現在必須使用 BenchmarkRunner 類來指定初始起點。這是一種通知 BenchmarkDotNet 在指定的類上運行基準的方式。
用以下程式碼替換 Main 方法的默認源程式碼。
static void Main(string[] args) { var summary = BenchmarkRunner.Run<StringBuilderBenchmarkDemo>(); }
現在在 Release 模式下編譯你的項目,並在命令行使用以下命令運行基準測試。
dotnet run -p StringBuilderPerfDemo.csproj -c Release
下面說明了兩種方法的性能差異。
正如你所看到的,使用 StringBuilderCache 追加字元串要快得多,需要的分配也少。
4. 使用 StringBuilder.AppendJoin 而不是 String.Join
String 對象是不可變的,所以修改一個 String 對象需要創建一個新的 String 對象。因此,在連接字元串時,你應該使用 StringBuilder.AppendJoin 方法,而不是String.Join,以減少分配,提高性能。
下面的程式碼列表說明了如何使用 String.Join 和 StringBuilder.AppendJoin 方法來組裝一個長字元串。
[Benchmark] public string UsingStringJoin() { var list = new List < string > { "A", "B", "C", "D", "E" }; var stringBuilder = new StringBuilder(); for (int i = 0; i < 10000; i++) { stringBuilder.Append(string.Join(' ', list)); } return stringBuilder.ToString(); } [Benchmark] public string UsingAppendJoin() { var list = new List < string > { "A", "B", "C", "D", "E" }; var stringBuilder = new StringBuilder(); for (int i = 0; i < 10000; i++) { stringBuilder.AppendJoin(' ', list); } return stringBuilder.ToString(); }
下圖顯示了這兩種方法的基準測試結果。
請注意,對於這個操作,這兩種方法的速度很接近,但 StringBuilder.AppendJoin 使用的記憶體明顯較少。
5. 使用 StringBuilder 追加單個字元
注意,在使用 StringBuilder 時,如果需要追加單個字元,應該使用 Append(char) 而不是 Append(String)。
請考慮以下兩個方法。
[Benchmark] public string AppendStringUsingString() { var stringBuilder = new StringBuilder(); for (int i = 0; i < 1000; i++) { stringBuilder.Append("a"); stringBuilder.Append("b"); stringBuilder.Append("c"); } return stringBuilder.ToString(); } [Benchmark] public string AppendStringUsingChar() { var stringBuilder = new StringBuilder(); for (int i = 0; i < 1000; i++) { stringBuilder.Append('a'); stringBuilder.Append('b'); stringBuilder.Append('c'); } return stringBuilder.ToString(); }
從名字中就可以看出,AppendStringUsingString 方法說明了如何使用一個字元串作為 Append 方法的參數來追加字元串。
AppendStringUsingChar 方法說明了你如何在 Append 方法中使用字元來追加字元。
下圖顯示了這兩種方法的基準測試結果。
6. 其他 StringBuilder 優化方法
StringBuilder 允許你設置容量以提高性能。如果你知道你要創建的字元串的大小,你可以相應地設置初始容量以大大減少記憶體分配。
你還可以通過使用一個可重複使用的 StringBuilder 對象池來避免分配來提高 StringBuilder 的性能。
最後,請注意,由於 StringBuilderCache是一個內部類,你需要將源程式碼粘貼到你的項目中才能使用它。回顧一下,在C#中你只能在同一個程式集或庫中使用一個內部類。
因此,我們的程式文件不能僅僅通過引用 StringBuilderCache 所在的庫來訪問 StringBuilderCache 類。
這就是為什麼我們把 StringBuilderCache 類的源程式碼複製到我們的程式文件中,也就是Program.cs文件。
參考資料:
1. C#教程
2. C#編程技術
3. 編程寶庫