C#中的字符串操作优化

时间:2008-11-11 23:12:49

标签: c# string optimization performance

以下C#代码需要5分钟才能运行:

int i = 1;
string fraction = "";
while (fraction.Length < 1000000)
{
    fraction += i.ToString();
    i++;
}

“优化它”会导致它在1.5秒内运行:

int i = 1;
string fraction = "";
while (fraction.Length < 1000000)
{
    // concatenating strings is much faster for small strings
    string tmp = "";
    for (int j = 0; j < 1000; j++)
    {
        tmp += i.ToString();
        i++;
    }
    fraction += tmp;
}

编辑:有些人建议使用StringBuilder,这也是一个很好的建议,这是0.06秒:

int i = 1;
StringBuilder fraction = new StringBuilder();
while (fraction.Length < 1000000)
{
    fraction.Append(i);
    i++;
}

找到j的最佳值是另一个时间的主题,但为什么这个非显而易见的优化工作得非常好呢?另外,在一个相关的主题上,我听说它不应该使用+运算符和字符串,而不是string.Format(),这是真的吗?

7 个答案:

答案 0 :(得分:9)

我根本没有得到你的结果。在我的盒子上,StringBuilder赢得了胜利。你能发布完整的测试程序吗?这是我的,有三个变体 - 你的字符串连接优化,“简单”的StringBuilder,以及具有初始容量的StringBuilder。我已经增加了限制,因为我的盒子上的速度太快,无法进行有效测量。

using System;
using System.Diagnostics;
using System.Text;

public class Test
{
    const int Limit = 4000000;

    static void Main()
    {
        Time(Concatenation, "Concat");
        Time(SimpleStringBuilder, "StringBuilder as in post");
        Time(SimpleStringBuilderNoToString, "StringBuilder calling Append(i)");
        Time(CapacityStringBuilder, "StringBuilder with appropriate capacity");
    }

    static void Time(Action action, string name)
    {
        Stopwatch sw = Stopwatch.StartNew();
        action();
        sw.Stop();
        Console.WriteLine("{0}: {1}ms", name, sw.ElapsedMilliseconds);
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }

    static void Concatenation()
    {
        int i = 1;
        string fraction = "";
        while (fraction.Length < Limit)
        {
            // concatenating strings is much faster for small strings
            string tmp = "";
            for (int j = 0; j < 1000; j++)
            {
                tmp += i.ToString();
                i++;
            }
            fraction += tmp;            
        }
    }

    static void SimpleStringBuilder()
    {
        int i = 1;
        StringBuilder fraction = new StringBuilder();
        while (fraction.Length < Limit)
        {
            fraction.Append(i.ToString());
            i++;
        }
    }

    static void SimpleStringBuilderNoToString()
    {
        int i = 1;
        StringBuilder fraction = new StringBuilder();
        while (fraction.Length < Limit)
        {
            fraction.Append(i);
            i++;
        }
    }

    static void CapacityStringBuilder()
    {
        int i = 1;
        StringBuilder fraction = new StringBuilder(Limit + 10);
        while (fraction.Length < Limit)
        {
            fraction.Append(i);
            i++;
        }
    }
}

结果:

Concat: 5879ms
StringBuilder as in post: 206ms
StringBuilder calling Append(i): 196ms
StringBuilder with appropriate capacity: 184ms

你的连接比第一个解决方案更快的原因很简单 - 你正在做几个“廉价”连接(每次复制相对较少的数据)和相对较少的“大”连接(整个字符串)至今)。在原文中,每一步都会复制到目前为止获得的所有数据,这显然更加昂贵。

答案 1 :(得分:8)

使用StringBuilder连接超过(大约)5个字符串(结果可能略有不同)。另外,给StringBuilder的构造函数一个关于预期最大大小的提示。

[更新]:只评论您对该问题的修改。如果您对连接字符串的最终大小有一个近似(或确切)的概念,也可以提高StringBuilder的性能,因为这会减少它必须执行的内存分配数量:

