以下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()
,这是真的吗?
答案 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)
在字符串中添加字符会产生两种结果:
要分析代码,添加单个字符的1000000次会更简单。您的确切示例要解释得有点复杂,因为对于更高的i,您一次添加更多字符。
然后,在没有预留额外空间的情况下,第一个例子必须进行1000000次分配和复制,平均为0.5 * 1000000个字符。第二个必须进行1000次分配和平均0.5 * 1000000个字符的副本,以及1000000个分配和0.5 * 1000个字符的副本。如果复制是免费复制和分配大小的直线空间,则第一种情况需要500亿次单位时间,第二种情况需要5亿次+ 5亿次单位时间。