.NET性能优化方面的总结
从2004年底开始接触C#到现在也有2年多的时间了,因为有C++方面的基础,对于C#,我习惯于与C++对比。现在总结一些.NET方面的性能优化方面的经验,算是对这两年多的.NET工作经历的总结。
由于使用C#时间不长,欢迎各高手补充。
标有 ★ 表示特别重要,会严重影响性能,程序中不应出现的情况。
1. C#语言方面
1.1 垃圾回收
垃圾回收解放了手工管理对象的工作,提高了程序的健壮性,但副作用就是程序代码可能对于对象创建变得随意。
1.1.1 避免不必要的对象创建
由于垃圾回收的代价较高,所以C#程序开发要遵循的一个基本原则就是避免不必要的对象创建。以下列举一些常见的情形。
1.1.1.1 避免循环创建对象 ★
如果对象并不会随每次循环而改变状态,那么在循环中反复创建对象将带来性能损耗。高效的做法是将对象提到循环外面创建。
1.1.1.2 在需要逻辑分支中创建对象
如果对象只在某些逻辑分支中才被用到,那么应只在该逻辑分支中创建对象。
1.1.1.3 使用常量避免创建对象
程序中不应出现如 new Decimal(0) 之类的代码,这会导致小对象频繁创建及回收,正确的做法是使用Decimal.Zero常量。我们有设计自己的类时,也可以学习这个设计手法,应用到类似的场景中。
1.1.1.4 使用StringBuilder做字符串连接
1.1.2 不要使用空析构函数 ★
如果类包含析构函数,由创建对象时会在 Finalize 队列中添加对象的引用,以保证当对象无法可达时,仍然可以调用到 Finalize 方法。垃圾回收器在运行期间,会启动一个低优先级的线程处理该队列。相比之下,没有析构函数的对象就没有这些消耗。如果析构函数为空,这个消耗就毫无意义,只会导致性能降低!因此,不要使用空的析构函数。
在实际情况中,许多曾在析构函数中包含处理代码,但后来因为种种原因被注释掉或者删除掉了,只留下一个空壳,此时应注意把析构函数本身注释掉或删除掉。
1.1.3 实现 IDisposable 接口
垃圾回收事实上只支持托管内在的回收,对于其他的非托管资源,例如 Window GDI 句柄或数据库连接,在析构函数中释放这些资源有很大问题。原因是垃圾回收依赖于内在紧张的情况,虽然数据库连接可能已濒临耗尽,但如果内存还很充足的话,垃圾回收是不会运行的。
C#的 IDisposable 接口是一种显式释放资源的机制。通过提供 using 语句,还简化了使用方式(编译器自动生成 try ... finally 块,并在 finally 块中调用 Dispose 方法)。对于申请非托管资源对象,应为其实现 IDisposable 接口,以保证资源一旦超出 using 语句范围,即得到及时释放。这对于构造健壮且性能优良的程序非常有意义!
为防止对象的 Dispose 方法不被调用的情况发生,一般还要提供析构函数,两者调用一个处理资源释放的公共方法。同时,Dispose 方法应调用 System.GC.SuppressFinalize(this),告诉垃圾回收器无需再处理 Finalize 方法了。
1.2 String 操作
1.2.1 使用 StringBuilder 做字符串连接
String 是不变类,使用 + 操作连接字符串将会导致创建一个新的字符串。如果字符串连接次数不是固定的,例如在一个循环中,则应该使用 StringBuilder 类来做字符串连接工作。因为 StringBuilder 内部有一个 StringBuffer ,连接操作不会每次分配新的字符串空间。只有当连接后的字符串超出 Buffer 大小时,才会申请新的 Buffer 空间。典型代码如下:StringBuilder sb = new StringBuilder( 256 );
for ( int i = 0 ; i < Results.Count; i ++ )
{
sb.Append (Results[i]);
}
如果连接次数是固定的并且只有几次,此时应该直接用 + 号连接,保持程序简洁易读。实际上,编译器已经做了优化,会依据加号次数调用不同参数个数的 String.Concat 方法。例如:
String str = str1 + str2 + str3 + str4;
会被编译为 String.Concat(str1, str2, str3, str4)。该方法内部会计算总的 String 长度,仅分配一次,并不会如通常想象的那样分配三次。作为一个经验值,当字符串连接操作达到 10 次以上时,则应该使用 StringBuilder。
这里有一个细节应注意:StringBuilder 内部 Buffer 的缺省值为 16 ,这个值实在太小。按 StringBuilder 的使用场景,Buffer 肯定得重新分配。经验值一般用 256 作为 Buffer 的初值。当然,如果能计算出最终生成字符串长度的话,则应该按这个值来设定 Buffer 的初值。使用 new StringBuilder(256) 就将 Buffer 的初始长度设为了256。
1.2.2 避免不必要的调用 ToUpper 或 ToLower 方法
String是不变类,调用ToUpper或ToLower方法都会导致创建一个新的字符串。如果被频繁调用,将导致频繁创建字符串对象。这违背了前面讲到的“避免频繁创建对象”这一基本原则。
例如,bool.Parse方法本身已经是忽略大小写的,调用时不要调用ToLower方法。
另一个非常普遍的场景是字符串比较。高效的做法是使用 Compare 方法,这个方法可以做大小写忽略的比较,并且不会创建新字符串。
还有一种情况是使用 HashTable 的时候,有时候无法保证传递 key 的大小写是否符合预期,往往会把 key 强制转换到大写或小写方法。实际上 HashTable 有不同的构造形式,完全支持采用忽略大小写的 key: new HashTable(StringComparer.OrdinalIgnoreCase)。
1.2.3 最快的空串比较方法
将String对象的Length属性与0比较是最快的方法:if (str.Length == 0)
其次是与String.Empty常量或空串比较:if (str == String.Empty)或if (str == "")
注:C#在编译时会将程序集中声明的所有字符串常量放到保留池中(intern pool),相同常量不会重复分配。
1.3 多线程
1.3.1 线程同步
线程同步是编写多线程程序需要首先考虑问题。C#为同步提供了 Monitor、Mutex、AutoResetEvent 和 ManualResetEvent 对象来分别包装 Win32 的临界区、互斥对象和事件对象这几种基础的同步机制。C#还提供了一个lock语句,方便使用,编译器会自动生成适当的 Monitor.Enter 和 Monitor.Exit 调用。
1.3.1.1 同步粒度
同步粒度可以是整个方法,也可以是方法中某一段代码。为方法指定 MethodImplOptions.Synchronized 属性将标记对整个方法同步。例如:
[MethodImpl(MethodImplOptions.Synchronized)]
public static SerialManager GetInstance()
{
if (instance == null )
{
instance = new SerialManager();
}
return instance;
}
通常情况下,应减小同步的范围,使系统获得更好的性能。简单将整个方法标记为同步不是一个好主意,除非能确定方法中的每个代码都需要受同步保护。
1.3.1.2 同步策略
使用 lock 进行同步,同步对象可以选择 Type、this 或为同步目的专门构造的成员变量。
避免锁定Type★
锁定Type对象会影响同一进程中所有AppDomain该类型的所有实例,这不仅可能导致严重的性能问题,还可能导致一些无法预期的行为。这是一个很不好的习惯。即便对于一个只包含static方法的类型,也应额外构造一个static的成员变量,让此成员变量作为锁定对象。
避免锁定 this
锁定 this 会影响该实例的所有方法。假设对象 obj 有 A 和 B 两个方法,其中 A 方法使用 lock(this) 对方法中的某段代码设置同步保护。现在,因为某种原因,B 方法也开始使用 lock(this) 来设置同步保护了,并且可能为了完全不同的目的。这样,A 方法就易做图扰了,其行为可能无法预知。所以,作为一种良好的习惯,建议避免使用 lock(this) 这种方式。
使用为同步目的专门构造的成员变量
&
补充:Web开发 , ASP.Net ,