// e.g. Initialise to 10MB
StringBuilder fraction = new StringBuilder(10000000);

答案 2 :(得分:7)

您可能会看到前1000个字符几乎没有时间反对最后1000个字符。

我认为耗时的部分是每次添加一个对你的计算机来说很难的字符时,将大字符串实际复制到一个新的内存区域。

您可以轻松地将优化与您通常使用的流进行比较,使用缓冲区。较大的块通常会导致更好的性能,直到您达到不再有任何差异的临界大小,并且在处理少量数据时开始变得不利。

但是如果你从一开始就定义了一个具有适当大小的char数组,它可能会非常快速,因为它不会一遍又一遍地复制它。

答案 3 :(得分:3)

  

另外,在一个相关的主题上,我听说过你应该永远不要在字符串中使用+运算符,而不是使用string.Format(),这是真的吗?

不,像所有绝对陈述一样,这是无稽之谈。但是, 是正确的,使用Format通常会使格式代码更具可读性,并且通常比连接稍快一些 - 但速度不是决定因素。

至于你的代码......它会导致在连接中复制较小的字符串(即tmp)。当然,在fraction += tmp中你复制一个更大的字符串,但这种情况不常发生。

因此,您已将许多大型副本减少为几个大型和多个小型副本。

嗯,我刚刚注意到你的外环在两种情况下都有相同的大小。那么这应该不会更快。

答案 4 :(得分:3)

我现在无法进行测试,但尝试使用StringBuilder。

int i = 1;
    StringBuilder fraction = new StringBuilder();
    while (fraction.Length < 1000000)
    {
        fraction.Append(i);
        i++;
    }
return sb.ToString();

答案 5 :(得分:1)

回答修改过的任务(“为什么这个非显而易见的优化工作得那么好”和“你是不是应该在字符串上使用+运算符”):

我不确定你在谈论哪种非显而易见的优化。但我认为,第二个问题的答案涵盖了所有基础。

字符串在C#中的工作方式是它们被分配为固定长度,并且不能更改。这意味着每次尝试更改字符串的长度时,都会创建一个完整的新字符串,并将旧字符串复制到适当的长度。这显然是一个缓慢的过程。当您使用String.Format时,它在内部使用StringBuilder来创建字符串。

StringBuilders通过使用比固定长度字符串更智能地分配的内存缓冲区来工作,因此在大多数情况下表现更好。我不确定内部的StringBuilder的细节,所以你不得不问一个新的问题。我可以推测它不会重新分配字符串的旧部分(而是在内部创建链接列表,只在ToString需要时实际分配最终输出)或者以指数增长重新分配(当它耗尽内存时,它会分配下一次是两倍,因此对于2GB字符串,它只需要重新分配大约30次)。

嵌套循环的示例呈线性增长。它需要一个小字符串并增长到1000,然后在一个大型操作中将1000加到更大的字符串上。由于大字符串变得非常大,因此创建新字符串所产生的副本需要很长时间。减少完成此操作的次数(而不是更频繁地调整较小的字符串),可以提高速度。当然,StringBuilder在分配内存方面更加智能,因此速度更快。

答案 6 :(得分:1)

在字符串中添加字符会产生两种结果:

  • 如果角色还有空间,则最后添加; (正如评论者注意到的那样,c#字符串不会发生这种情况,因为你是不可改变的。)
  • 如果最后没有空格,则为新字符串分配新的内存块,将旧字符串的内容复制到那里并添加字符。

要分析代码,添加单个字符的1000000次会更简单。您的确切示例要解释得有点复杂,因为对于更高的i,您一次添加更多字符。

然后,在没有预留额外空间的情况下,第一个例子必须进行1000000次分配和复制,平均为0.5 * 1000000个字符。第二个必须进行1000次分配和平均0.5 * 1000000个字符的副本,以及1000000个分配和0.5 * 1000个字符的副本。如果复制是免费复制和分配大小的直线空间,则第一种情况需要500亿次单位时间,第二种情况需要5亿次+ 5亿次单位时间。