写点什么

如何提高 C# StringBuilder 的性能

作者:编程宝库
  • 2021 年 11 月 18 日
  • 本文字数:3448 字

    阅读完需:约 11 分钟

本文探讨使用 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. 编程宝库

用户头像

编程宝库

关注

技术控 创业者 2019.08.26 加入

编程宝库站长,创业公司技术合伙人

评论

发布
暂无评论
如何提高C# StringBuilder的性能