.NET性能优化方法 学习笔记 (详细整理版)

发布于:2024-04-19 ⋅ 阅读:(26) ⋅ 点赞:(0)

1. C#语言方面

1.1 垃圾回收

垃圾回收解放了手工管理对象的工作,提高了程序的健壮性,但副作用就是程序代码可能对于对象创建变得随意。

1.1.1 避免不必要的对象创建

由于垃圾回收的代价较高,所以 C#程序开发要遵循的一个基本原则就是避免 不必要的对象创建。

以下列举一些常见的情形。

如果对象并不会随每次循环而改变状态,那么在循环中反复创建对象将带来性 能损耗。高效的做法是将对象提到循环外面创建。

  • 1. 在需要逻辑分支中创建对象
  • 如果对象只在某些逻辑分支中才被用到,那么应只在该逻辑分支中创建对象。
  • 2.使用常量避免创建对象
  • 程序中不应出现如 new Decimal(0) 之类的代码,这会导致小对象频繁创建及 回收,正确的做法是使用 Decimal.Zero 常量。我们有设计自己的类时,也可以学 习这个设计手法,应用到类似的场景中。
  • 3. 使用 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 方法,这 个方法可以做大小写忽略的比较,并且不会创建新字符串。

例: const string C_VALUE = "COMPARE";

if (String.Compare(sVariable, C_VALUE, true) == 0) { Console.Write("SAME"); } 还有一种情况是使用 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 同步粒度

同步粒度可以是整个方法,也可以是方法中某一段代码。为方法指定 MethodImplOptions.Synchronized 属性将标记对整个方法同步。

例如: [MethodImpl(MethodImplOptions.Synchronized)] public static SerialManager GetInstance() { if (instance == null ) { instance = new SerialManager(); } return instance; }

通常情况下,应减小同步的范围,使系统获得更好的性能。简单将整个方法标记 为同步不是一个好主意,除非能确定方法中的每个代码都需要受同步保护。

2 同步策略

使用 lock 进行同步,同步对象可以选择 Type、this 或为同步目的专门构造 的成员变量。

避免锁定 Type★

锁定 Type 对象会影响同一进程中所有 AppDomain 该类型的所有实例,这不仅 可能导致严重的性能问题,还可能导致一些无法预期的行为。这是一个很不好的 习惯。

即便对于一个只包含 static 方法的类型,也应额外构造一个 static 的成员 变量,让此成员变量作为锁定对象。

避免锁定 this 锁定 this 会影响该实例的所有方法。

假设对象 obj 有 A 和 B 两个方法, 其中 A 方法使用 lock(this) 对方法中的某段代码设置同步保护。

现在,因为某 种原因,B 方法也开始使用 lock(this) 来设置同步保护了,并且可能为了完全不 同的目的。

这样,A 方法就被干扰了,其行为可能无法预知。所以,作为一种 良好的习惯,建议避免使用 lock(this) 这种方式。

使用为同步目的专门构造的成员变量 这是推荐的做法。

方式就是 new 一个 object 对象, 该对象仅仅用于同步目 的。 如果有多个方法都需要同步,并且有不同的目的,那么就可以为些分别建立几 个同步成员变量。

4 集合同步

C#为各种集合类型提供了两种方便的同步机制:Synchronized 包装器和 SyncRoot 属性。

// Creates and initializes a new ArrayList ArrayList myAL = new ArrayList(); myAL.Add( " The " ); myAL.Add( " quick " ); myAL.Add( " brown " ); myAL.Add( " fox " );

// Creates a synchronized wrapper around the ArrayList ArrayList mySyncdAL = ArrayList.Synchronized(myAL);

调用 Synchronized 方法会返回一个可保证所有操作都是线程安全的相同集合对 象。

考虑 mySyncdAL[0] = mySyncdAL[0] + "test" 这一语句,读和写一共要用到 两个锁。

一般讲,效率不高。

推荐使用 SyncRoot 属性,可以做比较精细的控制。

1.3.2 使用 ThreadStatic 替代 NameDataSlot 

存取 NameDataSlot 的 Thread.GetData 和 Thread.SetData 方法需要线程同 步,涉及两个锁:

一个是 LocalDataStore.SetData 方法需要在 AppDomain 一级 加锁,另一个是 ThreadNative.GetDomainLocalStore 方法需要在 Process 一级加 锁。

如果一些底层的基础服务使用了 NameDataSlot,将导致系统出现严重的伸 缩性问题。 规避这个问题的方法是使用 ThreadStatic 变量。

示例如下: public sealed class InvokeContext { [ThreadStatic] private static InvokeContext current; private Hashtable maps = new Hashtable(); }

1.3.3 多线程编程技巧

1 使用 Double Check 技术创建对象 internal IDictionary KeyTable { get { if ( this ._keyTable == null ) { lock ( base ._lock) { if ( this ._keyTable ==