.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) 這種方式。
    使用爲同步目的專門構造的成員變量
    這是推薦的作法。方式就是 new 一個 object 對象, 該對象僅僅用於同步目的。
    若是有多個方法都須要同步,而且有不一樣的目的,那麼就能夠爲些分別創建幾個同步成員變量。

    1.3.1.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.3.3.1 使用 Double Check 技術建立對象安全

  internal   IDictionary KeyTable
  {
    
 get 
     
 {
        
 if  ( this ._keyTable  ==   null )
        
 {
            
 lock  ( base ._lock)
            
 {
                
 if  ( this ._keyTable  ==   null )
                
 {
                    
 this ._keyTable  =   new  Hashtable();
                }
 

            }
 

        }
 

        
 return   this ._keyTable;
    }
 

}

建立單例對象是很常見的一種編程狀況。通常在 lock 語句後就會直接建立對象了,但這不夠安全。由於在 lock 鎖定對象以前,可能已經有多個線程進入到了第一個 if 語句中。若是不加第二個 if 語句,則單例對象會被重複建立,新的實例替代掉舊的實例。若是單例對象中已有數據不容許被破壞或者別的什麼緣由,則應考慮使用 Double Check 技術。

    1.4 類型系統
    1.4.1 避免無心義的變量初始化動做
    CLR保證全部對象在訪問前已初始化,其作法是將分配的內存清零。所以,不須要將變量從新初始化爲0、false或null。
    須要注意的是:方法中的局部變量不是從堆而是從棧上分配,因此C#不會作清零工做。若是使用了未賦值的局部變量,編譯期間即會報警。不要由於有這個印象而對全部類的成員變量也作賦值動做,二者的機理徹底不一樣!
    1.4.2 ValueType 和 ReferenceType

    1.4.2.1 以引用方式傳遞值類型參數
    值類型從調用棧分配,引用類型從託管堆分配。當值類型用做方法參數時,默認會進行參數值複製,這抵消了值類型分配效率上的優點。做爲一項基本技巧,以引用方式傳遞值類型參數能夠提升性能。
    1.4.2.2 爲 ValueType 提供 Equals 方法
    .net 默認實現的 ValueType.Equals 方法使用了反射技術,依靠反射來得到全部成員變量值作比較,這個效率極低。若是咱們編寫的值對象其 Equals 方法要被用到(例如將值對象放到 HashTable 中),那麼就應該重載 Equals 方法。性能優化

  public     struct   Rectangle
  {
    
 public   double  Length;
    
 public   double  Breadth;
    
 public   override   bool  Equals ( object  ob)
    
 {
        
 if  (ob  is  Rectangle)
            
 return  Equels ((Rectangle)ob))
        
 else 
            
 return   false ;
    }
 

    
 private   bool  Equals (Rectangle rect)
    
 {
        
 return   this .Length  ==  rect.Length  &&   this .Breadth  ==  rect.Breach;
    }
 

}


    1.4.2.3 避免裝箱和拆箱
    C#能夠在值類型和引用類型之間自動轉換,方法是裝箱和拆箱。裝箱須要從堆上分配對象並拷貝值,有必定性能消耗。若是這一過程發生在循環中或是做爲底層方法被頻繁調用,則應該警戒累計的效應。
    一種常常的情形出如今使用集合類型時。例如:服務器

  ArrayList al   =     new   ArrayList();
  for   (  int   i   =     0  ; i   <     1000  ; i  ++  )
  {
    al.Add(i);    
 //  Implicitly boxed because Add() takes an object 
 
} 

 
int   f   =   (  int  )al[  0  ];      //   The element is unboxed


    1.5 異常處理
    異常也是現代語言的典型特徵。與傳統檢查錯誤碼的方式相比,異常是強制性的(不依賴因而否忘記了編寫檢查錯誤碼的代碼)、強類型的、並帶有豐富的異常信息(例如調用棧)。
    1.5.1 不要吃掉異常
    關於異常處理的最重要原則就是:不要吃掉異常。這個問題與性能無關,但對於編寫健壯和易於排錯的程序很是重要。這個原則換一種說法,就是不要捕獲那些你不能處理的異常。
    吃掉異常是極很差的習慣,由於你消除了解決問題的線索。一旦出現錯誤,定位問題將很是困難。除了這種徹底吃掉異常的方式外,只將異常信息寫入日誌文件但並不作更多處理的作法也一樣不妥。
    1.5.2 不要吃掉異常信息
    有些代碼雖然拋出了異常,但卻把異常信息吃掉了。
    爲異常披露詳盡的信息是程序員的職責所在。若是不能在保留原始異常信息含義的前提下附加更豐富和更人性化的內容,那麼讓原始的異常信息直接展現也要強得多。千萬不要吃掉異常。
    1.5.3 避免沒必要要的拋出異常
    拋出異常和捕獲異常屬於消耗比較大的操做,在可能的狀況下,應經過完善程序邏輯避免拋出沒必要要沒必要要的異常。與此相關的一個傾向是利用異常來控制處理邏輯。儘管對於極少數的狀況,這可能得到更爲優雅的解決方案,但一般而言應該避免。
    1.5.4 避免沒必要要的從新拋出異常
    若是是爲了包裝異常的目的(即加入更多信息後包裝成新異常),那麼是合理的。可是有很多代碼,捕獲異常沒有作任何處理就再次拋出,這將無謂地增長一次捕獲異常和拋出異常的消耗,對性能有傷害。

    1.6 反射
    反射是一項很基礎的技術,它將編譯期間的靜態綁定轉換爲延遲到運行期間的動態綁定。在不少場景下(特別是類框架的設計),能夠得到靈活易於擴展的架構。但帶來的問題是與靜態綁定相比,動態綁定會對性能形成較大的傷害。
    1.6.1 反射分類
     type comparison :類型判斷,主要包括 is 和 typeof 兩個操做符及對象實例上的 GetType 調用。這是最輕型的消耗,能夠無需考慮優化問題。注意 typeof 運算符比對象實例上的 GetType 方法要快,只要可能則優先使用 typeof 運算符。
    member enumeration : 成員枚舉,用於訪問反射相關的元數據信息,例如Assembly.GetModule、Module.GetType、Type對象上的IsInterface、IsPublic、GetMethod、GetMethods、GetProperty、GetProperties、GetConstructor調用等。儘管元數據都會被CLR緩存,但部分方法的調用消耗仍很是大,不過這類方法調用頻度不會很高,因此整體看性能損失程度中等。
    member invocation:成員調用,包括動態建立對象及動態調用對象方法,主要有Activator.CreateInstance、Type.InvokeMember等。
    1.6.2 動態建立對象
    C#主要支持 5 種動態建立對象的方式:
    1. Type.InvokeMember
    2. ContructorInfo.Invoke
    3. Activator.CreateInstance(Type)
    4. Activator.CreateInstance(assemblyName, typeName)
    5. Assembly.CreateInstance(typeName)
    最快的是方式 3 ,與 Direct Create 的差別在一個數量級以內,約慢 7 倍的水平。其餘方式,至少在 40 倍以上,最慢的是方式 4 ,要慢三個數量級。
    1.6.3 動態方法調用
    方法調用分爲編譯期的早期綁定和運行期的動態綁定兩種,稱爲Early-Bound Invocation和Late-Bound Invocation。Early-Bound Invocation可細分爲Direct-call、Interface-call和Delegate-call。Late-Bound Invocation主要有Type.InvokeMember和MethodBase.Invoke,還能夠經過使用LCG(Lightweight Code Generation)技術生成IL代碼來實現動態調用。
    從測試結果看,相比Direct Call,Type.InvokeMember要接近慢三個數量級;MethodBase.Invoke雖然比Type.InvokeMember要快三倍,但比Direct Call仍慢270倍左右。可見動態方法調用的性能是很是低下的。咱們的建議是:除非要知足特定的需求,不然不要使用!
    1.6.4 推薦的使用原則
    模式
    1. 若是可能,則避免使用反射和動態綁定
    2. 使用接口調用方式將動態綁定改造爲早期綁定
    3. 使用Activator.CreateInstance(Type)方式動態建立對象
    4. 使用typeof操做符代替GetType調用
    反模式
    1. 在已得到Type的狀況下,卻使用Assembly.CreateInstance(type.FullName)

    1.7 基本代碼技巧
    這裏描述一些應用場景下,能夠提升性能的基本代碼技巧。對處於關鍵路徑的代碼,進行這類的優化仍是頗有意義的。普通代碼能夠不作要求,但養成一種好的習慣也是有意義的。
    1.7.1 循環寫法
    能夠把循環的判斷條件用局部變量記錄下來。局部變量每每被編譯器優化爲直接使用寄存器,相對於普通從堆或棧中分配的變量速度快。若是訪問的是複雜計算屬性的話,提高效果將更明顯。for (int i = 0, j = collection.GetIndexOf(item); i < j; i++)
    須要說明的是:這種寫法對於CLR集合類的Count屬性沒有意義,緣由是編譯器已經按這種方式作了特別的優化。
    1.7.2 拼裝字符串
    拼裝好以後再刪除是很低效的寫法。有些方法其循環長度在大部分狀況下爲1,這種寫法的低效就更爲明顯了:多線程

  public     static     string   ToString(MetadataKey entityKey)
  {
    
 string  str  =   "" ;
    
 object [] vals  =  entityKey.values;
    
 for  ( int  i  =   0 ; i  <  vals.Length; i ++ )
    
 {
        str 
 +=   " , "   +  vals[i].ToString();
    }
 

    
 return  str  ==   ""   ?   ""  : str.Remove( 0  1 );
}

    推薦下面的寫法:架構

  if   (str.Length   ==     0  )
    str 
  =   vals[i].ToString();
  else 
    str 
  +=     "  ,  "     +   vals[i].ToString();

    其實這種寫法很是天然,並且效率很高,徹底不須要用個Remove方法繞來繞去。
    1.7.3 避免兩次檢索集合元素
    獲取集合元素時,有時須要檢查元素是否存在。一般的作法是先調用ContainsKey(或Contains)方法,而後再獲取集合元素。這種寫法很是符合邏輯。 
