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比較,
如下面的示例:
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方法的,
1.9 大批量資料操作
當需要對資料庫進行大批量資料操作的時候,推薦使用分批操作的功能,比如一百萬條資料將其分為每一萬條資料進行資料庫操作,而不是每條資料回圈去進行操作,
查詢 - 分批查詢對資料庫的壓力較小,如果那一張表在這個時候可能其他地方也更新或新增 可能需要考慮增加with(NOLOCK) ,當然如果是EF 就套上讀未提交的事務(會變卡) 也可以讓查詢不加鎖,
洗掉 - 首推根據主健進行洗掉,因為資料庫根據主鍵的索引查找和洗掉資料非常快,當然分批更好,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/99121.html
標籤:C#
上一篇:OpenCvSharp+ZXing實作多個DataMatrix決議
下一篇:C#快取初步學習
