腳踏實地,給自己一個更好的生活
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 不要使用空解構函式 ★
在實際情況中,許多曾在解構函式中包含處理程式碼,但後來因為種種原因被註釋掉或者刪除掉了,只留下一個空殼,此時應注意把解構函式本身註釋掉或刪除掉。
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比較。
如下面的示例:
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的效率有兩個因素:一是雜湊碼(GetHashC