性能主要指兩個方面:內存消耗和執行速度。性能優化簡而言之,就是在不影響系統運行正確性的前提下,使之運行地更快,完成特定功能所需的時間更短。緩存
本文以.NET平臺下的控件產品MultiRow爲例,描述C#性能優化的實踐。性能優化
性能優化原則數據結構
· 理解需求多線程
MultiRow的一個性能需求是:「百萬行數據綁定下平滑滾動。」整個MultiRow項目的開發過程一直在考慮這個目標。異步
· 理解瓶頸ide
99%的性能消耗是因爲1%的代碼形成的。大部分性能優化都是針對這1%的瓶頸代碼進行的。具體實施也就分爲兩步:「發現瓶頸」和「消除瓶頸」。函數
· 切忌過分工具
性能優化自己是有成本的。這個成本不僅僅體如今作性能優化所付出的工做量,還包括爲性能優化而寫出複雜的代碼致使額外的維護成本,好比引入新的Bug,額外的內存開銷等。性能優化經常須要在收益和成本之間作出權衡。性能
如何發現性能瓶頸
性能優化的第一步是發現性能瓶頸,下面是一些定位性能瓶頸的實踐。
· 如何獲取內存消耗
如下代碼能夠獲取某個操做的內存消耗。
long start = GC.GetTotalMemory(true); // 在這裏寫須要被測試內存消耗的代碼,例如,建立一個GcMultiRow var gcMulitRow1 = new GcMultiRow(); GC.Collect(); // 確保全部內存都被GC回收 GC.WaitForFullGCComplete(); long end = GC.GetTotalMemory(true); long useMemory = end - start;
· 如何獲取時間消耗
如下代碼能夠獲取某個操做時間消耗。
System.Diagnostics.Stopwatch watch = new System.Diagnostics.Stopwatch(); watch.Start(); for (int i = 0; i < 1000; i++) { gcMultiRow1.Sort(); } watch.Stop(); var useTime = (double)watch.ElapsedMilliseconds / 1000;
爲了得到更加穩定的時間消耗,這裏把一個操做循環執行了1000次,取時間消耗的平均值以排除不穩定數據。
· ANTS Performance Profiler
ANTS Performance Profiler是款功能強大的性能檢測軟件。熟練使用這個工具,咱們能夠快速準確的定位到有性能問題的代碼。這是一款收費軟件,會在IL中加入一些鉤子用來記錄時間,因此在分析時,軟件的執行速度會比實際運行慢一些,得到的數據也所以並非百分之百的準確,還要結合其餘技巧來分析程序的性能。
· CodeReview
CodeReview是發現性能問題的最後手段。CodeReview應該對產品的性能瓶頸儘量多的關注,確保該部分邏輯執行的儘量的快。
性能優化的方法和技巧
定位了性能問題後,解決的辦法有不少。下面是一些性能優化的技巧和實踐。
· 優化程序結構
在設計時就應該考慮產品結構是否能夠達到性能需求。若是後期發現了性能問題,調整結構會帶來很是大的開銷。
例如:
GcMultiRow要支持100萬行數據。假設每行有10列的話,就須要有1000萬個單元格,每一個單元格上又有不少的屬性。若是不作任何優化,大數據量時,一個GcMultiRow軟件的內存開銷會至關的大。GcMultiRow採用的方案是使用哈希表來存儲行數據:只有用戶改過的行放到哈希表裏,大部分沒有改過的行都直接使用模板代替。這就達到了節省內存的目的。
WPF平臺和Silverlight平臺的畫法和Winform平臺不一樣,是經過組合Visual元素的方法實現的。SpreadGrid for WPF產品一樣支持百萬級的數據量,可是又不能給每一個單元格都分配一個View。因此SpreadGrid使用了VirtualizingPanel來實現畫法。思路是每個Visual是一個Cell的展現模塊,能夠和Cell的數據模塊分離,這樣就只須要爲顯示出來的Cell建立Visual。當發生滾動時會有一部分Cell滾出屏幕,有一部分Cell滾入屏幕。這時,讓滾出屏幕的Cell和Visual分離,而後再複用這部分Visual給新進入屏幕的Cell。如此循環,就只須要幾百個Visual就能夠支持不少的Cell。
· 緩存
緩存(Cache)是性能優化中最經常使用的手段,針對須要頻繁的獲取一些數據,同時每次獲取數據須要的時間比較長的場景。若是使用了緩存的優化方法,須要特別注意緩存數據的同步:若是真實的數據發生了變化,應該及時的清除緩存數據,確保不會由於緩存而使用了錯誤的數據。
使用緩存的狀況比較多。最簡單的狀況就是緩存到一個Field或臨時變量裏。
for(int i = 0; i < gcMultiRow.RowCount; i++) { // Do something; }
以上代碼通常狀況下是沒有問題的,可是,若是GcMultiRow的行數比較大。而RowCount屬性的取值又比較慢的時候,就須要使用緩存來作性能優化。
int rowCount = gcMultiRow.RowCount; for (int i = 0; i < rowCount; i++) { // Do something; }
使用對象池也是一個常見的緩存方案,比使用Field或臨時變量稍微複雜一點。例如,在MultiRow中,畫邊線,畫背景,須要用到大量的Brush和Pen。這些GDI對象每次用以前要建立,用完後要銷燬。建立和銷燬的過程是比較慢的。GcMultiRow使用的方案是建立一個GDIPool。本質上是一些Dictionary,使用顏色作Key。因此只有第一次取的時候須要建立,之後就直接使用之前建立好的。
如下是GDIPool的代碼:
public static class GDIPool { Dictionary<Color, Brush > _cacheBrush = new Dictionary<Color, Brush>(); Dictionary<Color, Pen> _cachePen = new Dictionary<Color, Pen>(); public static Pen GetPen(Color color) { Pen pen; if_cachePen.TryGetValue(color, out pen)) { return pen; } pen = new Pen(color); _cachePen.Add(color, pen); return pen; } }
· 懶構造
大多時候,對於建立須要花費較長時間的對象,每每並非全部的場景下都須要使用。這時,使用懶構造的方法能夠有效提升程序啓動性能。
舉例來講,對象A須要內部建立對象B。對象B的構造時間比較長。 通常作法:
public class A { public B _b = new B(); }
通常作法下,因爲構造對象A的同時要構造對象B,致使A的構造速度也變慢了。
優化作法:
public class A { private B _b; public B BProperty { get { if(_b == null) { _b = new B(); } return _b; } } }
優化後,構造A的時候就不須要建立B對象,有效的提升了A的構造性能。
· 優化算法
優化算法能夠有效的提升特定操做的性能。使用一種算法時應該瞭解算法的適用狀況、最好狀況和最壞狀況。 以GcMultiRow爲例,最初MultiRow的排序算法使用了經典的快速排序算法。這看起來是沒有問題的。可是,對於表格軟件,用戶常常的操做是對有序表進行排序,如順序和倒序之間切換。而經典的快速排序算法的最差狀況就是基本有序的狀況。因此經典快速排序算法不適合MultiRow。
改進的快速排序算法使用了3箇中點來代替經典快排的一箇中點的算法,每次交換都是從3箇中點中選擇中間值。這樣,亂序和基本有序的狀況都不是這個算法的最壞狀況,從而優化了性能。
· 正確的使用既有數據結構
咱們如今工做的.NET framework平臺有不少現成的數據結構。咱們應該瞭解這些數據結構,提高咱們程序的性能。
例如:
1. String的加運算符和StringBuilder: 字符串的操做是咱們常常遇到的基本操做之一。 咱們常常會寫這樣的代碼 string str = str1 + str2。當操做的字符串不多的時候,這樣的操做沒有問題。可是若是大量操做的時候(例如文本文件的Save/Load, Asp.net的Render),這樣作就會帶來嚴重的性能問題。這時,咱們就應該用StringBuilder來代替string的加操做。
2. Dictionary和List: Dictionary和List是最經常使用的兩種集合類。選擇正確的集合類能夠很大的提高程序的性能。爲了作出正確的選擇,咱們應該對Dictionary和List的各類操做的性能比較瞭解。 下表中粗略的列出了兩種數據結構的性能比較。
操做
List
Dictionary
索引
快
慢
Find(Contains)
慢
快
Add
快
慢
Insert
慢
快
Remove
慢
快
3. TryGetValue: 對於Dictionary的取值,比較直接的方法是以下代碼:
if(_dic.ContainKey("Key") { return _dic["Key"]; }
當須要大量取值的時候,這樣的取法會帶來性能問題。優化方法以下:
object value; if(_dic.TryGetValue("Key", out value)) { return value; }
後一種用法要比前一種用法取值性能提升一倍。
4. 爲Dictionary選擇合適的Key: Dictionary的取值性能很大狀況下取決於作Key的對象的Equals和GetHashCode兩個方法的性能。若是能夠的話,使用Int作Key性能最好。若是是一個自定義的Class作Key的話,最好保證如下兩點:1. 不一樣對象的GetHashCode重複率低。2. GetHashCode和Equals方法簡單,效率高。
5. List的Sort和BinarySearch性能很好,若是能知足功能需求,推薦直接使用。
List<int> list = new List<int>{3, 10, 15};list.BinarySearch(10); // 對於存在的值,結果是1
list.BinarySearch(8); // 對於不存在的值,會使用負數表示位置,
// 如查找8時,結果是-2, 查找0結果是-1,查找100結果是-4.
· 經過異步提高響應時間
1. 多線程
有些操做確實須要花費比較長的時間。在處理的過程當中,若是用戶進行操做時失去響應,這個用戶體驗是不好的。使用多線程技術能夠解決這個問題。例如,有一個相似Excel的計算引擎,在構造的時候要初始化全部的函數定義。因爲函數比較多,初始化時間會比較長。這是若是用到了多線程,在工做線程中作函數定義進行的初始化,就不會影響到UI線程快速響應用戶的其餘操做了。
代碼以下:
public CalcParser() { if (_functions == null) { lock (_obtainFunctionLocker) { if (_functions == null) { System.Threading.ThreadPool.QueueUserWorkItem((s) => { if (_functions == null) { lock (_obtainFunctionLocker) { if (_functions == null) { _functions = EnsureFunctions(); } } } }); } } } }
這裏比較慢的操做就是EnsureFunctions函數,是在另外一個線程裏執行的,不會影響主線程的響應。固然,使用多線程是一個比較有難度的方案,須要充分考慮跨線程訪問和死鎖的問題。
2. 加延遲時間
在GcMultiRow實現AutoFilter功能的時候使用了一個相似於延遲執行的方案來提高響應速度。AutoFilter的功能是用戶在輸入的過程當中根據用戶的輸入更新篩選的結果。數據量大的時候一次篩選須要較長時間,會致使用戶輸入不流暢,體驗很差。使用多線程雖然是個好方案,可是會增長程序的複雜度。MultiRow的解決方案是當接收到用戶的鍵盤輸入消息的時候,並不當即出發Filter,而是等待0.3秒。若是用戶連續輸入,會在這0.3秒內再次收到鍵盤消息,放棄上一個任務,再等0.3秒,直到連續0.3秒內沒有新的鍵盤消息時再觸發Filter。這樣就實現了比較流暢的用戶體驗。
3. Application.Idle事件
在GcMultiRow的Designer裏,常常要根據當前的狀態刷新ToolBar上按鈕的Disable/Enable狀態,一次刷新須要較長的時間。這個又一次影響了用戶輸入的流暢性。GcMultiRow的優化方案是經過系統的Application.Idle事件,僅當系統空閒的時候處理刷新邏輯。接到這個事件時,通常都是用戶已經完成了連續的輸入,這時就能夠從容的刷新按鈕的狀態了。
4. Refresh, BeginInvoke
平臺自己也提供了一些異步方案。例如在WinForm下觸發一塊區域重畫的時候,調用Refresh方法不會致使當即重畫,而是設置Invalidate標記,觸發異步的刷新。在控件開發中,這個技巧能夠有效的提升產品的性能,同時簡化實現複雜度。
Control.BeginInvoke方法能夠被用來觸發異步的自定義行爲。
· 進度條,提高用戶體驗
有時候,以上提到的方案都沒有辦法快速響應用戶操做。進度條、一直轉圈圈的圖片、提示性文字(如"你的操做可能須要較長時間,請耐心等待")等,均可以有效的提高用戶體驗,能夠做爲最後方案來考慮。