使用StringBuilder Remove方法比在循环中创建新的StringBuilder更有效吗?

时间:2008-11-05 22:11:05

标签: c# memory-leaks garbage-collection stringbuilder

在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);
}

10 个答案:

答案 0 :(得分:7)

有几个答案温和地暗示我自己离开并自己弄明白,所以下面是我的结果。我认为这种情绪通常与这个网站的内容相悖,但如果你想做正确的事,你也可以做....:)

我修改了选项#1以利用@Ty建议使用StringBuilder.Length = 0而不是Remove方法。这使得两个选项的代码更相似。现在两个不同之处在于StringBuilder的构造函数是在循环中还是在循环之外,而选项#1现在使用Length方法来清除StringBuilder。这两个选项都设置为在包含100,000个元素的outputStrings数组上运行,以使垃圾收集器能够正常工作。

有几个答案提供了一些提示,可以看到各种PerfMon计数器&这样并使用结果来选择一个选项。我做了一些研究,最后使用了我工作的Visual Studio Team Systems Developer版本的内置Performance Explorer。我发现了多部分系列的第二篇博客文章,解释了如何设置它here。基本上,您连接单元测试以指向您要分析的代码;通过向导&一些配置;并启动单元测试分析。我启用了.NET对象分配&终身指标。分析的结果很难为这个答案格式化,所以我把它们放在最后。如果您将文本复制并粘贴到Excel中并稍微按摩它们,它们将是可读的。

选项#1的内存效率最高,因为它使垃圾收集器的工作量减少了一半,并且它将一半的内存和实例分配给StringBuilder对象而不是选项#2。对于日常编码,选择#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

答案 1 :(得分:6)

选项2应该(我相信)实际上胜过选项1.调用Remove的行为“强制”StringBuilder获取它已经返回的字符串的副本。该字符串在StringBuilder中实际上是可变的,并且除非需要,否则StringBuilder不会获取副本。使用选项1,它会在基本清除阵列之前复制 - 使用选项2不需要复制。

选项2的唯一缺点是,如果字符串最终变长,则会在附加时生成多个副本 - 而选项1保留缓冲区的原始大小。但是,如果情况确实如此,请指定初始容量以避免额外复制。 (在您的示例代码中,字符串最终将大于默认的16个字符 - 使用容量为32来初始化它将减少所需的额外字符串。)

除了表现之外,选项2更简洁。

答案 2 :(得分:4)

在进行分析时,您还可以尝试在进入循环时将StringBuilder的长度设置为零。

formattedOutput.Length = 0;

答案 3 :(得分:2)

由于你只关心记忆,我建议:

foreach (string outputString in outputStrings)
    {    
        string output = "prefix " + outputString + " postfix";
        ExistingOutputMethodThatOnlyTakesAString(output)  
    }

名为output的变量与原始实现中的大小相同,但不需要其他对象。 StringBuilder在内部使用字符串和其他对象,您将创建许多需要GC的对象。

选项1中的两行:

string output = formattedOutput.ToString();

选项2中的一行:

ExistingOutputMethodThatOnlyTakesAString(
           formattedOutputInsideALoop.ToString());

将使用前缀+ outputString + postfix的值创建 immutable 对象。无论您如何创建它,此字符串都是相同的大小。你真正要问的是哪种内存效率更高:

    StringBuilder formattedOutput = new StringBuilder(); 
    // create new string builder

    formattedOutput.Remove(0, output.Length); 
    // reuse existing string builder

完全跳过StringBuilder将比上述任何一个更有效。

如果你真的需要知道哪两个在你的应用程序中效率更高(这可能会根据你的列表,前缀和outputStrings的大小而有所不同)我会推荐红门ANTS Profiler http://www.red-gate.com/products/ants_profiler/index.htm

杰森

答案 4 :(得分:1)

讨厌说出来,但是如何测试呢?

答案 5 :(得分:1)

这些东西很容易被你自己发现。运行Perfmon.exe并为.NET Memory + Gen 0集合添加计数器。运行测试代码一百万次。您会看到选项#1需要一半的集合选项#2需要。

答案 6 :(得分:1)

我们talked about this before with Java,这是C#版本的[Release]结果:

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大致相同。

答案 7 :(得分:0)

如果绝对更直接,我会说选项#2。在性能方面,听起来像你需要测试和看到的东西。我猜它没有足够的差别来选择不太直接的选项。

答案 8 :(得分:0)

我认为选项1会稍微更强内存,因为每次都不会创建新对象。话虽如此,GC在清理资源方面做得非常好,如选项2所示。

我认为你可能陷入了过早优化的陷阱(the root of all evil --Knuth)。您的IO将占用比字符串构建器更多的资源。

我倾向于使用更清晰/更清晰的选项,在这种情况下选项2。

罗布

答案 9 :(得分:0)

  1. 测量它
  2. 尽可能接近预分配您认为需要的内存
  3. 如果速度是您的偏好,那么考虑一个相当直接的多线程前端到中间,中间到终端并发方法(根据需要扩展分工)
  4. 再次测量
  5. 什么对你更重要?

    1. 存储器

    2. 速度

    3. 清晰度