使用StringBuilderRemove方法比在循环中创建新的StringBuilder更节省内存
本文关键字:StringBuilder 内存 节省 创建 方法 StringBuilderRemove 循环 使用 | 更新日期: 2023-09-27 17:48:48
在C#中,选项#1和选项#2哪个内存效率更高?
public void TestStringBuilder()
{
//potentially a collection with several hundred items:
string[] outputStrings = new string[] { "test1", "test2", "test3" };
//Option #1
StringBuilder formattedOutput = new StringBuilder();
foreach (string outputString in outputStrings)
{
formattedOutput.Append("prefix ");
formattedOutput.Append(outputString);
formattedOutput.Append(" postfix");
string output = formattedOutput.ToString();
ExistingOutputMethodThatOnlyTakesAString(output);
//Clear existing string to make ready for next iteration:
formattedOutput.Remove(0, output.Length);
}
//Option #2
foreach (string outputString in outputStrings)
{
StringBuilder formattedOutputInsideALoop = new StringBuilder();
formattedOutputInsideALoop.Append("prefix ");
formattedOutputInsideALoop.Append(outputString);
formattedOutputInsideALoop.Append(" postfix");
ExistingOutputMethodThatOnlyTakesAString(
formattedOutputInsideALoop.ToString());
}
}
private void ExistingOutputMethodThatOnlyTakesAString(string output)
{
//This method actually writes out to a file.
System.Console.WriteLine(output);
}
有几个答案温和地建议我摆脱困境,自己解决问题,下面是我的结果。我认为这种情绪通常违背了这个网站的初衷,但如果你想把事情做好,你也可以……:)
我修改了选项#1,以利用@Ty的建议使用StringBuilder.Length=0而不是Remove方法。这使得两个选项的代码更加相似。现在的两个区别是StringBuilder的构造函数是在循环中还是在循环外,选项#1现在使用Length方法来清除StringBuilder。这两个选项都设置为在包含100000个元素的outputString数组上运行,以使垃圾收集器完成一些工作。
几个答案提供了查看各种PerfMon计数器的提示&并使用结果来选择一个选项。我做了一些研究,最终使用了我工作中使用的Visual Studio团队系统开发人员版的内置性能资源管理器。我发现了一个多部分系列的第二个博客条目,它解释了如何在这里设置它。基本上,你连接一个单元测试,指向你想要评测的代码;通过向导&一些配置;并启动单元测试评测。我启用了.NET对象分配&寿命指标。分析的结果很难格式化,所以我把它们放在最后。如果你将文本复制并粘贴到Excel中,并对其进行一些处理,它们就会可读。
选项#1是内存效率最高的,因为它使垃圾收集器所做的工作比选项#2少,并且它为StringBuilder对象分配了一半的内存和实例。对于日常编码,选择选项#2是非常好的。
如果你还在读,我问这个问题是因为选项#2会让体验C/C++开发人员的内存泄漏检测器变得非常强大。如果StringBuilder实例在重新分配之前没有释放,则会发生巨大的内存泄漏。当然,我们C#开发人员并不担心这样的事情(直到他们跳起来咬我们)。感谢大家!!
ClassName Instances TotalBytesAllocated Gen0_InstancesCollected Gen0BytesCollected Gen1InstancesCollected Gen1BytesCollected
=======Option #1
System.Text.StringBuilder 100,001 2,000,020 100,016 2,000,320 2 40
System.String 301,020 32,587,168 201,147 11,165,268 3 246
System.Char[] 200,000 8,977,780 200,022 8,979,678 2 90
System.String[] 1 400,016 26 1,512 0 0
System.Int32 100,000 1,200,000 100,061 1,200,732 2 24
System.Object[] 100,000 2,000,000 100,070 2,004,092 2 40
======Option #2
System.Text.StringBuilder 200,000 4,000,000 200,011 4,000,220 4 80
System.String 401,018 37,587,036 301,127 16,164,318 3 214
System.Char[] 200,000 9,377,780 200,024 9,379,768 0 0
System.String[] 1 400,016 20 1,208 0 0
System.Int32 100,000 1,200,000 100,051 1,200,612 1 12
System.Object[] 100,000 2,000,000 100,058 2,003,004 1 20
选项2实际上应该(我相信)优于选项1。调用Remove
的行为"强制"StringBuilder获取它已经返回的字符串的副本。字符串在StringBuilder中实际上是可变的,除非需要,否则StringBuilder不会复制。对于选项1,它在基本清除数组之前进行复制-对于选项2,不需要复制。
选项2的唯一缺点是,如果字符串最终很长,则在追加时会有多个副本,而选项1则保持缓冲区的原始大小。但是,如果是这种情况,请指定初始容量以避免额外的复制。(在您的示例代码中,字符串最终将比默认的16个字符大——用32的容量初始化它将减少所需的额外字符串。)
然而,除了性能之外,选项2更干净。
在进行评测时,您也可以尝试在进入循环时将StringBuilder的长度设置为零。
formattedOutput.Length = 0;
我们之前在Java中讨论过这一点,下面是C#版本的[发布]结果
Option #1 (10000000 iterations): 11264ms
Option #2 (10000000 iterations): 12779ms
更新:在我的非科学分析中,允许这两种方法在监视perfmon中的所有内存性能计数器的同时执行,并没有导致与任何一种方法有任何明显的差异(除了只有在执行任何一个测试时才有一些计数器尖峰)。
这是我过去测试的:
class Program
{
const int __iterations = 10000000;
static void Main(string[] args)
{
TestStringBuilder();
Console.ReadLine();
}
public static void TestStringBuilder()
{
//potentially a collection with several hundred items:
var outputStrings = new [] { "test1", "test2", "test3" };
var stopWatch = new Stopwatch();
//Option #1
stopWatch.Start();
var formattedOutput = new StringBuilder();
for (var i = 0; i < __iterations; i++)
{
foreach (var outputString in outputStrings)
{
formattedOutput.Append("prefix ");
formattedOutput.Append(outputString);
formattedOutput.Append(" postfix");
var output = formattedOutput.ToString();
ExistingOutputMethodThatOnlyTakesAString(output);
//Clear existing string to make ready for next iteration:
formattedOutput.Remove(0, output.Length);
}
}
stopWatch.Stop();
Console.WriteLine("Option #1 ({1} iterations): {0}ms", stopWatch.ElapsedMilliseconds, __iterations);
Console.ReadLine();
stopWatch.Reset();
//Option #2
stopWatch.Start();
for (var i = 0; i < __iterations; i++)
{
foreach (var outputString in outputStrings)
{
StringBuilder formattedOutputInsideALoop = new StringBuilder();
formattedOutputInsideALoop.Append("prefix ");
formattedOutputInsideALoop.Append(outputString);
formattedOutputInsideALoop.Append(" postfix");
ExistingOutputMethodThatOnlyTakesAString(
formattedOutputInsideALoop.ToString());
}
}
stopWatch.Stop();
Console.WriteLine("Option #2 ({1} iterations): {0}ms", stopWatch.ElapsedMilliseconds, __iterations);
}
private static void ExistingOutputMethodThatOnlyTakesAString(string s)
{
// do nothing
}
}
在这种情况下,选项1稍微快一点,尽管选项2更容易阅读和维护。除非你碰巧连续执行这个操作数百万次,否则我会坚持选项2,因为我怀疑选项1和2在单个迭代中运行时大致相同。
由于您只关心内存,我建议:
foreach (string outputString in outputStrings)
{
string output = "prefix " + outputString + " postfix";
ExistingOutputMethodThatOnlyTakesAString(output)
}
名为output的变量与原始实现中的大小相同,但不需要其他对象。StringBuilder在内部使用字符串和其他对象,您将创建许多需要GC’d的对象。
选项1:中的两行
string output = formattedOutput.ToString();
选项2的行:
ExistingOutputMethodThatOnlyTakesAString(
formattedOutputInsideALoop.ToString());
将创建一个不可变的对象,其值为前缀+outputString+后缀。无论你如何创建这个字符串,它的大小都是一样的。你真正想问的是哪个更高效:
StringBuilder formattedOutput = new StringBuilder();
// create new string builder
或
formattedOutput.Remove(0, output.Length);
// reuse existing string builder
完全跳过StringBuilder将比以上任何一种都更节省内存。
如果您真的需要知道这两者中哪一个在应用程序中更高效(这可能会根据列表、前缀和outputString的大小而有所不同),我建议使用红门ANTS Profilerhttp://www.red-gate.com/products/ants_profiler/index.htm
Jason
我不想这么说,但测试一下怎么样?
这些东西很容易自己找到。运行Perfmon.exe并为.NET Memory+Gen 0 Collections添加计数器。运行测试代码一百万次。您将看到选项#1所需的集合数量是选项#2所需集合数量的一半。
如果选项#2更直接的话。就性能而言,这听起来像是你只需要测试和观察的东西。我想,选择不那么简单的选项并没有带来足够的区别。
我认为选项1的内存效率会稍高,因为不是每次都创建新对象。话虽如此,GC在清理资源方面做得很好,就像选项2中一样。
我认为你可能陷入了过早优化的陷阱(万恶之源——克努思)。您的IO将比字符串生成器占用更多的资源。
我倾向于选择更清晰/更干净的选项,在本例中为选项2。
Rob
- 测量它
- 尽可能地预先分配您认为需要的内存
- 如果你喜欢速度,那么考虑一种相当直接的多线程前到中、中到端并行方法(根据需要扩大分工)
- 再次测量
对你来说什么更重要?
-
存储器
-
速度
-
清晰度