在C#中string关键字的映射实际上指向.NET基类System.String。System.String是一个功能非常强大且用途非常广泛的基类,所以我们在用C#string的时候实际就是在用.NET Framework String。String上是一个不可变的数据类型,一旦对字符串对象进行了初始化,该字符串对象就不能改变了。表面上修改字符串内容的方法和运算符实际上创建一个新字符串,所以重复修改给定的字符串,效率会很低。所以.Net Framework定义了另一个StringBuild类以提高字符串处理的性能,但String和StringBuild之间又有什么联系呢。

以下一个示例基于版本.Net Framework2.0这个示例主要是参考重谈字符串性能,先定义一个简单性能计数器主要目的有:

(1)打印出各字符串处理方法的消耗时间

(2)CPU时钟周期

(3)执行过程中垃圾回收器回收次数

 public class CodeTimer
{
public delegate void Action();
/// <summary>
/// 初始化
/// </summary>
public static void Initialize()
{
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
Thread.CurrentThread.Priority = ThreadPriority.Highest;
Time("", , () => { });
}
public static void Time(string name, int iteration, Action action)
{
if (String.IsNullOrEmpty(name)) return; // 1.
ConsoleColor currentForeColor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(name); // 2.
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
int[] gcCounts = new int[GC.MaxGeneration + ];
for (int i = ; i <= GC.MaxGeneration; i++)
{
gcCounts[i] = GC.CollectionCount(i);
} // 3.
Stopwatch watch = new Stopwatch();
watch.Start();
ulong cycleCount = GetCycleCount();
for (int i = ; i < iteration; i++) action();
ulong cpuCycles = GetCycleCount() - cycleCount;
watch.Stop(); // 4.
Console.ForegroundColor = currentForeColor;
Console.WriteLine("\tTime Elapsed:\t" + watch.ElapsedMilliseconds.ToString("N0") + "ms");
Console.WriteLine("\tCPU Cycles:\t" + cpuCycles.ToString("N0")); // 5.
for (int i = ; i <= GC.MaxGeneration; i++)
{
int count = GC.CollectionCount(i) - gcCounts[i];
Console.WriteLine("\tGen " + i + ": \t\t" + count);
} Console.WriteLine();
} private static ulong GetCycleCount()
{
ulong cycleCount = ;
QueryThreadCycleTime(GetCurrentThread(), ref cycleCount);
return cycleCount;
} [DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool QueryThreadCycleTime(IntPtr threadHandle, ref ulong cycleTime); [DllImport("kernel32.dll")]
static extern IntPtr GetCurrentThread();
}

原文链接:一个简单的性能计数器:CodeTimer

定义一个StringListBuilder用List<string>先将所有字符串保存起来最后转化为字符串数组,再返回字符串

public class StringListBuilder
{
private List<string> m_list = new List<string>(); public StringListBuilder Append(string s)
{
this.m_list.Add(s);
return this;
} public override string ToString()
{
return String.Concat(this.m_list.ToArray());
}
}

定义一个StrPerformance类用于维护各个字符串处理的方法

 public class StrPerformance
{
private static readonly string STR = ""; public static string NormalConcat(int count)
{
var result = "";
for (int i = ; i < count; i++) result += STR;
return result;
} public static string StringBuilder(int count)
{
var builder = new StringBuilder();
for (int i = ; i < count; i++) builder.Append(STR);
return builder.ToString();
} public static string StringListBuilder(int count)
{
var builder = new StringListBuilder();
for (int i = ; i < count; i++) builder.Append(STR);
return builder.ToString();
} public static string StringConcat(int count)
{
var array = new string[count];
for (int i = ; i < count; i++) array[i] = STR;
return String.Concat(array);
}
}

用性能计数器记录各个方法执行过程并且打印出对应的参数

CodeTimer.Initialize();

