使用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);
}

使用StringBuilderRemove方法比在循环中创建新的StringBuilder更节省内存

有几个答案温和地建议我摆脱困境,自己解决问题,下面是我的结果。我认为这种情绪通常违背了这个网站的初衷,但如果你想把事情做好,你也可以……:)

我修改了选项#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

  1. 测量它
  2. 尽可能地预先分配您认为需要的内存
  3. 如果你喜欢速度,那么考虑一种相当直接的多线程前到中、中到端并行方法(根据需要扩大分工)
  4. 再次测量

对你来说什么更重要?

  1. 存储器

  2. 速度

  3. 清晰度