【问题标题】:String operation optimisation in C#C#中的字符串操作优化
【发布时间】:2010-09-21 21:36:35
【问题描述】:

以下 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(),这是真的吗?

【问题讨论】:

  • 有趣,我想知道使用 System.Text.StringBuilder 需要多长时间,但我太累了,无法启动虚拟机,收藏(如果这是一个词)
  • 检查一下,我现在已经完成了 StringBuilder 测试。它比我的内部循环慢,但仍然比原始代码快得多。
  • 请注意,StringBuilder 将可选的初始容量作为其参数!这应该会快得多
  • 实际上,在这个例子中使用它几乎没有实际区别。
  • 当你测试字符串生成器调用 Append(i);

标签: c# string optimization performance


【解决方案1】:

我根本没有得到你的结果。在我的盒子上,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

您的连接比第一个解决方案更快的原因很简单 - 您正在执行几个“廉价”连接(每次复制的数据相对较少)和相对较少的“大”连接(整个字符串)迄今为止)。原来,每一步都会复制目前得到的所有数据,显然成本更高。

【讨论】:

  • 我正在使用 DateTime.Now 进行计时(我坚持使用 2.0),但除此之外,从字符串中提取的数字末尾只有几个乘法,这是一个常数所有运行的时间。
  • 实际上,不,现在我想起来了,你是对的,我有控制台打印...现在修复问题中的时间。
  • 这里也一样。对于我来说,原来的仍在运行,第一个大约 700 毫秒,最后一个(StringBuilder)63 毫秒。
  • 秒表是在 .NET 2.0 中引入的,因此您不会被 DateTime.Now 卡住 :)
  • :)。第二轮我忘记启动秒表了! ^^ 已更正。
【解决方案2】:

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

[更新]:仅评论您对问题的编辑。如果您对连接字符串的最终大小有一个近似(或准确)的想法,您还可以提高StringBuilder 的性能,因为这将减少它必须执行的内存分配数量:

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

【讨论】:

    【解决方案3】:

    您可能会看到前 1000 个字符几乎不需要时间,而后 1000 个字符则相反。

    我认为耗时的部分是每次添加字符时将大字符串实际复制到新的内存区域,这对您的计算机来说是一项艰巨的工作。

    您的优化可以很容易地与您通常对流执行的操作进行比较,您使用的是缓冲区。较大的块通常会带来更好的性能,直到您达到不再产生任何影响的临界大小,并且在处理少量数据时开始成为不利因素。

    如果您从一开始就定义了一个具有适当大小的字符数组,它可能会非常快,因为这样就不必一遍又一遍地复制它。

    【讨论】:

    • 这涉及到更多的代码在字符串和字符数组之间进行转换,但我同意你对情况的分析。我只是发现这可能成为障碍的确切水平很有趣。
    【解决方案4】:

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

    不,就像所有绝对陈述一样,这是无稽之谈。然而,确实,使用Format 通常会使格式化代码更具可读性,并且通常比串联略快——但速度并不是这里的决定因素。

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

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

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

    【讨论】:

    • 外循环在字符串的长度上,而不是i
    • 由于处理方式的原因,外部循环实际上在第二个代码 sn-p 中以更长的答案停止,因此它会在更短的时间内生成更长的序列。
    • “就像所有的绝对陈述一样,这是无稽之谈。”嘿,使用讽刺 +1!
    【解决方案5】:

    我现在不能做测试,但尝试使用 StringBuilder。

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

    【讨论】:

      【解决方案6】:

      回答修改后的问题(“为什么这种非显而易见的优化工作得这么好”和“你真的不应该在字符串上使用 + 运算符吗”):

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

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

      StringBuilders 通过使用比固定长度字符串更智能地分配的内存缓冲区来工作,因此在大多数情况下表现得更好。我不确定内部 StringBuilder 的详细信息,因此您必须为此提出一个新问题。我可以推测它要么不重新分配字符串的旧部分(而是在内部创建一个链表,并且仅在 ToString 需要时才实际分配最终输出),要么它以指数增长重新分配(当内存不足时,它分配下一次是两倍,因此对于一个 2GB 的字符串,它只需要重新分配大约 30 次)。

      您的嵌套循环示例线性增长。它需要一个小字符串并将其增长到 1000,然后在一次大型操作中将 1000 附加到较大的字符串上。随着大字符串变得非常大,创建新字符串所产生的副本需要很长时间。当您减少执行此操作的次数时(改为更频繁地调整较小字符串的大小),您会提高速度。当然,StringBuilder 在分配内存方面更加智能,因此速度也更快。

      【讨论】:

        【解决方案7】:

        将字符添加到字符串会产生两种结果:

        • 如果字符仍有空间,则将其添加到末尾; (正如评论者所注意到的,c# 字符串不会发生这种情况,因为它们是不可变的)。
        • 如果末尾没有空间,则为新字符串分配新的内存块,将旧字符串的内容复制到那里并添加字符。

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

        那么在没有预留额外空间的情况下,第一个例子要做1000000次分配和拷贝,平均0.5 * 1000000个字符。第二个必须执行 1000 次分配和平均 0.5 * 1000000 个字符的副本,以及 1000000 次分配和 0.5 * 1000 个字符的副本。如果复制是线性的,复制和分配的大小是免费的,第一种情况需要 500 000 000 000 个时间单位,第二种情况需要 500 000 000 + 500 000 000 个时间单位。

        【讨论】:

        • 在 C# 中,字符串是不可变的。它没有改变原地的字符串。每次添加一个字符,就会创建一个全新的字符串。
        • 嗯,我知道...忽略第一个后果
        猜你喜欢
        • 1970-01-01
        • 2012-07-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-09-26
        • 1970-01-01
        相关资源
        最近更新 更多