            for (int i = ; i <= 2048; i *= )
{
CodeTimer.Time(
String.Format("StringListBuilder ({0})", i),
,
() => StrPerformance.StringListBuilder(i)); CodeTimer.Time(
String.Format("String concat ({0})", i),
,
() => StrPerformance.StringConcat(i));
CodeTimer.Time(
String.Format("StringBuilder ({0})", i),
,
() => StrPerformance.StringBuilder(i)); }

分析可以得出,广受追捧的StringBuilder性能似乎并不是最好的,String.Concat方法有时候有时候更适合使用。那么为什么String.Concat方法性能那么高,StringBuilder反而比StringListBuilder要差,要知道StringListBuilder还要维护一个集合,通过反编译我们看一下.NET2.0的String.Concat和StringBuilder到底是怎么实现的。

先看在.Net2.0下StringBuilder的Append和ToString方法的实现过程,Append和ToString实现过程。

// System.Text.StringBuilder
public StringBuilder Append(string value)
{
if (value == null)
{
return this;
}
string text = this.m_StringValue;
IntPtr intPtr = Thread.InternalGetCurrentThread();
if (this.m_currentThread != intPtr)
{
text = string.GetStringForStringBuilder(text, text.Capacity);
}
int length = text.Length;
int requiredLength = length + value.Length;
if (this.NeedsAllocation(text, requiredLength))
{
string newString = this.GetNewString(text, requiredLength);
newString.AppendInPlace(value, length);
this.ReplaceString(intPtr, newString);
}
else
{
text.AppendInPlace(value, length);
this.ReplaceString(intPtr, text);
}
return this;
}
public override string ToString()
{
string currentValue = this.m_currentValue; if (this.m_currentThread != Thread.InternalGetCurrentThread())
{
return string.InternalCopy(currentValue);
} // 如果这个字符串对象“太空”的话
if (( * currentValue.Length) < currentValue.ArrayLength)
{
// 则构造一个“满当”地对象
return string.InternalCopy(currentValue);
} // 将字符序列最后放一个\0
currentValue.ClearPostNullChar(); // 既然容器已经“暴露”,则设制“当前线程”的标识为Zero,
// 这意味着下次操作会生成新字符串对象(即新的容器)
this.m_currentThread = IntPtr.Zero; // 如果“还不算太空”,则返回当前对象
return currentValue;
}

StringBuilder的ToString方法比较有意思,它会判断到底是“构造一个新对象”还是就“直接返回当前容器”给你。如果直接返回当前容器,则可能会浪费较多内存,而如果构造一个新对象,则又会损耗性能。让StringBuilder做出决定的便是容器内部的字符序列占“最大容积”的比例,如果超过一半,则表明“还不算太空”,便选择“时间”,直接返回容器;否则,StringBuilder会认为还是选择“空间”较为合算,便构造一个新对象并返回,至于当前的容器便会和StringBuilder一道被GC回收了。

同时我们可以看到,如果返回了新对象,则当前容器还可以继续在Append时使用,否则Append方法便会因为m_currentValue为Zero而创建新的容器。不过,从ToString的实现中也可以看出,多次调用ToString方法一定返回新建的对象。

而String.Concat又做了什么,String类Concat的具体实现过程

public static string Concat(params string[] values)
{
int totalLength = ; if (values == null)
{
throw new ArgumentNullException("values");
} string[] arrayToConcate = new string[values.Length]; // 遍历源数组,填充拼接用的数组
for (int i = ; i < values.Length; i++)
{
string str = values[i]; // null作为空字符串对待
arrayToConcate[i] = (str == null) ? Empty : str; // 累计字符串总长度
totalLength += arrayToConcate[i].Length; // 如果越界了,抛异常
if (totalLength < )
{
throw new OutOfMemoryException();
}
} // 拼接
return ConcatArray(arrayToConcate, totalLength);
}
[MethodImpl(MethodImplOptions.InternalCall)]
private static extern string FastAllocateString(int length); private static string ConcatArray(string[] values, int totalLength)
{
// 分配目标字符串所占用的空间(即创建对象)
string dest = FastAllocateString(totalLength); int destPos = ; for (int i = ; 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* chDest = &dest.m_firstChar)
{
fixed (char* chSrc = &src.m_firstChar)
{
wstrcpy(chDest + destPos, chSrc, length);
}
}
}

由于数组中的字符串都是确定的因此事先计算出结果的长度,于是遍历源字符串数组,将它们一个一个复制(或叫做“填充”)到目标字符串的某一段位置上去,因为在此之前已经确定结果的大小,因此直接创建一个“容器”即可,剩下的只是填充数据而已。既然可以不浪费任何一寸空间,也没有任何多余的操作,这也是String.Concat高效的原因。

同样的代码移植到.Net 4.5上会不会还像之前一样String.Concat在处理连接字符串中性能最高

这次StringBuilder又重新回到了我们最初的印象中,在处理多字符串连接的时候StringBuilder是性能最高的,通过和.Net 2.0的实验结果来看StringListBuilder和String Concat的性能变化不大,而似乎StringBuilder的性能提高了一倍,那么在.NET 4.5中StringBuilder的Append方法又做了什么呢,下面我们来看一下.Net 4.5中Append的具体实现过程

