提高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 软件包管理器控制台执行以下命令来完成。
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. 编程宝库
ASP.NET提高StringBuilder类操作性能就向你介绍到这里,希望对你有所帮助。