String.Format 与 StringBuilder 一样高效吗
-
08-06-2019 - |
题
假设我有一个 C# 字符串生成器可以执行以下操作:
StringBuilder sb = new StringBuilder();
string cat = "cat";
sb.Append("the ").Append(cat).(" in the hat");
string s = sb.ToString();
这是否与以下内容一样有效或更有效:
string cat = "cat";
string s = String.Format("The {0} in the hat", cat);
如果是这样,为什么?
编辑
在一些有趣的答案之后,我意识到我可能应该更清楚地了解我的问题。我并不是问哪个连接字符串更快,而是哪个更快 注射 将一根绳子插入另一根绳子。
在上述两种情况下,我想将一个或多个字符串插入到预定义模板字符串的中间。
对困惑感到抱歉
解决方案
笔记: 这个答案是在 .NET 2.0 是当前版本时写的。这可能不再适用于更高版本。
String.Format
使用一个 StringBuilder
内部:
public static string Format(IFormatProvider provider, string format, params object[] args)
{
if ((format == null) || (args == null))
{
throw new ArgumentNullException((format == null) ? "format" : "args");
}
StringBuilder builder = new StringBuilder(format.Length + (args.Length * 8));
builder.AppendFormat(provider, format, args);
return builder.ToString();
}
上面的代码是 mscorlib 的一个片段,所以问题变成了“是 StringBuilder.Append()
比...快 StringBuilder.AppendFormat()
"?
如果没有基准测试,我可能会说上面的代码示例使用以下命令运行得更快 .Append()
. 。但这只是一个猜测,请尝试对两者进行基准测试和/或分析以获得正确的比较。
这个家伙,Jerry Dixon,做了一些基准测试:
http://jdixon.dotnetdevelopersjournal.com/string_concatenation_stringbuilder_and_stringformat.htm
更新:
遗憾的是上面的链接已经失效了。然而,回归机器上仍然有一份副本:
归根结底,这取决于您的字符串格式是否会被重复调用,即您正在对超过 100 兆字节的文本进行一些严肃的文本处理,或者是否在用户时不时单击按钮时调用它。除非你正在做一些巨大的批处理工作,否则我会坚持使用 String.Format,它有助于代码的可读性。如果您怀疑存在性能瓶颈,请在代码上使用分析器并查看它到底在哪里。
其他提示
来自 MSDN 文档:
String 或 StringBuilder 对象的串联操作的性能取决于内存分配发生的频率。String 串联操作始终分配内存,而 StringBuilder 串联操作仅在 StringBuilder 对象缓冲区太小而无法容纳新数据时才分配内存。因此,如果连接固定数量的 String 对象,则 String 类更适合连接操作。在这种情况下,编译器甚至可能将各个串联操作组合成单个操作。如果连接任意数量的字符串,则 StringBuilder 对象更适合连接操作;例如,如果循环连接随机数量的用户输入字符串。
我运行了一些快速性能基准测试,对于平均超过 10 次运行的 100,000 次操作,第一种方法(字符串生成器)花费的时间几乎是第二种方法(字符串格式)的一半。
因此,如果这种情况不常见,也没关系。但如果是普通操作,那么你可能想使用第一种方法。
我希望 字符串格式 要慢一些 - 它必须解析字符串并且 然后 连接它。
几点注意事项:
- 格式 是专业应用程序中用户可见字符串的方法;这可以避免本地化错误
- 如果您事先知道结果字符串的长度,请使用 字符串生成器(Int32) 构造函数来预定义容量
如果只是因为 string.Format 并不完全符合您的想法,那么这里是 6 年后在 Net45 上重新运行的测试。
Concat 仍然是最快的,但实际上相差不到 30%。StringBuilder 和 Format 仅相差 5-10%。我运行测试几次后得到了 20% 的变化。
毫秒,一百万次迭代:
- 级联:第367章
- 每个键的新 stringBuilder:第452章
- 缓存的 StringBuilder:第419章
- 字符串.格式:第475章
我得到的教训是,性能差异是微不足道的,因此它不应该阻止您编写最简单的可读代码。对于我的钱来说,经常但不总是 a + b + c
.
const int iterations=1000000;
var keyprefix= this.GetType().FullName;
var maxkeylength=keyprefix + 1 + 1+ Math.Log10(iterations);
Console.WriteLine("KeyPrefix \"{0}\", Max Key Length {1}",keyprefix, maxkeylength);
var concatkeys= new string[iterations];
var stringbuilderkeys= new string[iterations];
var cachedsbkeys= new string[iterations];
var formatkeys= new string[iterations];
var stopwatch= new System.Diagnostics.Stopwatch();
Console.WriteLine("Concatenation:");
stopwatch.Start();
for(int i=0; i<iterations; i++){
var key1= keyprefix+":" + i.ToString();
concatkeys[i]=key1;
}
Console.WriteLine(stopwatch.ElapsedMilliseconds);
Console.WriteLine("New stringBuilder for each key:");
stopwatch.Restart();
for(int i=0; i<iterations; i++){
var key2= new StringBuilder(keyprefix).Append(":").Append(i.ToString()).ToString();
stringbuilderkeys[i]= key2;
}
Console.WriteLine(stopwatch.ElapsedMilliseconds);
Console.WriteLine("Cached StringBuilder:");
var cachedSB= new StringBuilder(maxkeylength);
stopwatch.Restart();
for(int i=0; i<iterations; i++){
var key2b= cachedSB.Clear().Append(keyprefix).Append(":").Append(i.ToString()).ToString();
cachedsbkeys[i]= key2b;
}
Console.WriteLine(stopwatch.ElapsedMilliseconds);
Console.WriteLine("string.Format");
stopwatch.Restart();
for(int i=0; i<iterations; i++){
var key3= string.Format("{0}:{1}", keyprefix,i.ToString());
formatkeys[i]= key3;
}
Console.WriteLine(stopwatch.ElapsedMilliseconds);
var referToTheComputedValuesSoCompilerCantOptimiseTheLoopsAway= concatkeys.Union(stringbuilderkeys).Union(cachedsbkeys).Union(formatkeys).LastOrDefault(x=>x[1]=='-');
Console.WriteLine(referToTheComputedValuesSoCompilerCantOptimiseTheLoopsAway);
我认为在大多数情况下,像这种清晰度,而不是效率,应该是您最关心的问题。除非您将大量的字符串压在一起,或者为低功率的移动设备构建一些东西,否则这可能不会对您的运行速度产生太大影响。
我发现,如果我以相当线性的方式构建字符串,那么直接连接或使用 StringBuilder 是最好的选择。如果您正在构建的大部分字符串都是动态的,我建议这样做。由于很少有文本是静态的,因此最重要的是清楚每段动态文本的放置位置,以备将来需要更新时使用。
另一方面,如果您正在谈论其中包含两个或三个变量的一大块静态文本,即使效率稍低,我认为从 string.Format 中获得的清晰度使其值得。本周早些时候,当我必须在 4 页文档的中心放置一点动态文本时,我使用了此方法。如果一大块文本是一体的,那么更新它会比更新连接在一起的三块文本更容易。
String.Format 使用 StringBuilder
在内部......从逻辑上讲,这导致了这样的想法:由于更多的开销,它的性能会稍差一些。然而,简单的字符串串联是在两个字符串之间注入一个字符串的最快方法……在很大程度上。几年前,里科·马里亚尼 (Rico Mariani) 在他的第一次绩效测验中证明了这一证据。简单的事实是,连接...当字符串部分的数量已知时(无限制...您可以连接一千个部分...只要您知道它始终是 1000 个部分)...总是比 StringBuilder
或字符串格式。它们可以通过单个内存分配和一系列内存副本来执行。 这里 就是证明
下面是一些 String.Concat 方法的实际代码,它最终调用 FillStringChecked ,它使用指针复制内存(通过 Reflector 提取):
public static string Concat(params string[] values)
{
int totalLength = 0;
if (values == null)
{
throw new ArgumentNullException("values");
}
string[] strArray = new string[values.Length];
for (int i = 0; i < values.Length; i++)
{
string str = values[i];
strArray[i] = (str == null) ? Empty : str;
totalLength += strArray[i].Length;
if (totalLength < 0)
{
throw new OutOfMemoryException();
}
}
return ConcatArray(strArray, totalLength);
}
public static string Concat(string str0, string str1, string str2, string str3)
{
if (((str0 == null) && (str1 == null)) && ((str2 == null) && (str3 == null)))
{
return Empty;
}
if (str0 == null)
{
str0 = Empty;
}
if (str1 == null)
{
str1 = Empty;
}
if (str2 == null)
{
str2 = Empty;
}
if (str3 == null)
{
str3 = Empty;
}
int length = ((str0.Length + str1.Length) + str2.Length) + str3.Length;
string dest = FastAllocateString(length);
FillStringChecked(dest, 0, str0);
FillStringChecked(dest, str0.Length, str1);
FillStringChecked(dest, str0.Length + str1.Length, str2);
FillStringChecked(dest, (str0.Length + str1.Length) + str2.Length, str3);
return dest;
}
private static string ConcatArray(string[] values, int totalLength)
{
string dest = FastAllocateString(totalLength);
int destPos = 0;
for (int i = 0; i < values.Length; i++)
{
FillStringChecked(dest, destPos, values[i]);
destPos += values[i].Length;
}
return dest;
}
private static unsafe void FillStringChecked(string dest, int destPos, string src)
{
int length = src.Length;
if (length > (dest.Length - destPos))
{
throw new IndexOutOfRangeException();
}
fixed (char* chRef = &dest.m_firstChar)
{
fixed (char* chRef2 = &src.m_firstChar)
{
wstrcpy(chRef + destPos, chRef2, length);
}
}
}
那么:
string what = "cat";
string inthehat = "The " + what + " in the hat!";
享受!
哦,还有,最快的是:
string cat = "cat";
string s = "The " + cat + " in the hat";
这确实取决于。对于很少连接的小字符串,仅附加字符串实际上会更快。
String s = "String A" + "String B";
但对于较大的字符串(非常非常大的字符串),使用 StringBuilder 会更有效。
在上述两种情况下,我想将一个或多个字符串插入到预定义模板字符串的中间。
在这种情况下,我建议 String.Format 是最快的,因为它是为此目的而设计的。
这实际上取决于您的使用模式。
之间的详细基准 string.Join
, string,Concat
和 string.Format
可以在这里找到: String.Format 不适合密集日志记录
我建议不要,因为 String.Format 不是为连接而设计的,它是为格式化各种输入(例如日期)的输出而设计的。
String s = String.Format("Today is {0:dd-MMM-yyyy}.", DateTime.Today);