代码之家  ›  专栏  ›  技术社区  ›  Sixto Saez

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

  •  8
  • Sixto Saez  · 技术社区  · 16 年前

    在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 回复  |  直到 16 年前
        1
  •  7
  •   Sixto Saez    16 年前

    几个答案温和地建议我放下包袱,自己弄清楚,所以下面是我的结果。我认为这种情绪通常与这个网站的精神背道而驰,但如果你想把事情做好,你最好……:)

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

    几个答案提供了查看各种PerfMon计数器的提示;并使用结果来选择一个选项。我做了一些研究,最终使用了我工作中使用的Visual Studio Team Systems Developer版本的内置性能资源管理器 here 基本上,你连接一个单元测试,指向你想要分析的代码;经历一个巫师&;一些配置;并启动单元测试分析。我启用了。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
  •  6
  •   Jon Skeet    14 年前

    选项2应该(我相信)实际上优于选项1。打电话的行为 Remove “强制”StringBuilder获取它已经返回的字符串的副本。字符串在StringBuilder中实际上是可变的,除非需要,否则StringBuilder不会进行复制。使用选项1,它会在基本清除数组之前进行复制,而使用选项2,则不需要复制。

    选项2的唯一缺点是,如果字符串最终变长,在追加时将制作多个副本,而选项1保持缓冲区的原始大小。然而,如果是这样的话,请指定一个初始容量以避免额外的复制。(在示例代码中,字符串最终将大于默认的16个字符——将其初始化为32个字符的容量将减少所需的额外字符串。)

    然而,除了性能之外,选项2更为简洁。

        3
  •  4
  •   Ty.    16 年前

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

    formattedOutput.Length = 0;
    
        4
  •  2
  •   bdukes Jon Skeet    16 年前

    we have的常用口语形式 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在单次迭代中运行时大致相同。

        5
  •  1
  •   Joel Lucsy    16 年前

    既然你只关心记忆,我建议:

    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。无论你如何创建这个字符串,它的大小都是一样的。你真正要问的是哪个更节省内存:

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

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

    完全跳过StringBuilder将比上述任何一种都更节省内存。

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

    杰森

        6
  •  1
  •   Hans Passant    16 年前

    我不想这么说,但测试一下怎么样?

        7
  •  1
  •   Community CDub    8 年前

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

        8
  •  0
  •   bdukes Jon Skeet    16 年前

    如果更直接的话,我会说选项2。就性能而言,听起来像是你只需要测试和观察的东西。我想,选择不那么直接的选项并没有太大的区别。

        9
  •  0
  •   Robert Wagner    16 年前

    我认为选项1会稍微多一点 记忆力 高效,因为新对象不是每次都创建的。话虽如此,GC在清理资源方面做得很好,就像选项2中一样。

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

    我倾向于选择更清晰/更干净的选项,在这种情况下是选项2。

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

    对你来说什么更重要?

    1. 记忆力

    2. 速度

    3. 清楚