C#NET性能优化总结文档格式.docx
《C#NET性能优化总结文档格式.docx》由会员分享,可在线阅读,更多相关《C#NET性能优化总结文档格式.docx(25页珍藏版)》请在冰豆网上搜索。
☑使用StringBuilder做字符串连接
String是不变类,使用+操作连接字符串将会导致创建一个新的字符串。
如果字符串连接次数不是固定的,例如在一个循环中,则应该使用StringBuilder类来做字符串连接工作。
因为StringBuilder内部有一个StringBuffer,连接操作不会每次分配新的字符串空间。
只有当连接后的字符串超出Buffer大小时,才会申请新的Buffer空间。
典型代码如下:
StringBuildersb=newStringBuilder(256);
for(inti=0;
i<
Results.Count;
i++)
{sb.Append(Results[i]);
}
如果连接次数是固定的并且只有几次,此时应该直接用+号连接,保持程序简洁易读。
实际上,编译器已经做了优化,会依据加号次数调用不同参数个数的String.Concat方法。
例如:
Stringstr=str1+str2+str3+str4;
会被编译为String.Concat(str1,str2,str3,str4)。
该方法内部会计算总的String长度,仅分配一次,并不会如通常想象的那样分配三次。
作为一个经验值,当字符串连接操作达到10次以上时,则应该使用StringBuilder。
这里有一个细节应注意:
StringBuilder内部Buffe的缺省值为16,这个值实在太小。
按StringBuilder的使用场景,Buffer肯定得重新分配。
经验值一般用256作为Buffer的初值。
当然,如果能计算出最终生成字符串长度的话,则应该按这个值来设定Buffer的初值。
使用newStringBuilder(256)就将Buffer的初始长度设为了256。
☑避免不必要的ToUpper或ToLower方法
String是不变类,调用ToUpper或ToLower方法都会导致创建一个新的字符串。
如果被频繁调用,将导致频繁创建字符串对象。
这违背了前面讲到的“避免频繁创建对象”这一基本原则。
例如,bool.Parse方法本身已经是忽略大小写的,调用时不要调用ToLower方法。
另一个非常普遍的场景是字符串比较。
高效的做法是使用Compare方法,这个方法可以做大小写忽略的比较,并且不会创建新字符串。
例:
conststringC_VALUE="
COMPARE"
;
if(String.Compare(sVariable,C_VALUE,true)==0)
{Console.Write("
SAME"
);
还有一种情况是使用HashTable的时候,有时候无法保证传递key的大小写是否符合预期,往往会把key强制转换到大写或小写方法。
实际上HashTable有不同的构造形式,完全支持采用忽略大小写的key:
newHashTable(StringComparer.OrdinalIgnoreCase)。
☑最快的空串比较方法
将String对象的Length属性与0比较是最快的方法:
if(str.Length==0)
其次是与String.Empty常量或空串比较:
if(str==String.Empty)或if(str=="
"
)
注:
C#在编译时会将程序集中声明的所有字符串常量放到保留池中(internpool),相同常量不会重复分配。
3.多线程
☑线程同步
线程同步是编写多线程程序需要首先考虑问题。
C#为同步提供了Monitor、Mutex、AutoResetEvent和ManualResetEvent对象来分别包装Win32的临界区、互斥对象和事件对象这几种基础的同步机制。
C#还提供了一个lock语句,方便使用,编译器会自动生成适当的Monitor.Enter和Monitor.Exit调用。
☑同步粒度
同步粒度可以是整个方法,也可以是方法中某一段代码。
为方法指定MethodImplOptions.Synchronized属性将标记对整个方法同步。
[MethodImpl(MethodImplOptions.Synchronized)]
publicstaticSerialManagerGetInstance()
{
if(instance==null)
{instance=newSerialManager();
returninstance;
}
通常情况下,应减小同步的范围,使系统获得更好的性能。
简单将整个方法标记为同步不是一个好主意,除非能确定方法中的每个代码都需要受同步保护。
☑同步策略
使用lock进行同步,同步对象可以选择Type、this或为同步目的专门构造的成员变量。
避免锁定Type★
锁定Type对象会影响同一进程中所有AppDomain该类型的所有实例,这不仅可能导致严重的性能问题,还可能导致一些无法预期的行为。
这是一个很不好的习惯。
即便对于一个只包含static方法的类型,也应额外构造一个static的成员变量,让此成员变量作为锁定对象。
避免锁定this
锁定this会影响该实例的所有方法。
假设对象obj有A和B两个方法,其中A方法使用lock(this)对方法中的某段代码设置同步保护。
现在,因为某种原因,B方法也开始使用lock(this)来设置同步保护了,并且可能为了完全不同的目的。
这样,A方法就被干扰了,其行为可能无法预知。
所以,作为一种良好的习惯,建议避免使用lock(this)这种方式。
使用为同步目的专门构造的成员变量这是推荐的做法。
方式就是new一个object对象,该对象仅仅用于同步目的。
如果有多个方法都需要同步,并且有不同的目的,那么就可以为些分别建立几个同步成员变量。
☑集合同步
C#为各种集合类型提供了两种方便的同步机制:
Synchronized包装器和SyncRoot属性。
//CreatesandinitializesanewArrayList
ArrayListmyAL=newArrayList();
myAL.Add("
The"
);
quick"
brown"
fox"
//CreatesasynchronizedwrapperaroundtheArrayList
ArrayListmySyncdAL=ArrayList.Synchronized(myAL);
调用Synchronized方法会返回一个可保证所有操作都是线程安全的相同集合对象。
考虑mySyncdAL[0]=mySyncdAL[0]+"
test"
这一语句,读和写一共要用到两个锁。
一般讲,效率不高。
推荐使用SyncRoot属性,可以做比较精细的控制。
☑使用ThreadStatic替代NameDataSlot★
存取NameDataSlot的Thread.GetData和Thread.SetData方法需要线程同步,涉及两个锁:
一个是LocalDataStore.SetData方法需要在AppDomain一级加锁,另一个是ThreadNative.GetDomainLocalStore方法需要在Process一级加锁。
如果一些底层的基础服务使用了NameDataSlot,将导致系统出现严重的伸缩性问题。
规避这个问题的方法是使用ThreadStatic变量。
示例如下:
publicsealedclassInvokeContext
[ThreadStatic]
privatestaticInvokeContextcurrent;
privateHashtablemaps=newHashtable();
☑多线程编程技巧
☑使用DoubleCheck技术创建对象
internalIDictionaryKeyTable
get
if(this._keyTable==null)
lock(base._lock)
{this._keyTable=newHashtable();
returnthis._keyTable;
创建单例对象是很常见的一种编程情况。
一般在lock语句后就会直接创建对象了,但这不够安全。
因为在lock锁定对象之前,可能已经有多个线程进入到了第一个if语句中。
如果不加第二个if语句,则单例对象会被重复创建,新的实例替代掉旧的实例。
如果单例对象中已有数据不允许被破坏或者别的什么原因,则应考虑使用DoubleCheck技术。
4.类型系统
☑避免无意义的变量初始化
CLR保证所有对象在访问前已初始化,其做法是将分配的内存清零。
因此,不需要将变量重新初始化为0、false或null。
需要注意的是:
方法中的局部变量不是从堆而是从栈上分配,所以C#不会做清零工作。
如果使用了未赋值的局部变量,编译期间即会报警。
不要因为有这个印象而对所有类的成员变量也做赋值动作,两者的机理完全不同。
☑ValueType和ReferenceType
以引用方式传递值类型参数
值类型从调用栈分配,引用类型从托管堆分配。
当值类型用作方法参数时,默认会进行参数值复制,这抵消了值类型分配效率上的优势。
作为一项基本技巧,以引用方式传递值类型参数可以提高性能。
为ValueType提供Equals方法
.NET默认实现的ValueType.Equals方法使用了反射技术,依靠反射来获得所有成员变量值做比较,这个效率极低。
如果我们编写的值对象其Equals方法要被用到(例如将值对象放到HashTable中),那么就应该重载Equals方法。
publicstructRectangle
publicdoubleLength;
publicdoubleBreadth;
publicoverrideboolEquals(objectob)
{
if(obisRectangle)
returnEquels((Rectangle)ob))
else
returnfalse;
privateboolEquals(Rectanglerect)
{returnthis.Length==rect.Length&
&
this.Breadth==rect.Breach;
避免装箱和拆箱
C#可以在值类型和引用类型之间自动转换,方法是装箱和拆箱。
装箱需要从堆上分配对象并拷贝值,有一定性能消耗。
如果这一过程发生在循环中或是作为底层方法被频繁调用,则应该警惕累计的效应。
一种经常的情形出现在使用集合类型时。
//避免如下操作
ArrayListal=newArrayList();
1000;
{//装箱操作
al.Add(i);
//拆箱操作
intf=(int)al[0];
但是得当心,如果你像使用引用类型那么频繁的使用一个值类型的话,值类型的优势会很快被耗尽。
比如,把一个值类型压到一个含有对象类型的群集。
这叫做装箱,很耗用处理器周期,尤其是当你的代码在把它作为值(对它进行数学运算)和把它作为引用之间来回运行时。
☑尽可能使用最合适的类型
尽可能使用最合适的类型来描述数据,从而减少类型转换。
使用泛型来创建群集和其它的数据结构,这样,在运行时,它们就可以被实例化来存储刚好合适的类型。
这节省了装箱/拆箱和类型转换的时间。
在C#中使用as,而不是is。
关键字is用来查看引用是否可以被作为某个具体的类型,但是并不返回转换到这个类型的引用。
所以,通常当你从is获得一个正的结果时,你首先应该cast——有效地执行两次cast。
采用as关键词时,如果可用,则返回cast为新类型的引用;
否则返回null。
你可以查看null然后做你喜欢做的事情。
整体来说,as方法要比is方法快50%。
5.异常处理
☑不要吃掉异常
关于异常处理的最重要原则就是:
不要吃掉异常。
这个问题与性能无关,但对于编写健壮和易于排错的程序非常重要。
这个原则换一种说法,就是不要捕获那些你不能处理的异常。
吃掉异常是极不好的习惯,因为你消除了解决问题的线索。
一旦出现错误,定位问题将非常困难。
除了这种完全吃掉异常的方式外,只将异常信息写入日志文件但并不做更多处理的做法也同样不妥。
☑不要吃掉异常信息
有些代码虽然抛出了异常,但却把异常信息吃掉了。
为异常披露详尽的信息是程序员的职责所在。
如果不能在保留原始异常信息含义的前提下附加更丰富和更人性化的内容,那么让原始的异常信息直接展示也要强得多。
千万不要吃掉异常信息。
☑避免不必要的抛出异常
抛出异常和捕获异常属于消耗比较大的操作,在可能的情况下,应通过完善程序逻辑避免抛出不必要的异常。
与此相关的一个倾向是利用异常来控制处理逻辑。
尽管对于极少数的情况,这可能获得更为优雅的解决方案,但通常而言应该避免。
☑避免不必要的重新抛出异常
如果是为了包装异常的目的(即加入更多信息后包装成新异常),那么是合理的。
但是有不少代码,捕获异常没有做任何处理就再次抛出,这将无谓地增加一次捕获异常和抛出异常的消耗,对性能有伤害。
☑捕获指定的异常不要使用通用的System.Exception
//避免
try
{}
catch(Exceptionexc)
//推荐
catch(System.NullReferenceExceptionexc)
catch(System.ArgumentOutOfRangeExceptionexc)
catch(System.InvalidCastExceptionexc)
☑要在finally里释放占用的资源
使用Try...catch...finally时,要在finally里释放占用的资源如:
连接,文件流等,不然在Catch到错误后占用的资源不能释放。
6.反射
反射是一项很基础的技术,它将编译期间的静态绑定转换为延迟到运行期间的动态绑定。
在很多场景下(特别是类框架的设计),可以获得灵活易于扩展的架构。
但带来的问题是与静态绑定相比,动态绑定会对性能造成较大的伤害。
☑反射分类
typecomparison:
类型判断,主要包括is和typeof两个操作符及对象实例上的GetType调用。
这是最轻型的消耗,可以无需考虑优化问题。
注意typeof运算符比对象实例上的GetType方法要快,只要可能则优先使用typeof运算符。
memberenumeration:
成员枚举,用于访问反射相关的元数据信息,例如Assembly.GetModule、Module.GetType、Type对象上的IsInterface、IsPublic、GetMethod、GetMethods、GetProperty、GetProperties、GetConstructor调用等。
尽管元数据都会被CLR缓存,但部分方法的调用消耗仍非常大,不过这类方法调用频度不会很高,所以总体看性能损失程度中等。
memberinvocation:
成员调用,包括动态创建对象及动态调用对象方法,主要有Activator.CreateInstance、Type.InvokeMember等。
☑动态创建对象
C#主要支持5种动态创建对象的方式:
1.Type.InvokeMember
2.ContructorInfo.Invoke
3.Activator.CreateInstance(Type)
4.Activator.CreateInstance(assemblyName,typeName)
5.Assembly.CreateInstance(typeName)
最快的是方式3,与DirectCreate的差异在一个数量级之内,约慢7倍的水平。
其他方式,至少在40倍以上,最慢的是方式4,要慢三个数量级。
☑动态方法调用
方法调用分为编译期的早期绑定和运行期的动态绑定两种,称为Early-BoundInvocation和Late-BoundInvocation。
Early-BoundInvocation可细分为Direct-call、Interface-call和Delegate-call。
Late-BoundInvocation主要有Type.InvokeMember和MethodBase.Invoke,还可以通过使用LCG(LightweightCodeGeneration)技术生成IL代码来实现动态调用。
从测试结果看,相比DirectCall,Type.InvokeMember要接近慢三个数量级;
MethodBase.Invoke虽然比Type.InvokeMember要快三倍,但比DirectCall仍慢270倍左右。
可见动态方法调用的性能是非常低下的。
我们的建议是:
除非要满足特定的需求,否则不要使用。
☑推荐的使用原则
1.如果可能,则避免使用反射和动态绑定
2.使用接口调用方式将动态绑定改造为早期绑定
3.使用Activator.CreateInstance(Type)方式动态创建对象
4.使用typeof操作符代替GetType调用
5.在已获得Type的情况下,使用Assembly.CreateInstance(type.FullName)。
7.基本代码技巧
这里描述一些应用场景下,可以提高性能的基本代码技巧。
对处于关键路径的代码,进行这类的优化还是很有意义的。
普通代码可以不做要求,但养成一种好的习惯也是有意义的。
☑循环写法
可以把循环的判断条件用局部变量记录下来。
局部变量往往被编译器优化为直接使用寄存器,相对于普通从堆或栈中分配的变量速度快。
如果访问的是复杂计算属性的话,提升效果将更明显。
for(inti=0,j=collection.GetIndexOf(item);
j;
需要说明的是:
这种写法对于CLR集合类的Count属性没有意义,原因是编译器已经按这种方式做了特别的优化。
☑拼装字符串
拼装好之后再删除是很低效的写法。
有些方法其循环长度在大部分情况下为1,这种写法的低效就更为明显了:
publicstaticstringToString(MetadataKeyentityKey)
stringstr="
object[]vals=entityKey.values;
for(inti=0;
vals.Length;
i++)
{str+="
"
+vals[i].ToString();
}
returnstr=="
?
"
:
str.Remove(0,1);
推荐下面的写法:
if(str.Length==0)
str=vals[i].ToString();
else
str+="
"
其实这种写法非常自然,而且效率很高,完全不需要用个Remove方法绕来绕去。
☑避免两次检索集合元素
获取集合元素时,有时需要检查元素是否存在。
通常的做法是先调用ContainsKey(或Contains)方法,然后再获取集合元素。
这种写法非常符合逻辑。
但如果考虑效率,可以先直接获取对象,然后判断对象是否为null来确定元素是否存在。
对于Hashtable,这可以节省一次GetHashCode调用和n次Equals比较。
如下面的示例:
publicIDataGetItemByID(Guidid)
IDatadata1=null;
if(this.idTable.ContainsKey(id.ToString())
{data1=this.idTable[id.ToString(