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

使用StringBuilder移除方法是否比在循环中创建新的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 回复  |  直到 14 年前
        1
  •  7
  •   Sixto Saez    16 年前

    有几个答案温和地建议我不要再胡思乱想了,下面是我的结果。我认为,这种情绪通常与本网站的内容背道而驰,但如果你想做对的事情,你不妨……:)

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

    一些答案提供了查看各种PerfMon计数器等的提示,并使用结果选择一个选项。我做了一些研究,最后使用了我工作时使用的Visual Studio TeamSystems开发人员版的内置性能资源管理器。我找到了一个多部分系列的第二个博客条目,解释了如何设置它。 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
    
        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 年前

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

    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+postfix值的对象。无论您如何创建,此字符串的大小都相同。你真正要问的是哪种记忆效率更高:

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

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

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

    如果您真的需要知道这两者中哪一个在您的应用程序中更有效(这可能会根据列表、前缀和输出字符串的大小而变化),我建议您使用Red Gate Ants Profiler http://www.red-gate.com/products/ants_profiler/index.htm

    杰森

        5
  •  1
  •   Joel Lucsy    16 年前

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

        6
  •  1
  •   Hans Passant    16 年前

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

        7
  •  1
  •   Community Keith    7 年前

    我们已经 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大致相同。

        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. 清晰