为什么使用StringBuilder比字符串拼接更慢?

12
为什么与+号拼接相比,StringBuilder更慢? StringBuilder旨在避免额外的对象创建,但为什么它会惩罚性能?
    static void Main(string[] args)
    {
        int max = 1000000;
        for (int times = 0; times < 5; times++)
        {
            Console.WriteLine("\ntime: {0}", (times+1).ToString());
            Stopwatch sw = Stopwatch.StartNew();
            for (int i = 0; i < max; i++)
            {
                string msg = "Your total is ";
                msg += "$500 ";
                msg += DateTime.Now;
            }
            sw.Stop();
            Console.WriteLine("String +\t: {0}ms", ((int)sw.ElapsedMilliseconds).ToString().PadLeft(6));

            sw = Stopwatch.StartNew();
            for (int j = 0; j < max; j++)
            {
                StringBuilder msg = new StringBuilder();
                msg.Append("Your total is ");
                msg.Append("$500 ");
                msg.Append(DateTime.Now);
            }
            sw.Stop();
            Console.WriteLine("StringBuilder\t: {0}ms", ((int)sw.ElapsedMilliseconds).ToString().PadLeft(6));
        }
        Console.Read();
    }

图片说明

编辑:按照建议将超出范围的变量移动:

图片说明