 public unsafe StringBuilder Append(string value)
{
if (value != null)
{
//StringBuilder内维护的一个字符数组
char[] chunkChars = this.m_ChunkChars;
int chunkLength = this.m_ChunkLength;
int length = value.Length;
int num = chunkLength + length; //不必增加m_ChunkChars字符数组的长度
if (num < chunkChars.Length)
{
if (length <= )
{
if (length > )
{
chunkChars[chunkLength] = value[];
}
if (length > )
{
chunkChars[chunkLength + ] = value[];
}
}
else
{
fixed (string text = value)
{
char* ptr = text;
if (ptr != null)
{
ptr += RuntimeHelpers.OffsetToStringData / ;
}
fixed (char* ptr2 = &chunkChars[chunkLength])
{
string.wstrcpy(ptr2, ptr, length);
}
}
}
this.m_ChunkLength = num;
}
//增加m_ChunkChars数组的长度
else
{
this.AppendHelper(value);
}
}
return this;
}
 private unsafe void AppendHelper(string value)
{
fixed (string text = value)
{
//去字符串的地址
char* ptr = text;
if (ptr != null)
{
ptr += RuntimeHelpers.OffsetToStringData / ;
}
this.Append(ptr, value.Length);
}
} public unsafe StringBuilder Append(char* value, int valueCount)
{
if (valueCount < )
{
throw new ArgumentOutOfRangeException("valueCount", Environment.GetResourceString("ArgumentOutOfRange_NegativeCount"));
}
int num = valueCount + this.m_ChunkLength;
if (num <= this.m_ChunkChars.Length)
{
//把字符串一个一个复制到m_ChunkChars字符数组中
StringBuilder.ThreadSafeCopy(value, this.m_ChunkChars, this.m_ChunkLength, valueCount);
this.m_ChunkLength = num;
}
else
{
int num2 = this.m_ChunkChars.Length - this.m_ChunkLength;
if (num2 > )
{
StringBuilder.ThreadSafeCopy(value, this.m_ChunkChars, this.m_ChunkLength, num2);
this.m_ChunkLength = this.m_ChunkChars.Length;
}
int num3 = valueCount - num2;
this.ExpandByABlock(num3);
StringBuilder.ThreadSafeCopy(value + num2, this.m_ChunkChars, , num3);
this.m_ChunkLength = num3;
}
return this;
}

在分析代码可知在.Net 4.5StringBuilder中内部维护了一个m_ChunkChars字符数组,来避免不断扩容,不断复制的过程所造成的性能消耗,所以StringBuilder性能又成为三者中最高的一个。

看了老赵blog之后,(此处省去一千溢美之词)——只想说一句:“我对阁下的景仰有如滔滔江水,连绵不绝,又如黄河泛滥,一发而不可收拾!

从.Net版本演变看String和StringBuild性能之争的更多相关文章

  1. 从.Net版本演变看String和StringBuilder性能之争

    在C#中string关键字的映射实际上指向.NET基类System.String.System.String是一个功能非常强大且用途非常广泛的基类,所以我们在用C#string的时候实际就是在用.NE ...

  2. OpenStack调研:OpenStack是什么、版本演变、组件关系(Havana)、同类产品及个人感想

    一点调研资料,比较浅,只是觉得部分内容比较有用,记在这里: 首先,关于云计算,要理解什么是SAAS.PAAS.IAAS,这里不述:关于虚拟化,需要知道什么是Hypervisor,这里也不述: Open ...

  3. 有关string stringbuff stringbuild 的区别

    string  stringbuff stringbuild的执行效率: stringbuild>stringbuff>string String类是不可变类,任何对String的改变都会 ...

  4. String与StringBuild、StringBuffer的区别

    String与StringBuild.StringBuffer的区别相信困扰了好多新入门的JAVA程序员,而这也是笔试和面试的一道常见题型,如何全面的回答该问题,变得尤为重要. 首先我们需要清楚一点, ...

  5. Hadoop的版本演变

    Hadoop版本演变 Apache Hadoop的四大分支构成了三个系列的Hadoop版本: 0.20.X系列 主要有两个特征:Append与Security 0.21.0/0.22.X系列 整个Ha ...

  6. 当要将其他类型转成String类型时候 看String的方法

    当要将其他类型转成String类型时候 看String的方法进行转换

  7. JVM系列之:String.intern的性能

    目录 简介 String.intern和G1字符串去重的区别 String.intern的性能 举个例子 简介 String对象有个特殊的StringTable字符串常量池,为了减少Heap中生成的字 ...

  8. 工作10年后,再看String s = new String("xyz") 创建了几个对象?

    这个问题相信每个学习java的同学都不陌生,作为一个经典的面试题,到现在工作这么多年了我真是认为挺操蛋的一个问题,在网上到现在你仍然可以看见很多讨论这个问题的人,其中不乏工作很多年的人都有争论,我认为 ...

  9. 透过WinDBG的视角看String

    摘要 : 最近在博客园里面看到有人在讨论 C# String的一些特性. 大部分情况下是从CODING的角度来讨论String. 本人觉得非常好奇, 在运行时态, String是如何与这些特性联系上的 ...

随机推荐

  1. 老李推荐:第5章6节《MonkeyRunner源码剖析》Monkey原理分析-启动运行: 初始化事件源

    老李推荐:第5章6节<MonkeyRunner源码剖析>Monkey原理分析-启动运行: 初始化事件源   poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试 ...

  2. Android开发学习-view

    题记:抱着对Android开发的浓厚兴趣,加入了Study jams的线上学习小组,开启了自己的Android学习之旅.一.学习前准备:1.自己动手搭建了"Android Studio&qu ...

  3. linux awk 命令详解

    awk是一个非常棒的数字处理工具.相比于sed常常作用于一整行的处理,awk则比较倾向于将一行分为数个"字段"来处理.运行效率高,而且代码简单,对格式化的文本处理能力超强.先来一个 ...

  4. iOS 从url中获取文件名以及后缀

    //这里有一个模拟器沙盒路径(完整路径) NSString* index=@"/Users/junzoo/Library/Application Support/iPhone Simulat ...

  5. .dll 文件编写和使用

    1.基本概念 dll(dynamic-link library),动态链接库,是微软实现共享函数库的一种方式.动态链接,就是把一些常用的函数代码制作成dll文件,当某个程序调用到dll中的某个函数的时 ...

  6. Spring Boot 整合 Redis 实现缓存操作

    摘要: 原创出处 www.bysocket.com 「泥瓦匠BYSocket 」欢迎转载,保留摘要,谢谢!   『 产品没有价值,开发团队再优秀也无济于事 – <启示录> 』   本文提纲 ...

  7. JavaEE开发之SpringMVC中的路由配置及参数传递详解

    在之前我们使用Swift的Perfect框架来开发服务端程序时,聊到了Perfect中的路由配置.而在SpringMVC中的路由配置与其也是大同小异的.说到路由,其实就是将URL映射到Java的具体类 ...

  8. redis的安装和测试

    redis一直都是调用别人部署好的,近日想要自己从灵开始搭建一次.其中也生出不少枝节,与各位猿友共同分享,望少走些弯路! 1.提前准备的资源 redis安装包(本人上传到csdn不需积分即可下载): ...

  9. BackgroundWorker的DoWork方法中发生异常无法传递到RunWorkedCompleted方法

    在使用C#的BackgroundWorker时需要在UI界面上显示DoWork中发生的异常,但怎么调试都无法跳转到界面上,异常也不会传递到RunWorkerCompleted方法中(e.Error为空 ...

  10. register_sysctl_table实现内核数据交互

    作者:Younger Liu, 本作品采用知识共享署名-非商业性使用-相同方式共享 3.0 未本地化版本许可协议进行许可. Sysctl是一种用户应用来设置和获得运行时内核的配置参数的一种有效方式,通 ...