Answers:
试试这个代码。
这是您代码的略微修改版本。
1.我删除了Console.WriteLine,因为它可能比我要测量的速度慢几个数量级。
2.我要在循环之前启动秒表,然后在循环之后立即停止它,这样,如果函数需要26.4个滴答来执行,我就不会失去精度。
3.将结果除以一些迭代的方法是错误的。查看如果您有1000毫秒和100毫秒,将会发生什么。在这两种情况下,将其除以1000000都将得到0 ms。
Stopwatch s = new Stopwatch();
var p = new { FirstName = "Bill", LastName = "Gates" };
int n = 1000000;
long fElapsedMilliseconds = 0, fElapsedTicks = 0, cElapsedMilliseconds = 0, cElapsedTicks = 0;
string result;
s.Start();
for (var i = 0; i < n; i++)
result = (p.FirstName + " " + p.LastName);
s.Stop();
cElapsedMilliseconds = s.ElapsedMilliseconds;
cElapsedTicks = s.ElapsedTicks;
s.Reset();
s.Start();
for (var i = 0; i < n; i++)
result = string.Format("{0} {1}", p.FirstName, p.LastName);
s.Stop();
fElapsedMilliseconds = s.ElapsedMilliseconds;
fElapsedTicks = s.ElapsedTicks;
s.Reset();
Console.Clear();
Console.WriteLine(n.ToString()+" x result = string.Format(\"{0} {1}\", p.FirstName, p.LastName); took: " + (fElapsedMilliseconds) + "ms - " + (fElapsedTicks) + " ticks");
Console.WriteLine(n.ToString() + " x result = (p.FirstName + \" \" + p.LastName); took: " + (cElapsedMilliseconds) + "ms - " + (cElapsedTicks) + " ticks");
Thread.Sleep(4000);
这些是我的结果:
1000000 x结果= string.Format(“ {0} {1}”,p.FirstName,p.LastName); 花费:618ms-2213706滴答声
1000000 x结果=(p.FirstName +“” + p.LastName); 耗时:166毫秒-595610滴答
令我如此惊讶的是,有这么多人立即想找到执行速度最快的代码。如果一百万次迭代仍然需要不到一秒钟的时间来处理,那么最终用户是否会注意到这一点?不太可能。
过早优化=失败。
我会选择该String.Format
选项,只是因为从体系结构的角度来看它最有意义。在出现问题之前,我不会在意它的性能(如果确实如此,我会问自己:我是否需要一次串联一百万个名字?它们肯定不会全部显示在屏幕上...)
考虑一下您的客户以后是否要更改它,以便他们可以配置是显示"Firstname Lastname"
还是"Lastname, Firstname."
使用“格式”选项,这很容易-只需换出格式字符串即可。使用concat,您将需要额外的代码。当然,在这个特定示例中这听起来没什么大不了,但是可以推断出来。
哦,亲爱的-阅读其他回复之一后,我尝试了反转操作的顺序-因此请先执行串联,然后执行String.Format ...
Bill Gates
Console.WriteLine(p.FirstName + " " + p.LastName); took: 8ms - 30488 ticks
Bill Gates
Console.WriteLine("{0} {1}", p.FirstName, p.LastName); took: 0ms - 182 ticks
因此,操作的顺序会产生巨大的差异,或者说第一个操作总是慢得多。
这是一个运行的结果,其中操作已多次完成。我尝试更改订单,但是一旦忽略第一个结果,事情通常遵循相同的规则:
Bill Gates
Console.WriteLine(FirstName + " " + LastName); took: 5ms - 20335 ticks
Bill Gates
Console.WriteLine(FirstName + " " + LastName); took: 0ms - 156 ticks
Bill Gates
Console.WriteLine(FirstName + " " + LastName); took: 0ms - 122 ticks
Bill Gates
Console.WriteLine("{0} {1}", FirstName, LastName); took: 0ms - 181 ticks
Bill Gates
Console.WriteLine("{0} {1}", FirstName, LastName); took: 0ms - 122 ticks
Bill Gates
String.Concat(FirstName, " ", LastName); took: 0ms - 142 ticks
Bill Gates
String.Concat(FirstName, " ", LastName); took: 0ms - 117 ticks
如您所见,相同方法的后续运行(我将代码重构为3个方法)以更快的速度递增。最快的似乎是Console.WriteLine(String.Concat(...))方法,然后是普通串联,然后是格式化操作。
启动的最初延迟可能是Console Stream的初始化,因为在第一个操作使所有时间恢复一致之前放置Console.Writeline(“ Start!”)。
字符串是不可变的,这意味着一遍又一遍地在代码中使用相同的内存。一遍又一遍地添加相同的两个字符串并一次创建相同的新字符串不会影响内存。.Net足够聪明,可以使用相同的内存引用。因此,您的代码并没有真正测试两个concat方法之间的区别。
尝试以下尺寸:
Stopwatch s = new Stopwatch();
int n = 1000000;
long fElapsedMilliseconds = 0, fElapsedTicks = 0, cElapsedMilliseconds = 0, cElapsedTicks = 0, sbElapsedMilliseconds = 0, sbElapsedTicks = 0;
Random random = new Random(DateTime.Now.Millisecond);
string result;
s.Start();
for (var i = 0; i < n; i++)
result = (random.Next().ToString() + " " + random.Next().ToString());
s.Stop();
cElapsedMilliseconds = s.ElapsedMilliseconds;
cElapsedTicks = s.ElapsedTicks;
s.Reset();
s.Start();
for (var i = 0; i < n; i++)
result = string.Format("{0} {1}", random.Next().ToString(), random.Next().ToString());
s.Stop();
fElapsedMilliseconds = s.ElapsedMilliseconds;
fElapsedTicks = s.ElapsedTicks;
s.Reset();
StringBuilder sb = new StringBuilder();
s.Start();
for(var i = 0; i < n; i++){
sb.Clear();
sb.Append(random.Next().ToString());
sb.Append(" ");
sb.Append(random.Next().ToString());
result = sb.ToString();
}
s.Stop();
sbElapsedMilliseconds = s.ElapsedMilliseconds;
sbElapsedTicks = s.ElapsedTicks;
s.Reset();
Console.WriteLine(n.ToString() + " x result = string.Format(\"{0} {1}\", p.FirstName, p.LastName); took: " + (fElapsedMilliseconds) + "ms - " + (fElapsedTicks) + " ticks");
Console.WriteLine(n.ToString() + " x result = (p.FirstName + \" \" + p.LastName); took: " + (cElapsedMilliseconds) + "ms - " + (cElapsedTicks) + " ticks");
Console.WriteLine(n.ToString() + " x sb.Clear();sb.Append(random.Next().ToString()); sb.Append(\" \"); sb.Append(random.Next().ToString()); result = sb.ToString(); took: " + (sbElapsedMilliseconds) + "ms - " + (sbElapsedTicks) + " ticks");
Console.WriteLine("****************");
Console.WriteLine("Press Enter to Quit");
Console.ReadLine();
样本输出:
1000000 x result = string.Format("{0} {1}", p.FirstName, p.LastName); took: 513ms - 1499816 ticks
1000000 x result = (p.FirstName + " " + p.LastName); took: 393ms - 1150148 ticks
1000000 x sb.Clear();sb.Append(random.Next().ToString()); sb.Append(" "); sb.Append(random.Next().ToString()); result = sb.ToString(); took: 405ms - 1185816 ticks
string.Format
值得在这里达到微不足道的性能。从结构上讲,它更好,因为它意味着您可以更轻松地更改格式。但是我真的不明白stringbuilder的意义。这里的所有其他线程都说您应该使用Stringbuilder而不是连接字符串。有什么好处?正如该基准所证明的,显然不是速度。
可怜可怜的翻译
如果您知道您的应用程序将使用英语,那就很好,保存时钟滴答。但是,许多文化通常会在例如地址中看到“姓氏名”。
因此,请使用string.Format()
,尤其是如果您要将应用程序放到英语不是第一语言的任何地方。
string.Format()
在不同的文化中表现如何不同?它还会打印名字然后姓吗?在这两种情况下,您似乎都必须考虑到不同的文化。我觉得我在这里想念什么。
string.Format()
知道您使用的是地址名称?如果根据文化进行string.Format()
交换{0} {1}
,我会认为它是坏的。
这是我经过100,000次迭代的结果:
Console.WriteLine("{0} {1}", p.FirstName, p.LastName); took (avg): 0ms - 689 ticks
Console.WriteLine(p.FirstName + " " + p.LastName); took (avg): 0ms - 683 ticks
这是基准代码:
Stopwatch s = new Stopwatch();
var p = new { FirstName = "Bill", LastName = "Gates" };
//First print to remove the initial cost
Console.WriteLine(p.FirstName + " " + p.LastName);
Console.WriteLine("{0} {1}", p.FirstName, p.LastName);
int n = 100000;
long fElapsedMilliseconds = 0, fElapsedTicks = 0, cElapsedMilliseconds = 0, cElapsedTicks = 0;
for (var i = 0; i < n; i++)
{
s.Start();
Console.WriteLine(p.FirstName + " " + p.LastName);
s.Stop();
cElapsedMilliseconds += s.ElapsedMilliseconds;
cElapsedTicks += s.ElapsedTicks;
s.Reset();
s.Start();
Console.WriteLine("{0} {1}", p.FirstName, p.LastName);
s.Stop();
fElapsedMilliseconds += s.ElapsedMilliseconds;
fElapsedTicks += s.ElapsedTicks;
s.Reset();
}
Console.Clear();
Console.WriteLine("Console.WriteLine(\"{0} {1}\", p.FirstName, p.LastName); took (avg): " + (fElapsedMilliseconds / n) + "ms - " + (fElapsedTicks / n) + " ticks");
Console.WriteLine("Console.WriteLine(p.FirstName + \" \" + p.LastName); took (avg): " + (cElapsedMilliseconds / n) + "ms - " + (cElapsedTicks / n) + " ticks");
因此,我不知道将谁的回复标记为答案:)
在这样的简单情况下,连接字符串是很好的-它比任何复杂的事情都复杂,甚至包括LastName,FirstName。使用这种格式,您一眼就能看到在读取代码时字符串的最终结构是什么,通过串联几乎几乎不可能立即分辨出最终结果(除了像这样的非常简单的示例)。
从长远来看,这意味着当您回来更改字符串格式时,您将可以弹出并对格式字符串进行一些调整,或者可以皱眉并开始四处移动各种属性访问器与文本混合在一起,这很可能会带来问题。
如果您使用的是.NET 3.5,则可以使用像这样的扩展方法,这样就可以轻松实现,就像这样:
string str = "{0} {1} is my friend. {3}, {2} is my boss.".FormatWith(prop1,prop2,prop3,prop4);
最后,随着应用程序复杂性的增加,您可能决定要合理地在应用程序中维护字符串,您希望将其移动到资源文件中以进行本地化或仅移动到静态帮助器中。如果您使用一致的格式,这将更容易实现,并且您的代码可以很简单地重构为使用类似
string name = String.Format(ApplicationStrings.General.InformalUserNameFormat,this.FirstName,this.LastName);
虽然我完全理解样式首选项,并部分根据自己的喜好选择了第一个答案,但是我的部分决定是基于这样的想法,即串联会更快。因此,出于好奇,我对其进行了测试,结果令人st舌,尤其是对于这么小的字符串。
使用以下代码:
System.Diagnostics.Stopwatch s = new System.Diagnostics.Stopwatch();
var p = new { FirstName = "Bill", LastName = "Gates" };
s.Start();
Console.WriteLine("{0} {1}", p.FirstName, p.LastName);
s.Stop();
Console.WriteLine("Console.WriteLine(\"{0} {1}\", p.FirstName, p.LastName); took: " + s.ElapsedMilliseconds + "ms - " + s.ElapsedTicks + " ticks");
s.Reset();
s.Start();
Console.WriteLine(p.FirstName + " " + p.LastName);
s.Stop();
Console.WriteLine("Console.WriteLine(p.FirstName + \" \" + p.LastName); took: " + s.ElapsedMilliseconds + "ms - " + s.ElapsedTicks + " ticks");
我得到以下结果:
Bill Gates
Console.WriteLine("{0} {1}", p.FirstName, p.LastName); took: 2ms - 7280 ticks
Bill Gates
Console.WriteLine(p.FirstName + " " + p.LastName); took: 0ms - 67 ticks
使用格式化方法要慢100倍!!串联甚至都没有注册为1ms,这就是为什么我也输出定时器滴答的原因。
从C#6.0开始,可以使用插值字符串来执行此操作,从而进一步简化了格式。
var name = "Bill";
var surname = "Gates";
MessageBox.Show($"Welcome to the show, {name} {surname}!");
内插的字符串表达式看起来像包含表达式的模板字符串。内插的字符串表达式通过将包含的表达式替换为表达式结果的ToString表示形式来创建字符串。
插值的字符串具有与String.Format相似的性能,但是由于将值和表达式插入行内,因此提高了可读性并缩短了语法。
另请参阅有关字符串内插的dotnetperls文章。
如果您正在寻找格式化字符串的默认方式,那么就可读性和性能而言,这是有意义的(除非微秒会在您的特定用例中有所作为)。
对于基本的字符串连接,我通常使用第二种样式-易于阅读和简化。但是,如果我要进行更复杂的字符串组合,则通常会选择String.Format。
String.Format节省了大量的引号和加号...
Console.WriteLine("User {0} accessed {1} on {2}.", user.Name, fileName, timestamp);
vs
Console.WriteLine("User " + user.Name + " accessed " + fileName + " on " + timestamp + ".");
仅保存了一些字符,但是在此示例中,我认为格式使它更加整洁。
更好的测试方法是使用Perfmon和CLR内存计数器监视您的内存。我的理解是,您想使用String.Format而不是仅连接字符串的全部原因是,由于字符串是不可变的,因此不必要地给垃圾收集器增加了临时字符串,这些字符串需要在下一遍中回收。
尽管StringBuilder和String.Format可能更慢,但它们的内存效率更高。
通常,我更喜欢前者,特别是当字符串变长时,它会更容易阅读。
我相信另一个好处是性能,因为后者实际上会在将最终字符串传递给Console.Write方法之前执行2个字符串创建语句。我相信String.Format在底层使用了StringBuilder,因此避免了多个串联。
但是应注意,如果要传递给String.Format的参数(以及诸如Console.Write之类的其他此类方法)是值类型,则它们将在传递之前被装箱,这可以提供其自身的性能命中率。关于此的博客文章。
从2015年8月19日起一周,这个问题将是正确的七(7)年。现在有一种更好的方法。在可维护性方面更好,因为与串联字符串相比,我没有进行任何性能测试(但这几天有关系吗?相差几毫秒?)。使用C#6.0的新方法:
var p = new { FirstName = "Bill", LastName = "Gates" };
var fullname = $"{p.FirstName} {p.LastName}";
这个新功能更好,IMO,实际上对我们来说更好,因为我们有代码来构建查询字符串,其值取决于某些因素。想象一个有6个参数的querystring。因此,例如:
var qs = string.Format("q1={0}&q2={1}&q3={2}&q4={3}&q5={4}&q6={5}",
someVar, anotherVarWithLongName, var3, var4, var5, var6)
in可以这样写,更容易阅读:
var qs=$"q1={someVar}&q2={anotherVarWithLongName}&q3={var3}&q4={var4}&q5={var5}&q6={var6}";
我选择基于可读性。当变量周围有一些文本时,我更喜欢格式选项。在此示例中:
Console.WriteLine("User {0} accessed {1} on {2}.",
user.Name, fileName, timestamp);
即使没有变量名,您也可以理解其含义,而concat却用引号和+号打乱了我的眼睛:
Console.WriteLine("User " + user.Name + " accessed " + fileName +
" on " + timestamp + ".");
(我喜欢麦克的例子,因为我喜欢)
如果没有变量名,格式字符串的意义不大,那么我必须使用concat:
Console.WriteLine("{0} {1}", p.FirstName, p.LastName);
格式选项使我可以读取变量名并将其映射到相应的数字。concat选项不需要。我仍然对引号和+号感到困惑,但替代方法更糟。红宝石?
Console.WriteLine(p.FirstName + " " + p.LastName);
在性能方面,我希望format选项比concat慢,因为format需要解析字符串。我不记得必须优化这种指令,但是如果这样做的话,我会看一下string
像Concat()
和Join()
。
格式的另一个优点是可以将格式字符串放在配置文件中。错误消息和UI文本非常方便。
我将使用String.Format,但我还将在资源文件中使用格式字符串,以便可以将其本地化为其他语言。使用简单的字符串concat不允许您这样做。显然,如果您永远不需要本地化该字符串,这都不是考虑的理由。这实际上取决于字符串的用途。
如果要显示给用户,我将使用String.Format以便可以本地化-FxCop会为我拼写检查,以防万一:)
如果它包含数字或任何其他非字符串形式的内容(例如日期),我将使用String.Format,因为它使我可以更好地控制formatting。
如果是用于构建类似SQL的查询,则可以使用Linq。
如果要在循环内连接字符串,我将使用StringBuilder以避免性能问题。
如果它用于某些输出,用户将看不到,并且不会影响性能,那么我将使用String.Format,因为无论如何我都习惯使用它,而我只是习惯了:)
好一个!
刚刚添加
s.Start();
for (var i = 0; i < n; i++)
result = string.Concat(p.FirstName, " ", p.LastName);
s.Stop();
ceElapsedMilliseconds = s.ElapsedMilliseconds;
ceElapsedTicks = s.ElapsedTicks;
s.Reset();
而且它甚至更快(我猜这两个示例中都调用了string.Concat,但是第一个需要某种翻译)。
1000000 x result = string.Format("{0} {1}", p.FirstName, p.LastName); took: 249ms - 3571621 ticks
1000000 x result = (p.FirstName + " " + p.LastName); took: 65ms - 944948 ticks
1000000 x result = string.Concat(p.FirstName, " ", p.LastName); took: 54ms - 780524 ticks
string.Concat(...)
。它是在编译期间完成的,因此对运行时性能没有影响。如果您多次运行测试或在更大的测试样本上运行它们,您会发现它们是相同的。
由于我认为此处的答案无法涵盖所有内容,因此我想在此处添加一些内容。
Console.WriteLine(string format, params object[] pars)
来电string.Format
。“ +”表示字符串连接。我认为这并不总是与风格有关。我倾向于根据所处的环境来混合使用两种样式。
简短答案
您面临的决定与字符串分配有关。我会尽量简化。
说你有
string s = a + "foo" + b;
如果执行此操作,它将评估如下:
string tmp1 = a;
string tmp2 = "foo"
string tmp3 = concat(tmp1, tmp2);
string tmp4 = b;
string s = concat(tmp3, tmp4);
tmp
这里实际上不是局部变量,但对于JIT是临时的(已将其压入IL堆栈)。如果将字符串压入堆栈(例如,ldstr
在IL中输入文字),则会在堆栈上放置对字符串指针的引用。
调用concat
此引用的那一刻就成了问题,因为没有任何包含两个字符串的字符串引用。这意味着.NET需要分配一个新的内存块,然后用两个字符串填充它。之所以成为问题,是因为分配相对昂贵。
哪个将问题变为:如何减少concat
操作次数?
因此,粗略的答案是:string.Format
对于> 1个concat,“ +”将对1个concat正常工作。而且,如果您不关心微性能优化,string.Format
在一般情况下也可以正常工作。
关于文化的笔记
还有一种叫做文化的东西
string.Format
使您可以使用CultureInfo
格式。一个简单的运算符“ +”使用当前的区域性。
如果要编写文件格式和f.ex,这尤其重要。double
您“添加”到字符串的值。在不同的机器上,如果不使用string.Format
explicit ,可能会以不同的字符串结尾CultureInfo
。
F.ex. 考虑一下如果更改“。”会发生什么。对于“,”,则在编写您的逗号分隔值文件时……用荷兰语,十进制分隔符是一个逗号,因此您的用户可能会得到一个“有趣”的惊喜。
更详细的答案
如果您事先不知道字符串的确切大小,最好使用这样的策略来综合使用的缓冲区。首先填充松弛空间,然后复制数据。
增长意味着分配新的内存块并将旧数据复制到新缓冲区。然后可以释放旧的内存块。您现在已经掌握了底线:增长是一项昂贵的操作。
执行此操作的最实用方法是使用过度分配策略。最常见的策略是以2的幂来整体分配缓冲区。 ),但您得到了图片。该策略确保您不需要我在上面描述的太多昂贵的操作。
StringBuilder
是一类,基本上以2的幂次占用基础缓冲区。在引擎盖下string.Format
使用StringBuilder
。
这使您的决定成为总体分配与追加(多个)(无文化)或分配分配与追加之间的基本权衡。
我很好奇StringBuilder在这些测试中的地位。结果如下...
class Program {
static void Main(string[] args) {
var p = new { FirstName = "Bill", LastName = "Gates" };
var tests = new[] {
new { Name = "Concat", Action = new Action(delegate() { string x = p.FirstName + " " + p.LastName; }) },
new { Name = "Format", Action = new Action(delegate() { string x = string.Format("{0} {1}", p.FirstName, p.LastName); }) },
new { Name = "StringBuilder", Action = new Action(delegate() {
StringBuilder sb = new StringBuilder();
sb.Append(p.FirstName);
sb.Append(" ");
sb.Append(p.LastName);
string x = sb.ToString();
}) }
};
var Watch = new Stopwatch();
foreach (var t in tests) {
for (int i = 0; i < 5; i++) {
Watch.Reset();
long Elapsed = ElapsedTicks(t.Action, Watch, 10000);
Console.WriteLine(string.Format("{0}: {1} ticks", t.Name, Elapsed.ToString()));
}
}
}
public static long ElapsedTicks(Action ActionDelg, Stopwatch Watch, int Iterations) {
Watch.Start();
for (int i = 0; i < Iterations; i++) {
ActionDelg();
}
Watch.Stop();
return Watch.ElapsedTicks / Iterations;
}
}
结果:
康卡特:406滴答 康卡特:356滴答 康卡特:411滴答 康卡特:299滴答 康卡特:266滴答 格式:5269 ticks 格式:954 ticks 格式:1004 ticks 格式:984 ticks 格式:974 StringBuilder:629分钟 StringBuilder:484个报价 StringBuilder:482个滴答 StringBuilder:508分钟 StringBuilder:504分钟
string.Format
,它不使用任何复合格式设置功能(即,简单的{0}
),而是用相当快的字符串连接替换它们。我想知道,使用现有的IL重写器(例如PostSharp)是否可以实现这样的壮举。