5
我认为StringBuilder对于“较大”的字符串更快。 - LarsTech
除非你只是进行简单的单个连接并且不值得增加代码行数,否则通常情况下它都会更快。正如NullUser指出的那样,你正在循环内部分配内存,这是错误的,并且会影响你的数据。 - bryanmac
1
为什么每个人都默认字符串拼接很“慢”?重复的字符串拼接有一个更差的大O,但不要忘记C并牢记n :) 使用正确的工具来完成工作——很少有一天我会使用StringBuilder。(另外,我不知道C#做了哪些优化,但Java 可能在编译期间将+转换为等效的StringBuilder代码。) - user166390
5
StringBuilder的设计初衷是为了避免创建额外的对象,但如果由于某些未知的原因你在每个迭代中都创建一个新的StringBuilder对象的话...... - Ed S.
@sehe 我测试过了,调用 "new StringBuilder(64)" 几乎和字符串拼接一样快。String +: 2959毫秒,StringBuilder: 3122毫秒,StringBuilder(64): 2902毫秒。 - MerickOWA
显示剩余7条评论
7个回答

16

修改代码以避免反复实例化StringBuilder,改用.Clear()方法:

time: 1
String +    :   3348ms
StringBuilder   :   3151ms

time: 2
String +    :   3346ms
StringBuilder   :   3050ms

请注意,这仍然测试完全相同的功能,但尝试更智能地重用资源。

代码:(也可以在http://ideone.com/YuaqY上实时查看)

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

public class Program
{
    static void Main(string[] args)
    {
        int max = 1000000;
        for (int times = 0; times < 5; times++)
        {
            {
                Console.WriteLine("\ntime: {0}", (times+1).ToString());
                Stopwatch sw = Stopwatch.StartNew();
                for (int i = 0; i < max; i++)
                {
                    string msg = "Your total is ";
                    msg += "$500 ";
                    msg += DateTime.Now;
                }
                sw.Stop();
                Console.WriteLine("String +\t: {0}ms", ((int)sw.ElapsedMilliseconds).ToString().PadLeft(6));
            }

            {
                Stopwatch sw = Stopwatch.StartNew();
                StringBuilder msg = new StringBuilder();
                for (int j = 0; j < max; j++)
                {
                    msg.Clear();
                    msg.Append("Your total is ");
                    msg.Append("$500 ");
                    msg.Append(DateTime.Now);
                }
                sw.Stop();
                Console.WriteLine("StringBuilder\t: {0}ms", ((int)sw.ElapsedMilliseconds).ToString().PadLeft(6));
            }
        }
        Console.Read();
    }
}

的确我漏了.Clear()。使用StringBuilder后,成本大大降低。现在是3196ms!有时会变慢,但考虑到这个调整,已经有所改善了。:D - Junior Mayhé
@JuniorMayhé 几百毫秒的差异(在3000+中)并不重要。当处理大字符串时,使用SB和+之间存在数量级的差异:http://ideone.com/SNBr1 - NullUserException
@NullUserExceptionఠ_ఠ:我们都知道这一点。我很好奇是否可以在不改变测试的情况下使它更快。结果证明,是可以的。当然,这是一个合成测试的糟糕例子,但它清楚地驳斥了“StringBuilder”会更慢的观点:即使用于非最优任务,也不一定如此。 - sehe
1
@sehe 确实,那也让我有点惊讶。+1 为了展示它 ;) - NullUserException
你的 ideone 链接给了我一个编译错误。顺便说一句,我喜欢你刚刚创建了一个新的作用域而不是重命名变量...哈哈 - NullUserException
@NullUserExceptionఠ_ఠ:说得好。**StringBuilder.Clear在.NET #4.0中是新功能**,而ideone是在3.5上运行的。现在使用msg.Remove(0, msg.Length)进行仿真-具有相同的性能提升。 - sehe

8
您每次迭代都创建了一个新的StringBuilder实例,这会产生一些开销。由于您没有将其用于其实际意图(即:构建需要许多字符串连接操作的大型字符串),因此不足为奇地看到比连接更差的性能。 StringBuilder的更常见的比较/用法是类似于:
string msg = "";
for (int i = 0; i < max; i++)
{
    msg += "Your total is ";
    msg += "$500 ";
    msg += DateTime.Now;
}

StringBuilder msg_sb = new StringBuilder();
for (int j = 0; j < max; j++)
{
    msg_sb.Append("Your total is ");
    msg_sb.Append("$500 ");
    msg_sb.Append(DateTime.Now);
}

使用StringBuilder和字符串拼接之间会有明显的性能差异。这里的“明显”是指数量级的差异,而不是你在示例中观察到的大约10%的差异。

StringBuilder不需要构建大量将被丢弃的中间字符串,因此它具有更好的性能。这就是它的用途所在。对于较小的字符串,最好使用字符串拼接以保持简单和清晰。


2
你正在测量完全不同的东西,尽管如此。 - sehe
1
+1 - 正如NullUser所指出的,您只需要在循环外分配一次内存,然后在循环内追加数据即可。 - bryanmac
2
@JuniorMayhé 这是正确的答案。在您的示例中,将StringBuilder和连接放在同一个循环中并不完全说明问题,因为StringBuilder必须等待连接发生。您的字符串变得越大,StringBuilder就会变得越快。 - Pete
@sehe 我知道了;我已经相应地修改了我的答案。 - NullUserException

2
请注意,此处需要注意的是,
string msg = "Your total is ";
msg += "$500 ";
msg += DateTime.Now;

编译成

string msg = String.Concat("Your total is ", "$500 ");
msg = String.Concat(msg, DateTime.Now.ToString());

每次迭代需要两个字符串拼接和一个ToString操作。此外,单个String.Concat非常快,因为它知道结果字符串的大小,所以它只分配一次结果字符串,然后快速将源字符串复制到其中。这意味着在实践中

String.Concat(x, y);

将始终表现更好

StringBuilder builder = new StringBuilder();
builder.Append(x);
builder.Append(y);

因为StringBuilder不能采用这样的快捷方式(您可以调用第三个Append或Remove,但String.Concat不支持)。 StringBuilder的工作方式是通过分配初始缓冲区并将字符串长度设置为0来实现的。每次Append时,它必须检查缓冲区,可能分配更多的缓冲区空间(通常是将旧缓冲区复制到新缓冲区),复制字符串并增加构建器的字符串长度。String.Concat不需要执行所有这些额外的工作。
因此,对于简单的字符串连接,x + y(即String.Concat)将始终优于StringBuilder。
现在,一旦您开始将许多字符串连接到单个缓冲区中,或者在缓冲区上进行大量操作时,您将开始从StringBuilder中获得好处,因为当不使用StringBuilder时,您需要不断创建新字符串。这是因为StringBuilder仅偶尔分配新存储器块,但String.Concat、String.SubString等几乎总是分配新存储器(像"".SubString(0,0)或String.Concat("", "")这样的东西不会分配存储器,但这些是退化情况)。

2

除了没有以最有效的方式使用StringBuilder之外,您也没有尽可能有效地使用字符串连接。如果您提前知道要连接多少个字符串,则将所有操作都放在一行上应该是最快的。编译器会优化操作,以便不生成中间字符串。

我添加了几个测试用例。其中一个与sehe建议的基本相同,另一个在一行中生成字符串:

sw = Stopwatch.StartNew();
builder = new StringBuilder();
for (int j = 0; j < max; j++)
{
    builder.Clear();
    builder.Append("Your total is ");
    builder.Append("$500 ");
    builder.Append(DateTime.Now);
}
sw.Stop();
Console.WriteLine("StringBuilder (clearing)\t: {0}ms", ((int)sw.ElapsedMilliseconds).ToString().PadLeft(6));

sw = Stopwatch.StartNew();
for (int i = 0; i < max; i++)
{
    msg = "Your total is " + "$500" + DateTime.Now;
}
sw.Stop();
Console.WriteLine("String + (one line)\t: {0}ms", ((int)sw.ElapsedMilliseconds).ToString().PadLeft(6));

以下是我电脑上看到的输出示例:
time: 1
String +    :   3707ms
StringBuilder   :   3910ms
StringBuilder (clearing)    :   3683ms
String + (one line) :   3645ms

time: 2
String +    :   3703ms
StringBuilder   :   3926ms
StringBuilder (clearing)    :   3666ms
String + (one line) :   3625ms

总的来说: - 如果你需要在多个步骤中构建一个较大的字符串,或者你不知道会有多少字符串要连接起来,StringBuilder 做得更好。 - 当可以合理地使用单个表达式将它们全部拼接在一起时,这是更好的选择。

2

使用StringBuilder相对于短字符串来说,更能体现它的优势。

每次拼接字符串都会创建一个新的字符串对象,所以原字符串越长,就需要越多的时间从旧的字符串中复制到新的字符串中。

此外,创建许多临时对象可能会对性能产生不利影响,这种影响并不能通过StopWatch来衡量,因为它会在托管堆上产生大量临时对象,从而导致更多的垃圾回收循环。

如果修改测试用例以创建(更)长的字符串,并使用(更)多的拼接/追加操作,则StringBuilder的性能应该会更好。


0
这是一个示例,演示了一个情况,在这种情况下,StringBuilder 的执行速度比字符串拼接更快:
static void Main(string[] args)
{
    const int sLen = 30, Loops = 10000;
    DateTime sTime, eTime;
    int i;
    string sSource = new String('X', sLen);
    string sDest = "";
    // 
    // Time StringBuilder.
    // 
    for (int times = 0; times < 5; times++)
    {
        sTime = DateTime.Now;
        System.Text.StringBuilder sb = new System.Text.StringBuilder((int)(sLen * Loops * 1.1));
        Console.WriteLine("Result # " + (times + 1).ToString());
        for (i = 0; i < Loops; i++)
        {
            sb.Append(sSource);
        }
        sDest = sb.ToString();
        eTime = DateTime.Now;
        Console.WriteLine("String Builder took :" + (eTime - sTime).TotalSeconds + " seconds.");
        // 
        // Time string concatenation.
        // 
        sTime = DateTime.Now;
        for (i = 0; i < Loops; i++)
        {
            sDest += sSource;
            //Console.WriteLine(i);
        }
        eTime = DateTime.Now;
        Console.WriteLine("Concatenation took : " + (eTime - sTime).TotalSeconds + " seconds.");
        Console.WriteLine("\n");
    }
    // 
    // Make the console window stay open
    // so that you can see the results when running from the IDE.
    // 
}

结果 # 1 字符串构建器需要:0秒。 连接需要:8.7659616秒。

结果 # 2 字符串构建器需要:0秒。 连接需要:8.7659616秒。

结果 # 3 字符串构建器需要:0秒。 连接需要:8.9378432秒。

结果 # 4 字符串构建器需要:0秒。 连接需要:8.7972128秒。

结果 # 5 字符串构建器需要:0秒。 连接需要:8.8753408秒。

StringBuilder 比 + 连接更快。


0

我认为比较String和StringBuilder的效率要比比较时间更好。

msdn所说: String被称为不可变对象,因为一旦创建后其值就无法修改。看似修改String的方法实际上是返回一个包含修改内容的新String。如果需要修改类似字符串的对象的实际内容,则应使用System.Text.StringBuilder类。

string msg = "Your total is "; // a new string object
msg += "$500 "; // a new string object
msg += DateTime.Now; // a new string object

看看哪一个更好。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接