提高C# StringBuilder操作性能優化的方法

本文探討使用C# StringBuilder 的最佳實踐,用於減少內存分配,提高字符串操作的性能。

在 .NET 中,String 對象是不可改變的。每次使用 System.String 類中的方法之一時,都要在內存中創建一個新的字符串對象,這就需要為該新對象分配新的空間。

在需要對字符串執行重復修改的情況下,與創建新的 String 對象相關的系統開銷可能會非常昂貴。如果要修改字符串而不創建新的對象,則可以使用 System.Text.StringBuilder 類。例如,當在一個循環中將許多字符串連接在一起時,使用 StringBuilder 類可以提升性能。

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 軟件包管理器控制臺執行以下命令來完成。

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 模式下編譯你的項目,並在命令行使用以下命令運行基準測試。

下面說明瞭兩種方法的性能差異。

正如你所看到的,使用 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文件。

ASP.NET提高StringBuilder類操作性能就向你介紹到這裡,希望對你有所幫助。

參考資料:

1. C#教程

2. C#編程技術

3. 編程寶庫

推薦閱讀: