8

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

10 回答 10

7

一些答案温和地建议我摆脱困境并自己弄清楚,所以下面是我的结果。我认为这种情绪通常与本网站的宗旨背道而驰,但如果你想把事情做好,你不妨这样做...... :)

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

几个答案提供了查看各种 PerfMon 计数器等的提示,并使用结果来选择一个选项。我做了一些研究,最终使用了我在工作中使用的 Visual Studio Team Systems Developer 版本的内置性能资源管理器。我找到了一个多部分系列的第二个博客条目,它解释了如何在此处进行设置。基本上,你连接一个单元测试来指向你想要分析的代码;通过向导和一些配置;并启动单元测试分析。我启用了 .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
于 2008-11-07T21:48:21.627 回答
6

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

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

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

于 2008-11-05T22:16:09.153 回答
4

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

formattedOutput.Length = 0;
于 2008-11-05T23:49:55.780 回答
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 + 后缀的值创建一个不可变对象。无论您如何创建此字符串,它的大小都是相同的。您真正要问的是哪个内存效率更高:

    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

杰森

于 2008-11-05T23:41:56.593 回答
1

讨厌这么说,但只是测试它怎么样?

于 2008-11-05T22:19:28.613 回答
1

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

于 2008-11-05T22:27:52.140 回答
1

我们之前用 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
    }
} 

尽管选项 2 更易于阅读和维护,但此方案中的选项 1 稍微快一些。除非您碰巧连续数百万次执行此操作,否则我会坚持使用选项 2,因为我怀疑选项 1 和 2 在单次迭代中运行时大致相同。

于 2008-11-05T22:34:23.390 回答
0

如果肯定更直接,我会说选项#2。就性能而言,听起来像是您只需要测试和查看的东西。我猜想选择不太直接的选项并没有足够的区别。

于 2008-11-05T22:13:46.473 回答
0

我认为选项 1 会稍微提高内存效率,因为不是每次都创建一个新对象。话虽如此,GC 在清理资源方面做得非常好,就像在选项 2 中一样。

我认为您可能陷入过早优化的陷阱(万恶之源--Knuth)。您的 IO 将比字符串生成器占用更多的资源。

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

于 2008-11-05T22:31:32.867 回答
0
  1. 测量它
  2. 预分配尽可能接近您认为需要的内存量
  3. 如果速度是您的偏好,那么考虑一个相当直接的多线程从前到中、中到端的并发方法(根据需要扩展分工)
  4. 再次测量

什么对你更重要?

  1. 记忆

  2. 速度

  3. 明晰

于 2008-11-06T15:55:16.503 回答