2008-11-05 110 views
8

在C#中更高效的内存:选项#1或选项#2?正在使用StringBuilder删除方法比在循环中创建新的StringBuilder更有效率吗?

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

回答

7

若干问题的答案轻轻地认为,我得到了我的达夫,并计算出它自己,所以下面是我的结果。我认为这种情绪通常违背了这个网站的谷物,但如果你想做的东西正确的话,你不妨做...... :)

我修改了选项#1以利用@Ty建议使用StringBuilder .Length = 0而不是Remove方法。这使得两个选项的代码更相似。现在两个区别是StringBuilder的构造函数是否在循环中,选项#1现在使用Length方法来清除StringBuilder。这两个选项都设置为运行在具有100,000个元素的outputStrings数组上,以使垃圾回收器做一些工作。

几个答案提供了一些提示,以查看各种PerfMon计数器&等,并使用结果来选择一个选项。我做了一些研究,最终使用了我在工作的Visual Studio Team Systems Developer版本的内置性能浏览器。我发现多部分系列的第二篇博客文章解释了如何设置它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 
0

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

6

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

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

除了性能之外,选项2只是更干净。

+0

难道string.Concat( “前缀”,outputString, “后缀”)会比这两个选项快? – configurator 2009-03-05 21:21:06

+0

是的,在这种情况下 - 但我把这个问题当作一个给定的StringBuilder来使用(例如,在所有这些之前,在实际代码中都有一个附加循环)。 – 2009-03-05 23:17:41

+0

为什么要“拷贝”?我们所要做的就是清除字符串。 – 2009-08-25 09:36:41

1

讨厌这么说,但如何测试呢?

+0

关于如何配置内存的任何建议将是有帮助的。你知道.NET以外的商业ANTS产品的内存分析器吗? – 2008-11-05 22:27:00

1

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

0

我认为选项1会稍微更多内存有效,因为每次都不会创建新的对象。话虽如此,GC在清理资源方面做得相当不错,如选项2.

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

我倾向于去更清晰/清洁剂的选择,在这种情况下,选择2

罗布

1

我们talked about this before with 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在单次迭代中运行时大致相同。

2

因为您是使用内存只关注我建议:

foreach (string outputString in outputStrings) 
    {  
     string output = "prefix " + outputString + " postfix"; 
     ExistingOutputMethodThatOnlyTakesAString(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的完全将更多的内存比上述任何的高效。

如果你真的需要知道哪两个是更有效的在你的应用程序(这可能会有所不同根据您的名单,前缀和outputStrings的大小),我会建议红色门蚂蚁探查http://www.red-gate.com/products/ants_profiler/index.htm

Jason

4

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

formattedOutput.Length = 0; 
0
  1. 衡量
  2. 预分配尽可能接近你多少内存认为你需要
  3. 如果速度是你的偏好,然后再考虑一个相当简单的多线程前线到中间,中间结束并行的方法(扩大分工的要求)
  4. 措施再次

什么哟更重要ü?

  1. 内存

  2. 速度

  3. 清晰度

相关问题