如何提高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. 編程寶庫