但若是考慮效率,能夠先直接獲取對象,而後判斷對象是否爲null來肯定元素是否存在。對於Hashtable,這能夠節省一次GetHashCode調用和n次Equals比較。app

以下面的示例:

  public   IData GetItemByID(Guid id)
  {
    IData data1 
 =   null ;
    
 if  ( this .idTable.ContainsKey(id.ToString())
    
 {
        data1 
 =   this .idTable[id.ToString()]  as  IData;
    }
 

    
 return  data1;
}

其實徹底可用一行代碼完成:return this.idTable[id] as IData;
    1.7.4 避免兩次類型轉換
    考慮以下示例,其中包含了兩處類型轉換: 

  if   (obj   is   SomeType)
  {
    SomeType st 
 =  (SomeType)obj;
    st.SomeTypeMethod();
}

    效率更高的作法以下:

  SomeType st   =   obj   as   SomeType;
  if   (st   !=     null  )
  {
    st.SomeTypeMethod();
}


    1.8 Hashtable
    Hashtable是一種使用很是頻繁的基礎集合類型。須要理解影響Hashtable的效率有兩個因素:一是散列碼(GetHashCode方法),二是等值比較(Equals方法)。Hashtable首先使用鍵的散列碼將對象分佈到不一樣的存儲桶中,隨後在該特定的存儲桶中使用鍵的Equals方法進行查找。
良好的散列碼是第一位的因素,最理想的狀況是每一個不一樣的鍵都有不一樣的散列碼。Equals方法也很重要,由於散列只須要作一次,而存儲桶中查找鍵可能須要作屢次。從實際經驗看,使用Hashtable時,Equals方法的消耗通常會佔到一半以上。

    System.Object類提供了默認的GetHashCode實現,使用對象在內存中的地址做爲散列碼。咱們遇到過一個用Hashtable來緩存對象的例子,每次根據傳遞的OQL表達式構造出一個ExpressionList對象,再調用QueryCompiler的方法編譯獲得CompiledQuery對象。以ExpressionList對象和CompiledQuery對象做爲鍵值對存儲到Hashtable中。ExpressionList對象沒有重載GetHashCode實現,其超類ArrayList也沒有,這樣最後用的就是System.Object類的GetHashCode實現。因爲ExpressionList對象會每次構造,所以它的HashCode每次都不一樣,因此這個CompiledQueryCache根本就沒有起到預想的做用。這個小小的疏漏帶來了重大的性能問題,因爲解析OQL表達式頻繁發生,致使CompiledQueryCache不斷增加,形成服務器內存泄漏!解決這個問題的最簡單方法就是提供一個常量實現,例如讓散列碼爲常量0。雖然這會致使全部對象匯聚到同一個存儲桶中,效率不高,但至少能夠解決掉內存泄漏問題。固然,最終仍是會實現一個高效的GetHashCode方法的。    以上介紹這些Hashtable機理,主要是但願你們理解:若是使用Hashtable,你應該檢查一下對象是否提供了適當的GetHashCode和Equals方法實現。不然,有可能出現效率不高或者與預期行爲不符的狀況。

相關文章
相關標籤/搜索