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.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 方法了。
如果類包含析構函數,由創建對象時會在 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 空間。典型代碼如下:
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),相同常量不會重複分配。
會被編譯爲 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 屬性將標記對整個方法同步。例如:
線程同步是編寫多線程程序需要首先考慮問題。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.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 屬性。
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 變量。示例如下:
存取 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 技術創建對象
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.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 方法。
值類型從調用棧分配,引用類型從託管堆分配。當值類型用作方法參數時,默認會進行參數值複製,這抵消了值類型分配效率上的優勢。作爲一項基本技巧,以引用方式傳遞值類型參數可以提高性能。
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#可以在值類型和引用類型之間自動轉換,方法是裝箱和拆箱。裝箱需要從堆上分配對象並拷貝值,有一定性能消耗。如果這一過程發生在循環中或是作爲底層方法被頻繁調用,則應該警惕累計的效應。
一種經常的情形出現在使用集合類型時。例如:
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.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.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,這種寫法的低效就更爲明顯了:
這裏描述一些應用場景下,可以提高性能的基本代碼技巧。對處於關鍵路徑的代碼,進行這類的優化還是很有意義的。普通代碼可以不做要求,但養成一種好的習慣也是有意義的。
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比較。
1.7.3 避免兩次檢索集合元素
獲取集合元素時,有時需要檢查元素是否存在。通常的做法是先調用ContainsKey(或Contains)方法,然後再獲取集合元素。這種寫法非常符合邏輯。
但如果考慮效率,可以先直接獲取對象,然後判斷對象是否爲null來確定元素是否存在。對於Hashtable,這可以節省一次GetHashCode調用和n次Equals比較。
如下面的示例:
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 避免兩次類型轉換
考慮如下示例,其中包含了兩處類型轉換:
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方法的消耗一般會佔到一半以上。
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方法實現。否則,有可能出現效率不高或者與預期行爲不符的情況。
2. Ado.Net
以上介紹這些Hashtable機理,主要是希望大家理解:如果使用Hashtable,你應該檢查一下對象是否提供了適當的GetHashCode和Equals方法實現。否則,有可能出現效率不高或者與預期行爲不符的情況。
2. Ado.Net
2.1 應用Ado.net的一些思考原則
1. 根據數據使用的方式來設計數據訪問層
2. 緩存數據,避免不必要的操作
3. 使用服務帳戶進行連接
4. 必要時申請,儘早釋放
5. 關閉可關閉的資源
6. 減少往返
7. 僅返回需要的數據
8. 選擇適當的事務類型
9. 使用存儲過程
1. 根據數據使用的方式來設計數據訪問層
2. 緩存數據,避免不必要的操作
3. 使用服務帳戶進行連接
4. 必要時申請,儘早釋放
5. 關閉可關閉的資源
6. 減少往返
7. 僅返回需要的數據
8. 選擇適當的事務類型
9. 使用存儲過程
2.2 Connection
數據庫連接是一種共享資源,並且打開和關閉的開銷較大。Ado.net默認啓用了連接池機制,關閉連接不會真的關閉物理連接,而只是把連接放回到連接池中。因爲池中共享的連接資源始終是有限的,如果在使用連接後不盡快關閉連接,那麼就有可能導致申請連接的線程被阻塞住,影響整個系統的性能表現。
2.2.1 在方法中打開和關閉連接
這個原則有幾層含義:
1. 主要目的是爲了做到必要時申請和儘早釋放
2. 不要在類的構造函數中打開連接、在析構函數中釋放連接。因爲這將依賴於垃圾回收,而垃圾回收只受內存影響,回收時機不定
3. 不要在方法之間傳遞連接,這往往導致連接保持打開的時間過長
數據庫連接是一種共享資源,並且打開和關閉的開銷較大。Ado.net默認啓用了連接池機制,關閉連接不會真的關閉物理連接,而只是把連接放回到連接池中。因爲池中共享的連接資源始終是有限的,如果在使用連接後不盡快關閉連接,那麼就有可能導致申請連接的線程被阻塞住,影響整個系統的性能表現。
2.2.1 在方法中打開和關閉連接
這個原則有幾層含義:
1. 主要目的是爲了做到必要時申請和儘早釋放
2. 不要在類的構造函數中打開連接、在析構函數中釋放連接。因爲這將依賴於垃圾回收,而垃圾回收只受內存影響,回收時機不定
3. 不要在方法之間傳遞連接,這往往導致連接保持打開的時間過長
這裏強調一下在方法之間傳遞連接的危害:曾經在壓力測試中遇到過一個測試案例,當增大用戶數的時候,這個案例要比別的案例早很久就用掉連接池中的所有連接。經分析,就是因爲A方法把一個打開的連接傳遞到了B方法,而B方法又調用了一個自行打開和關閉連接的C方法。在A方法的整個運行期間,它至少需要佔用兩條連接才能夠成功工作,並且其中的一條連接佔用時間還特別長,所以造成連接池資源緊張,影響了整個系統的可伸縮性!
2.2.2 顯式關閉連接
Connection對象本身在垃圾回收時可以被關閉,而依賴垃圾回收是很不好的策略。推薦使用using語句顯式關閉連接,如下例:
Connection對象本身在垃圾回收時可以被關閉,而依賴垃圾回收是很不好的策略。推薦使用using語句顯式關閉連接,如下例:
using (SqlConnection conn = new SqlConnection(connString)) { conn.Open(); } // Dispose is automatically called on the conn variable here |
2.2.3 確保連接池啓用
Ado.net是爲每個不同的連接串建立連接池,因此應該確保連接串不會出現與具體用戶相關的信息。另外,要注意連接串是大小寫敏感的。
2.2.4 不要緩存連接
例如,把連接緩存到Session或Application中。在啓用連接池的情況下,這種做法沒有任何意義。
2.3 Command
2.3.1 使用ExecuteScalar和ExecuteNonQuery
如果想返回像Count(*)、Sum(Price)或Avg(Quantity)那樣的單值,可以使用ExecuteScalar方法。ExecuteScalar返回第一行第一列的值,將結果集作爲標量值返回。因爲單獨一步就能完成,所以ExecuteScalar不僅簡化了代碼,還提高了性能。
使用不返回行的SQL語句時,例如修改數據(INSERT、UPDATE或DELETE)或僅返回輸出參數或返回值,請使用ExecuteNonQuery。這避免了用於創建空DataReader的任何不必要處理。
2.3.2 使用Prepare
當需要重複執行同一SQL語句多次,可考慮使用Prepare方法提升效率。需要注意的是,如果只是執行一次或兩次,則完全沒有必要。例如:
Ado.net是爲每個不同的連接串建立連接池,因此應該確保連接串不會出現與具體用戶相關的信息。另外,要注意連接串是大小寫敏感的。
2.2.4 不要緩存連接
例如,把連接緩存到Session或Application中。在啓用連接池的情況下,這種做法沒有任何意義。
2.3 Command
2.3.1 使用ExecuteScalar和ExecuteNonQuery
如果想返回像Count(*)、Sum(Price)或Avg(Quantity)那樣的單值,可以使用ExecuteScalar方法。ExecuteScalar返回第一行第一列的值,將結果集作爲標量值返回。因爲單獨一步就能完成,所以ExecuteScalar不僅簡化了代碼,還提高了性能。
使用不返回行的SQL語句時,例如修改數據(INSERT、UPDATE或DELETE)或僅返回輸出參數或返回值,請使用ExecuteNonQuery。這避免了用於創建空DataReader的任何不必要處理。
2.3.2 使用Prepare
當需要重複執行同一SQL語句多次,可考慮使用Prepare方法提升效率。需要注意的是,如果只是執行一次或兩次,則完全沒有必要。例如:
cmd.CommandText = "insert into Table1 ( Col1, Col2 ) values ( @val1, @val2 )";
cmd.Parameters.Add( "@val1", SqlDbType.Int, 4, "Col1" );
cms.Parameters.Add( "@val2", SqlDbType.NChar, 50, "Col2"); cmd.Parameters[0].Value = 1;
cmd.Parameters[1].Value = "XXX"; cmd.Prepare(); cmd.ExecuteNonQuery(); cmd.Parameters[0].Value = 2;
cmd.Parameters[1].Value = "YYY"; cmd.ExecuteNonQuery(); cmd.Parameters[0].Value = 3; cmd.Parameters[1].Value = "ZZZ"; cmd.ExecuteNonQuery(); |
2.3.3 使用綁定變量 ★
SQL語句需要先被編譯成執行計劃,然後再執行。如果使用綁定變量的方式,那麼這個執行計劃就可以被後續執行的SQL語句所複用。而如果直接把參數合併到了SQL語句中,由於參數值千變萬化,執行計劃就難以被複用了。例如上面Prepare一節給出的示例,如果把參數值直接寫到insert語句中,那麼上面的四次調用將需要編譯四次執行計劃。
爲避免這種情況造成性能損失,要求一律使用綁定變量方式。
2.4 DataReader
DataReader最適合於訪問只讀的單向數據集。與DataSet不同,數據集並不全部在內存中,而是隨不斷髮出的read請求,一旦發現數據緩衝區中的數據均被讀取,則從數據源傳輸一個數據緩衝區大小的數據塊過來。另外,DataReader保持連接,DataSet則與連接斷開。
2.4.1 顯式關閉DataReader
與連接類似,也需要顯式關閉DataReader。另外,如果與DataReader關聯的Connection僅爲DataReader服務的話,可考慮使用Command對象的ExecuteReader(CommandBehavior.CloseConnection)方式。這可以保證當DataReader關閉時,同時自動關閉Connection。
2.4.2 用索引號訪問代替名稱索引號訪問屬性
從Row中訪問某列屬性,使用索引號的方式比使用名稱方式有細微提高。如果會被頻繁調用,例如在循環中,那麼可考慮此類優化。示例如下:
SQL語句需要先被編譯成執行計劃,然後再執行。如果使用綁定變量的方式,那麼這個執行計劃就可以被後續執行的SQL語句所複用。而如果直接把參數合併到了SQL語句中,由於參數值千變萬化,執行計劃就難以被複用了。例如上面Prepare一節給出的示例,如果把參數值直接寫到insert語句中,那麼上面的四次調用將需要編譯四次執行計劃。
爲避免這種情況造成性能損失,要求一律使用綁定變量方式。
2.4 DataReader
DataReader最適合於訪問只讀的單向數據集。與DataSet不同,數據集並不全部在內存中,而是隨不斷髮出的read請求,一旦發現數據緩衝區中的數據均被讀取,則從數據源傳輸一個數據緩衝區大小的數據塊過來。另外,DataReader保持連接,DataSet則與連接斷開。
2.4.1 顯式關閉DataReader
與連接類似,也需要顯式關閉DataReader。另外,如果與DataReader關聯的Connection僅爲DataReader服務的話,可考慮使用Command對象的ExecuteReader(CommandBehavior.CloseConnection)方式。這可以保證當DataReader關閉時,同時自動關閉Connection。
2.4.2 用索引號訪問代替名稱索引號訪問屬性
從Row中訪問某列屬性,使用索引號的方式比使用名稱方式有細微提高。如果會被頻繁調用,例如在循環中,那麼可考慮此類優化。示例如下:
cmd.CommandText = "select Col1, Col2 from Table1" ;
SqlDataReader dr = cmd.ExecuteReader(); int col1 = dr.GetOrdinal("Col1");
int col2 = dr.GetOrdinal("Col2"); while (dr.Read()) { Console.WriteLine( dr[col1] + "_" + dr[col2]); } |
2.4.3 使用類型化方法訪問屬性
從Row中訪問某列屬性,用GetString、GetInt32這種顯式指明類型的方法,其效率較通用的GetValue方法有細微提高,因爲不需要做類型轉換。
2.4.4 使用多數據集
部分場景可以考慮一次返回多數據集來降低網絡交互次數,提升效率。示例如下:
從Row中訪問某列屬性,用GetString、GetInt32這種顯式指明類型的方法,其效率較通用的GetValue方法有細微提高,因爲不需要做類型轉換。
2.4.4 使用多數據集
部分場景可以考慮一次返回多數據集來降低網絡交互次數,提升效率。示例如下:
cmd.CommandText = "StoredProcedureName"; // The stored procedure returns multiple result sets.
SqlDataReader dr = cmd.ExecuteReader(); while (dr.read())
// read first result set dr.NextResult();
while (dr.read()) // |
2.5 DataSet
2.5.1 利用索引加快查找行的效率
如果需要反覆查找行,建議增加索引。有兩種方式:
1. 設置DataTable的PrimaryKey
適用於按PrimaryKey查找行的情況。注意此時應調用DataTable.Rows.Find方法,一般慣用的Select方法不能利用索引。
2. 使用DataView
適用於按Non-PrimaryKey查找行的情況。可爲DataTable創建一個DataView,並通過SortOrder參數指示建立索引。此後使用Find或FindRows查找行。
3.1 減少往返行程(Reduce Round Trips)2.5.1 利用索引加快查找行的效率
如果需要反覆查找行,建議增加索引。有兩種方式:
1. 設置DataTable的PrimaryKey
適用於按PrimaryKey查找行的情況。注意此時應調用DataTable.Rows.Find方法,一般慣用的Select方法不能利用索引。
2. 使用DataView
適用於按Non-PrimaryKey查找行的情況。可爲DataTable創建一個DataView,並通過SortOrder參數指示建立索引。此後使用Find或FindRows查找行。
使用下面的方法可以減少Web服務器和Browser之間的往返行程:
1. 爲Browser啓用緩存
如果呈現的內容是靜態的或變化週期較長,應啓用Browser緩存,避免發出冗餘的http請求。
2. 緩衝頁面輸出
如果可能,則儘量緩衝頁面輸出,處理結束後再一次傳送到客戶端,這可以避免頻繁傳遞小塊內容所造成的多次網絡交互。由於這種方式在頁面處理結束之前客戶端無法看到頁面內容,因此如果一個頁面的尺寸較大的話,可考慮使用Response.Flush方法。該方法強制輸出迄今爲止在緩衝區中的內容,你應當採用合理的算法控制調用Response.Flush方法的次數。
如果可能,則儘量緩衝頁面輸出,處理結束後再一次傳送到客戶端,這可以避免頻繁傳遞小塊內容所造成的多次網絡交互。由於這種方式在頁面處理結束之前客戶端無法看到頁面內容,因此如果一個頁面的尺寸較大的話,可考慮使用Response.Flush方法。該方法強制輸出迄今爲止在緩衝區中的內容,你應當採用合理的算法控制調用Response.Flush方法的次數。
3. 使用Server.Transfer重定向請求
使用Server.Transfer方法重定向請求優於Response.Redirect方法。原因是Response.Redirect會向Broswer回送一個響應頭,在響應頭中指出重定向的URL,之後Brower使用新的URL重新發出請求。而Server.Transfer方法直接是一個簡單的服務端調用,完全沒有這些開銷!
需要注意Server.Transfer有侷限性:第一,它會跳過安全檢查;第二,只適用於在同一Web應用內的頁面間跳轉。
使用Server.Transfer方法重定向請求優於Response.Redirect方法。原因是Response.Redirect會向Broswer回送一個響應頭,在響應頭中指出重定向的URL,之後Brower使用新的URL重新發出請求。而Server.Transfer方法直接是一個簡單的服務端調用,完全沒有這些開銷!
需要注意Server.Transfer有侷限性:第一,它會跳過安全檢查;第二,只適用於在同一Web應用內的頁面間跳轉。
3.2 避免阻塞和長時間的作業
如果需要運行阻塞或長時間運行的操作,可以考慮使用異步調用的機制,以便Web服務器能夠繼續處理其它的請求。
1. 使用異步方式調用Web服務和遠程對象
只要有可能就要避免在請求的處理過程中對Web服務和遠程對象的同步調用,因爲它佔用的是的ASP.NET 線程池中的工作線程,這將直接影響Web服務器響應其它請求的能力。
如果需要運行阻塞或長時間運行的操作,可以考慮使用異步調用的機制,以便Web服務器能夠繼續處理其它的請求。
1. 使用異步方式調用Web服務和遠程對象
只要有可能就要避免在請求的處理過程中對Web服務和遠程對象的同步調用,因爲它佔用的是的ASP.NET 線程池中的工作線程,這將直接影響Web服務器響應其它請求的能力。
2. 考慮給不需要返回值的Web方法或遠程對象的方法添加OneWay屬性
這種模式能讓Web Server調用之後就立即返回。可根據實際情況決定是否使用這種方法。
這種模式能讓Web Server調用之後就立即返回。可根據實際情況決定是否使用這種方法。
3. 使用工作隊列
將作業提交到服務器上的工作隊列中。客戶端通過發送請求來輪詢作業的執行結果。
將作業提交到服務器上的工作隊列中。客戶端通過發送請求來輪詢作業的執行結果。
3.3 使用緩存
緩存能在很大程度上決定ASP.NET應用的最終性能。Asp.net支持頁面輸出緩存和頁面部分緩存,並提供Cache API,供應用程序緩存自己的數據。是否使用緩存可考慮下面的要點:
1. 識別創建與訪問代價較大的數據
2. 評估需要緩存數據的易變性
3. 評估數據的使用頻次
4. 將要緩存數據中易變數據和不變數據分離,只緩存不變數據
5. 選擇合適的緩存機制(除Asp.net Cache外,Application state和Session state也可以作爲緩存使用)
緩存能在很大程度上決定ASP.NET應用的最終性能。Asp.net支持頁面輸出緩存和頁面部分緩存,並提供Cache API,供應用程序緩存自己的數據。是否使用緩存可考慮下面的要點:
1. 識別創建與訪問代價較大的數據
2. 評估需要緩存數據的易變性
3. 評估數據的使用頻次
4. 將要緩存數據中易變數據和不變數據分離,只緩存不變數據
5. 選擇合適的緩存機制(除Asp.net Cache外,Application state和Session state也可以作爲緩存使用)
3.4 多線程
1. 避免在請求處理過程中創建線程
在執行請求的過程中創建線程是一種代價較大的操作,會嚴重影響Web Server的性能。如果後續的操作必須用線程完成,建議通過thread pool來創建/管理線程。
1. 避免在請求處理過程中創建線程
在執行請求的過程中創建線程是一種代價較大的操作,會嚴重影響Web Server的性能。如果後續的操作必須用線程完成,建議通過thread pool來創建/管理線程。
2. 不要依賴線程數據槽或線程靜態變量
由於執行請求的線程是ASP.NET thread pool中的工作線程,同一個Client的兩次請求不一定由相同的線程來處理。
由於執行請求的線程是ASP.NET thread pool中的工作線程,同一個Client的兩次請求不一定由相同的線程來處理。
3. 避免阻塞處理請求的線程
參考"避免阻塞和長時間的作業"小節。
參考"避免阻塞和長時間的作業"小節。
4. 避免異步調用
這和1的情況類似。異步調用會導致創建新的線程,增加服務器的負擔。所以,如果沒有併發的作業要執行,就不要執行異步調用。
這和1的情況類似。異步調用會導致創建新的線程,增加服務器的負擔。所以,如果沒有併發的作業要執行,就不要執行異步調用。
3.5 系統資源
1. 考慮實現資源池以提升性能
2. 明確地調用Dispose或Close釋放系統資源
3. 不要緩存或長時間佔用資源池中的資源
4. 儘可能晚的申請,儘可能早的釋放
1. 考慮實現資源池以提升性能
2. 明確地調用Dispose或Close釋放系統資源
3. 不要緩存或長時間佔用資源池中的資源
4. 儘可能晚的申請,儘可能早的釋放
3.6 頁面處理
1. 儘量減小Page的尺寸
包括縮短控件的名稱、CSS的class的名稱、去掉無謂空行和空格、禁用不需要的ViewState
2. 啓用頁面輸出的緩衝區(Buffer)
如果Buffer的機制被關閉,可以用下面的方法打開。
使用程序打開頁面輸出緩存:
Response.BufferOutput = true;
1. 儘量減小Page的尺寸
包括縮短控件的名稱、CSS的class的名稱、去掉無謂空行和空格、禁用不需要的ViewState
2. 啓用頁面輸出的緩衝區(Buffer)
如果Buffer的機制被關閉,可以用下面的方法打開。
使用程序打開頁面輸出緩存:
Response.BufferOutput = true;
使用@Page開關打開頁面輸出緩衝機制:
<%@ Page Buffer = "true" %>
<%@ Page Buffer = "true" %>
使用Web.config或Machine.config配置文件的<pages>節點:
<pages buffer="true" …>
3. 利用Page.IsPostBack優化頁面輸出
4. 通過分離頁面的不同的內容,來提高緩存效率和減少呈現的時間
5. 優化複雜和代價較大的循環
6. 合理利用客戶端的計算資源,將一些操作轉移到客戶端進行
<pages buffer="true" …>
3. 利用Page.IsPostBack優化頁面輸出
4. 通過分離頁面的不同的內容,來提高緩存效率和減少呈現的時間
5. 優化複雜和代價較大的循環
6. 合理利用客戶端的計算資源,將一些操作轉移到客戶端進行
3.7 ViewState
ViewState是Asp.net爲服務端控件在頁面回傳之間跟蹤狀態信息而設計的一種機制。
1. 關閉ViewState
如果不需要跟蹤頁面狀態,例如頁面不會 回傳(PostBack)、不需要處理服務端控件事件或者每次頁面刷新時都會重新計算控件內容,那麼就不需要用ViewState來記錄頁面狀態了。可以對特定的WebControl設置EnableViewState屬性,也可以在頁面一級設置:
<%@ Page EnableViewState="false" %>
ViewState是Asp.net爲服務端控件在頁面回傳之間跟蹤狀態信息而設計的一種機制。
1. 關閉ViewState
如果不需要跟蹤頁面狀態,例如頁面不會 回傳(PostBack)、不需要處理服務端控件事件或者每次頁面刷新時都會重新計算控件內容,那麼就不需要用ViewState來記錄頁面狀態了。可以對特定的WebControl設置EnableViewState屬性,也可以在頁面一級設置:
<%@ Page EnableViewState="false" %>
2. 在恰當的時間點初始化控件屬性
ASP.NET的控件在執行構造函數、初始化的期間設置的屬性不會被跟蹤變化;而在初始化階段之後對屬性的修改都會被跟蹤,並最終記錄到IE頁面的__VIEWSTATE之中。所以,選擇合理的初始化控件屬性的執行點,能有效的減小頁面尺寸。
ASP.NET的控件在執行構造函數、初始化的期間設置的屬性不會被跟蹤變化;而在初始化階段之後對屬性的修改都會被跟蹤,並最終記錄到IE頁面的__VIEWSTATE之中。所以,選擇合理的初始化控件屬性的執行點,能有效的減小頁面尺寸。
3. 謹慎選擇放到ViewState中的內容
放到ViewState中的內容會被序列化/反序列化,Asp.net爲String、Integer、Boolean等基本類型的序列化做了優化,如果Array、ArrayList、HashTable存儲的是基本類型效率也較高,但其它類型則需要提供類型轉換器(Type Converter),否則將使用代價昂貴的二進制序列化程序。
放到ViewState中的內容會被序列化/反序列化,Asp.net爲String、Integer、Boolean等基本類型的序列化做了優化,如果Array、ArrayList、HashTable存儲的是基本類型效率也較高,但其它類型則需要提供類型轉換器(Type Converter),否則將使用代價昂貴的二進制序列化程序。
4.1 JScript性能優化的基本原則
1. 儘可能少地減少執行次數。畢竟對解釋語言來說,每一個執行步驟,都需要和解釋引擎做一次交互。
2. 儘可能使用語言內置的功能,比如串鏈接。
3. 儘可能使用系統提供的API來進行優化。因爲這些API是編譯好的二進制代碼,執行效率很高。
4. 書寫最正確的代碼。容錯功能是要付出性能代價的。
1. 儘可能少地減少執行次數。畢竟對解釋語言來說,每一個執行步驟,都需要和解釋引擎做一次交互。
2. 儘可能使用語言內置的功能,比如串鏈接。
3. 儘可能使用系統提供的API來進行優化。因爲這些API是編譯好的二進制代碼,執行效率很高。
4. 書寫最正確的代碼。容錯功能是要付出性能代價的。
4.2 JScript語言本身的優化
4.2.1 變量
1. 儘量使用局部變量。
因爲全局變量其實是全局對象的成員,而局部變量在棧上定義,優先查找,性能相對於全局變量要高。
4.2.1 變量
1. 儘量使用局部變量。
因爲全局變量其實是全局對象的成員,而局部變量在棧上定義,優先查找,性能相對於全局變量要高。
2. 儘量在一個語句中做定義變量和賦值。
3. 省略不必要的變量定義。
如果變量的定義可以被一個常量替代,就直接使用常量。
如果變量的定義可以被一個常量替代,就直接使用常量。
4. 使用Object語法對對象賦值。
Object的賦值語法在操作複雜對象時效率更高。
例如,可以將下面的代碼:
Object的賦值語法在操作複雜對象時效率更高。
例如,可以將下面的代碼:
car = new Object();
car.make = "Honda"; car.model = "Civic"; car.transmission = "manual"; car.miles = 100000; car.condition = "needs work"; 替換成: car = { make: "Honda", model: "Civic", transmission: "manual", miles: 100000, condition: "needs work" } |
4.2.2 對象緩存
1. 緩存對象查找的中間結果。
因爲JavaScript的解釋性,所以a.b.c.d.e,需要進行至少4次查詢操作,先檢查a再檢查a中的b,再檢查b中的c,如此往下。所以如果這樣的表達式重複出現,只要可能,應該儘量少出現這樣的表達式,可以利用局部變量,把它放入一個臨時的地方進行查詢。
1. 緩存對象查找的中間結果。
因爲JavaScript的解釋性,所以a.b.c.d.e,需要進行至少4次查詢操作,先檢查a再檢查a中的b,再檢查b中的c,如此往下。所以如果這樣的表達式重複出現,只要可能,應該儘量少出現這樣的表達式,可以利用局部變量,把它放入一個臨時的地方進行查詢。
2. 緩存創建時間較長的對象。
自定義高級對象和Date、RegExp對象在構造時都會消耗大量時間。如果可以複用,應採用緩存的方式。
自定義高級對象和Date、RegExp對象在構造時都會消耗大量時間。如果可以複用,應採用緩存的方式。
4.2.3 字符串操作
1. 使用"+=" 追加字符串,使用"+"來連接字符串。
如果是追加字符串,最好使用s+=anotherStr操作,而不是要使用s=s+anotherStr。
如果要連接多個字符串,應該使用"+",如:
s+=a;
s+=b;
s+=c;
應該寫成
s+=a + b + c;
1. 使用"+=" 追加字符串,使用"+"來連接字符串。
如果是追加字符串,最好使用s+=anotherStr操作,而不是要使用s=s+anotherStr。
如果要連接多個字符串,應該使用"+",如:
s+=a;
s+=b;
s+=c;
應該寫成
s+=a + b + c;
2. 連接大量的字符串,應使用Array的join方法。
如果是收集字符串,最好使用JavaScript數組緩存,最後使用join方法連接起來,如下:
如果是收集字符串,最好使用JavaScript數組緩存,最後使用join方法連接起來,如下:
var buf = new Array();
for (var i = 0; i < 100; i++) { buf.push(i.toString()); } var all = buf.join(""); |
4.2.4 類型轉換
1. 使用Math.floor()或者Math.round()將浮點數轉換成整型。
浮點數轉換成整型,這個更容易出錯,很多人喜歡使用parseInt(),其實parseInt()是用於將字符串轉換成數字,而不是浮點數和整型之間的轉換,我們應該使用Math.floor()或者Math.round()。
對象查找中的問題不一樣,Math是內部對象,所以Math.floor()其實並沒有多少查詢方法和調用的時間,速度是最快的。
1. 使用Math.floor()或者Math.round()將浮點數轉換成整型。
浮點數轉換成整型,這個更容易出錯,很多人喜歡使用parseInt(),其實parseInt()是用於將字符串轉換成數字,而不是浮點數和整型之間的轉換,我們應該使用Math.floor()或者Math.round()。
對象查找中的問題不一樣,Math是內部對象,所以Math.floor()其實並沒有多少查詢方法和調用的時間,速度是最快的。
2. 自定義的對象,推薦定義和使用toString()方法來進行類型轉換。
對於自定義的對象,如果定義了toString()方法來進行類型轉換的話,推薦顯式調用toString()。因爲內部的操作在嘗試所有可能性之後,會嘗試對象的toString()方法嘗試能否轉化爲String,所以直接調用這個方法效率會更高。
對於自定義的對象,如果定義了toString()方法來進行類型轉換的話,推薦顯式調用toString()。因爲內部的操作在嘗試所有可能性之後,會嘗試對象的toString()方法嘗試能否轉化爲String,所以直接調用這個方法效率會更高。
4.2.5 循環的優化
1. 儘可能少使用for(in)循環。
在JavaScript中,我們可以使用for(;;),while(),for(in)三種循環,事實上,這三種循環中for(in)的效率極差,因爲他需要查詢散列鍵,只要可以就應該儘量少用。
1. 儘可能少使用for(in)循環。
在JavaScript中,我們可以使用for(;;),while(),for(in)三種循環,事實上,這三種循環中for(in)的效率極差,因爲他需要查詢散列鍵,只要可以就應該儘量少用。
2. 預先計算collection的length。
如:將for (var i = 0; i < collection.length; i++)
替換成:for (var i = 0, len = collection.length; i < len; i++)
效果會更好,尤其是在大循環中。
如:將for (var i = 0; i < collection.length; i++)
替換成:for (var i = 0, len = collection.length; i < len; i++)
效果會更好,尤其是在大循環中。
3. 儘量減少循環內的操作。
循環內的每個操作,都會被放大爲循環次數的倍數。所以,大循環內微小的改進,在性能的整體提升上都是可觀的。
循環內的每個操作,都會被放大爲循環次數的倍數。所以,大循環內微小的改進,在性能的整體提升上都是可觀的。
4. 使用循環替代遞歸。
相比循環,遞歸的效率更差一些。遞歸的優點是在形式上更自然一些。所以,在不影響代碼的維護性的前提下,用循環替代遞歸。
相比循環,遞歸的效率更差一些。遞歸的優點是在形式上更自然一些。所以,在不影響代碼的維護性的前提下,用循環替代遞歸。
4.2.6 其它方面
1. 儘量使用語言內置的語法。
"var arr = […];"和"var arr = new Array(…);"是等效的,但是前者的效能優於後者。同樣,"var foo = {};"的方式也比"var foo = new Object();"快;"var reg = /../;"要比"var reg=new RegExp()"快。
1. 儘量使用語言內置的語法。
"var arr = […];"和"var arr = new Array(…);"是等效的,但是前者的效能優於後者。同樣,"var foo = {};"的方式也比"var foo = new Object();"快;"var reg = /../;"要比"var reg=new RegExp()"快。
2. 儘量不要使用eval。
使用eval,相當於在運行時再次調用解釋引擎,對傳入的內容解釋運行,需要消耗大量時間。
使用eval,相當於在運行時再次調用解釋引擎,對傳入的內容解釋運行,需要消耗大量時間。
3. 使用prototype代替closure。
使用closure在性能和內存消耗上都是不利的。如果closure使用量過大,這就會成爲一個問題。所以,儘量將:
this.methodFoo = function()
替換成:
MyClass.protoype.methodFoo = function()
和closure存在於對象實例之中不同,prototype存在於類中,被該類的所有的對象實例共享。
使用closure在性能和內存消耗上都是不利的。如果closure使用量過大,這就會成爲一個問題。所以,儘量將:
this.methodFoo = function()
替換成:
MyClass.protoype.methodFoo = function()
和closure存在於對象實例之中不同,prototype存在於類中,被該類的所有的對象實例共享。
4. 避免使用with語句。
With語句臨時擴展對象查找的範圍,節省了文字的錄入時間,但付出了更多的執行時間。因爲每個給出的名稱都要在全局範圍查找。所以,可以將下面的代碼:
With語句臨時擴展對象查找的範圍,節省了文字的錄入時間,但付出了更多的執行時間。因爲每個給出的名稱都要在全局範圍查找。所以,可以將下面的代碼:
with (document.formname)
{ field1.value = "one"; field2.value = "two"; } 變更爲: var form = document.formname; form.field1.value = "one"; form.field2.value = "two"; |
4.3 DOM相關
4.3.1 創建DOM節點
相比較通過document.write來給頁面生成內容,找一個容器元素(比如指定一個div或者span)並設置他們的innerHTML效率更高。
而設置innerHTML的方式比通過createElement方法創建節點的效率更高。事實上,設置元素的innerHTML是創建節點效率最高的一種方式。
如果必須使用createElement方法,而如果文檔中存在現成的樣板節點,應該是用cloneNode()方法。因爲使用createElement()方法之後,你需要設置多次元素的屬性,使用cloneNode()則可以減少屬性的設置次數。同樣,如果需要創建很多元素,應該先準備一個樣板節點。
4.3.1 創建DOM節點
相比較通過document.write來給頁面生成內容,找一個容器元素(比如指定一個div或者span)並設置他們的innerHTML效率更高。
而設置innerHTML的方式比通過createElement方法創建節點的效率更高。事實上,設置元素的innerHTML是創建節點效率最高的一種方式。
如果必須使用createElement方法,而如果文檔中存在現成的樣板節點,應該是用cloneNode()方法。因爲使用createElement()方法之後,你需要設置多次元素的屬性,使用cloneNode()則可以減少屬性的設置次數。同樣,如果需要創建很多元素,應該先準備一個樣板節點。
4.3.2 離線操作大型的DOM樹
在添加一個複雜的DOM樹時,可以先構造,構造結束後再將其添加到DOM數的適當節點。這能夠節省界面刷新的時間。
在添加一個複雜的DOM樹時,可以先構造,構造結束後再將其添加到DOM數的適當節點。這能夠節省界面刷新的時間。
同樣,在準備編輯一個複雜的樹時,可以先將樹從DOM樹上刪除,等編輯結束後再添加回來。
4.3.3 對象查詢
使用[""]查詢要比.item()更快。調用.item()增加了一次查詢和函數的調用。
4.3.3 對象查詢
使用[""]查詢要比.item()更快。調用.item()增加了一次查詢和函數的調用。
4.3.4 定時器
如果針對的是不斷運行的代碼,不應該使用setTimeout,而應該用setInterval。setTimeout每次要重新設置一個定時器。
如果針對的是不斷運行的代碼,不應該使用setTimeout,而應該用setInterval。setTimeout每次要重新設置一個定時器。
4.4 其他
1. 儘量減小文件尺寸。
將JScript文件中無關的空行、空格、註釋去掉,有助於減小JS文件的尺寸,提高下載的時間。(可以通過工具來支持代碼發佈)
2. 儘量不要在同一個Page內同時引用JScript和VBScript引擎
3. 將Page內的JScript移入到單獨的JS文件中。
4. 將Page內的JScript放置在Page的最下面,有助於提高頁面的響應速度。
5. 利用cache,減少JScript文件的下載次數
6. 在HTML內書寫JScript文件的URL時,注意統一大小寫。這樣可以利用前面URL緩存的文件。
7. 推薦使用JScript Lint檢查Javascript代碼。畢竟,對JScript引擎來說,最容易理解的JScript代碼,執行的效率也就最高。
1. 儘量減小文件尺寸。
將JScript文件中無關的空行、空格、註釋去掉,有助於減小JS文件的尺寸,提高下載的時間。(可以通過工具來支持代碼發佈)
2. 儘量不要在同一個Page內同時引用JScript和VBScript引擎
3. 將Page內的JScript移入到單獨的JS文件中。
4. 將Page內的JScript放置在Page的最下面,有助於提高頁面的響應速度。
5. 利用cache,減少JScript文件的下載次數
6. 在HTML內書寫JScript文件的URL時,注意統一大小寫。這樣可以利用前面URL緩存的文件。
7. 推薦使用JScript Lint檢查Javascript代碼。畢竟,對JScript引擎來說,最容易理解的JScript代碼,執行的效率也就最高。