参考文献:
[原创].NET Framework:用Coding证明Application Domain的隔离性
[原创]如何改善Managed Code的Performance和Scalability系列之二:深入理解string和如何高效地使用string
[原创].Net Framework: 字符串的驻留(String Interning)
什么是string
提起这个问题,我想下面的图例可以给出一点启示:
string在本质上就是一连串的有顺序的字符集合。
打开string的Disassemble代码,我们可直击其本质:
[Serializable, ComVisible(true)]
public sealed class String : IComparable, ICloneable, IConvertible, IComparable<string>,IEnumerable<char>, IEnumerable, IEquatable<string>
{
}
结合string的定义,我们可以看出其基本的特性及其研究课题主要包括:
n 引用类型,string本质上是引用类型(class)
n 字符串恒等性。对象的封性原则决定了,String类根本没有提供一个对外的接口(方法、属性)来改变其内部的数据,而所提供的诸如SubString等方法都是创建了一个新的String的事例并返回 。证明恒等性最好的例子就是找不到一个例子可以说明我们能够改变String的内部内容。如:
static void Main(string[] args)
{
string a = "abc";
a.Replace('a', 'b');
Console.Write(a);
Console.ReadKey();
}
string的恒定性不单单是针对某一个单独的AppDomain,而是针对一个进程的。(可以参见[原创].NET Framework:用Coding证明Application Domain的隔离性 )
n 字符串驻留性。由于String是我们做到频率最高的一种类型,CLR考虑性能的提升和内存节约上,对于相同的字符串,一般不会为他们分别分配内存块,相反地,他们会共享一块内存。CLR实际上采用这个的机制来实现的:CLR内部维护着一块特殊的数据结构——我们可以把它看成是一个Hash table,这个Hash table维护者大部分创建的string(我这里没有说全部,因为有特例)。这个Hash table的Key对应的相应的string本身,而Value则是分配给这个string的内存块的引用。当CLR初始化的时候创建这个Hash table。一般地,在程序运行过程中,如果需要的创建一个string,CLR会根据这个string的Hash Code试着在Hash table中找这个相同的string,如果找到,则直接把找到的string的地址赋给相应的变量,如果没有则在托管堆中创建一个string,CLR会先在managed heap中创建该strng,并在Hash table中创建一个Key-Value Pair——Key为这个string本身,Value位这个新创建的string的内存地址,这个地址最重被赋给响应的变量。
n 密封性,由sealed关键字可见,sealed特性为实现字符串恒等性和字符串驻留机制,提供了基础保证。
n 字符串比较:以等价规则而非恒等规则进行比较。
Equals: 判断值是否相等
ReferenceEquals :判断实例相等(引用地址)。判断时要注意编译时和运行时的区别
n 常用方法:Trim()、ToLow()、Replace()、Split()、PadRight()、SubString()和Join() 格式化。
n 转移字符
n StringBuilder
n Encoding,编码。
什么是字符串驻留(String Interning)
公共语言运行库通过维护一个表来存放字符串,该表称为拘留池,它包含程序中以编程方式声明或创建的每个唯一的字符串的一个引用。因此,具有特定值的字符串的实例在系统中只有一个。
例如,如果将同一字符串分配给几个变量,运行库就会从拘留池中检索对该字符串的相同引用,并将它分配给各个变量。
基本原理可以概括为:(类似缓冲机制**个人意见**)
n CLR维护一个类似于哈希表的内部结构,用于维护对于字符串的统一管理。
n 但JIT编译时,CLR首先查找哈希表,如果没有找到匹配的字符串记录,则在托管堆中创建新的string实例,并为哈希表添加一个键值对记录;下一次查找相同string时,则只返回该记录的值给第二次创建的string对象。
n 通过这种方式,字符串驻留机制有效实现了对string的池管理,节省了大量的内存空间。
注意:并非所有的情况下字符串的驻留都会起作用。对于对一个动态创建的字符串(比如string+variable;variable+variable),这种驻留机制便不会起作用。因为对于这样的字符串,是不会被添加到内部的Hash table中的。但是对于string+string则不同,因为当这样的语句被编译成IL的时候,编译器是先把结构计算出来,然后再调用ldstr指令——而对于string+variable;variable+variable这种情况,所对应的IL指令是Concat。所以对于string+string字符串的驻留仍然有效。
虽然对于对一个动态创建的字符串(比如string+variable;variable+variable),驻留机制便不会起作用。但是我们可以手工的启用驻留机制——那就是调用定义的System.String中的静态方法Intern。这个方法接受一个字符串作为他的输入参数,返回的经过驻留处理的string。他的实现机制是:如果能在内部的Hash Table中找到传入的string,则返回对应的string引用,否则就在Hash Table添加该string对应的Entry,并返回string的引用。
关于如何高效使用string
一方面由于string的恒定性,我们不用考虑多线程的并发操作产生的线程同步问题。另一方面由于字符串的驻留,我们在对一个string对象进行加锁操作的时候,极有可能拖慢这个Application的performance,就像我们的Sample中演示的那样。而且很有可能影响到处于同一进程的其他Application,以致造成死锁。所以我们在使用锁的时候,除非万不得已,切忌对一个string进行加锁。
1. 尽量使用字符串(literal string)相加来代替字符串变量和字符创相加,因为这样可以使用现有的string操作指令进行操作和利用字符串驻留。
比如:
string s = "abc" + "def";
优于
string s = "abc";
s = s + "def";
2. 在需要的时候使用StringBuilder对string作频繁的操作:
由于string的恒定性,在我们对一个string进行某些操作的时候,比如调用ToUpper()或者ToLower()把某个string每个字符转化成大写或者小写;调用SubString()取子串;会创建一个新的string,有时候会创建一些新的临时string。这样的操作会增加内存的压力。所有在对string作频繁操作的情况下,我们会考虑使用StringBuilder来高效地操作string。StringBuilder之所以能对string操作带来更好的performance,是因为在它的内部维护一个字符数组,而不是一个string来避免string操作带来的新的string的创建。
StringBuilder是一个很好的字符累加器,我们应该充分地利用这一个功能:
StringBuilder sb = new StringBuilder();
sb.Append(str1 + str2);
最好写成
StringBuilder sb = new StringBuilder();
sb.Append(str1);
sb.Append(str2);
避免创建一个新的临时string来保存str1 + str2。
再比如下面的Code
StringBuilder sb = new StringBuilder();
sb.Append(WorkOnString1());
sb.Append(WorkOnString2());
sb.Append(WorkOnString3());
最好把WorkOnString1,WorkOnString2,WorkOnString3定义成:
WorkOnString1(StringBuilder sb)
WorkOnString2(StringBuilder sb)
WorkOnString3(StringBuilder sb)
3. 高效地进行string的比较操作
我们知道,对象之间的比较有比较Value和比较Reference之说。一般地对Reference进行比较的速度最快。对于string,在字符串驻留的前提下,我们可以把对Value的比较用Reference的比较来代替从而会使Performance提升。
此外,对于忽略大小写的比较,我们最好使用string的static方法Compare(string strA, string strB, bool ignoreCase)。也就是说:
if(str1.ToLower()==str2.ToLower())
最好写成
If(string. Compare(str1,str2,true))