如何提升C# StringBuilder的性能

2021年11月25日 阅读数:2
这篇文章主要向大家介绍如何提升C# StringBuilder的性能,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

本文探讨使用C# StringBuilder 的最佳实践,用于减小内存分配,提升字符串操做的性能。html

在 .NET 中,字符串是不可变的类型。每当你在 .NET 中修改一个字符串对象时,就会在内存中建立一个新的字符串对象来保存新的数据。相比之下,StringBuilder 对象表明了一个可变的字符串,并随着字符串大小的增加动态地扩展其内存分配。编程

String 和 StringBuilder 类是你在 .NET Framework 和 .NET Core 中处理字符串时常常使用的两个流行类。然而,每一个类都有其优势和缺点。缓存

BenchmarkDotNet 是一个轻量级的开源库,用于对 .NET 代码进行基准测试。BenchmarkDotNet 能够将你的方法转化为基准,跟踪这些方法,而后提供对捕获的性能数据的洞察力。在这篇文章中,咱们将利用 BenchmarkDotNet 为咱们的 StringBuilder 操做进行基准测试。markdown

要使用本文提供的代码示例,你的系统中应该安装有 Visual Studio 2019 或者以上版本。ide

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 来大大减小分配的成本。ui

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. 编程宝库