C#性能优化实践
性能是考量一個軟件產品好壞的重要指標,與產品的功能有著同等重要的地位。用戶在選擇一款軟件產品的時候基本都會親身試驗比較同類產品的性能。作為選購那個軟件重要因素之一。
軟件的性能指什么
在軟件開發中,內存消耗一般作為次要的考慮,因為現在的計算機一般都擁有比較大的內存,很多情況下,性能優化的手段就是空間換取時間。但是,并不是說,我們可以肆無忌憚的揮霍內存。如果需要支持在大數據量的用例時,如果內存被耗盡,操作系統會發生頻繁的內外存交換。導致執行速度急劇下降。
性能優化的原則
以MultiRow產品為例,MultiRow的一個性能需求是:"百萬行數據綁定下平滑滾動。"整個MultiRow項目的開發過程一直要考慮這個目標。
根據經驗,99%的性能消耗是由于1%的代碼造成的。所以,大部分性能優化都是針對這1%的瓶頸代碼進行的。具體實施也就分為兩步。首先,確定瓶頸,其次消除瓶頸。
首先必須要認識到,性能優化本身是有成本的。這個成本不單單體現在做性能優化所付出的工作量。還包括為性能優化而寫出的復雜代碼,額外的維護成本,會引入新的Bug,額外的內存開銷等。 一個常見問題是,一些剛接觸軟件開發的同學會對一些不必要的點生搬硬套性能優化技巧或者設計模式,帶來不必要的復雜度。性能優化常常需要對收益和成本之間做出權衡。
如何發現性能瓶頸
上一節提到,性能優化的第一步就是發現性能瓶頸,這一節主要介紹定位性能瓶頸的一些實踐。
以下代碼可以獲取某個操作的內存消耗。?
| // 在這里寫一些可能消耗內存的代碼,例如,如果想了解創建一個GcMultiRow軟件需要多少內存可以執行以下代碼 long? start = GC.GetTotalMemory(?true?); 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次,最后再把消耗的時間除以1000來確定最終消耗的時間。可以是結果更準確穩定,排除意外數據。
很多情況下,可以通過CodeReview發現性能問題。對于大數據量的循環,要格外關注。循環內的邏輯應該執行的盡可能的快。
ANTS?Profiler是款功能強大的性能檢測軟件??梢院芎玫膸椭覀儼l現性能瓶頸。使用這款軟件定位性能瓶頸可以起到事半功倍的效果。熟練使用這個工具,我們可以快速準確的定位到有性能問題的代碼。 這個工具很強大,但是也并不是完美無缺的。首先,這是一款收費軟件,部門只有幾個許可號。其次,這個軟件的工作原理是在IL中加入一些鉤子,用來記錄時間。所以在分析時,軟件的執行速度會比實際運行慢一些獲得的數據也因此并不是百分之百的準確,應該把軟件分析的數據作為參考,幫助快速定位問題,但是不要完全依賴,還要結合其他技巧來分析程序的性能。
?
性能優化的方法和技巧
定位了性能問題后,解決的辦法有很多。這個章節會介紹一些性能優化的技巧和實踐。
對于程序結構,在設計時就應該考慮,評估是否可以達到性能需求。如果后期發現了性能問題需要考慮調整結構會帶來非常大的開銷。舉例:
緩存(Cache)是性能優化中最常用的優化手段.適用的情況是頻繁的獲取一些數據,而每次獲取這些數據需要的時間比較長。這時,第一次獲取的時候會用正常的方法,并且在獲取之后把數據緩存下來。之后就使用緩存的數據。 如果使用了緩存的優化方法,需要特別注意緩存數據的同步,就是說,如果真實的數據發生了變化,應該及時的清除緩存數據,確保不會因為緩存而使用了錯誤的數據。 舉例:
| 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; } |
| 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;? ????}? } |
| 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; ???????} ????} } |
優化算法可以有效的提高特定操作的性能,使用一種算法時應該了解算法的適用情況,最好情況和最壞情況。 以GcMultiRow為例,最初MultiRow的排序算法使用了經典的快速排序算法。這看起來是沒有問題的,但是,對于表格軟件,用戶經常的操作是對有序表進行排序,如順序和倒序之間切換。而經典的快速排序算法的最差情況就是基本有序的情況。所以經典快速排序算法不適合MultiRow。最后通過改的排序算法解決了這個問題。改進的快速排序算法使用了3個中點來代替經典快排的一個中點的算法。每次交換都是從3個中點中選擇一個。這樣,亂序和基本有序的情況都不是這個算法的最壞情況,從而優化了性能。
我們現在工作的.net framework平臺,有很多現成的數據數據結構。我們應該了解這些數據結構,提升我們程序的性能: 舉例:
| 操作 | List | Dictionary |
| 索引 | 快 | 慢 |
| Find(Contains) | 慢 | 快 |
| Add | 快 | 慢 |
| Insert | 慢 | 快 |
| Remove | 慢 | 快 |
| if?(_dic.ContainKey(?"Key"?) { ?????return?_dic\[?"Key"?\]; } |
當需要大量取值的時候,這樣的取法會帶來性能問題。優化方法如下:
| object? value; if?(_dic.TryGetValue(?"Key"?, ?out?value)) { ?????return?value; } |
使用TryGetValue可以比先Contain再取值提高一倍的性能。
| List<?int?> list = ?new?List<?int?>{3, 10, 15}; list.BinarySearch(10); ?// 對于存在的值,結果是1 list.BinarySearch(8); ?// 對于不存在的值,會使用負數表示位置,如查找8時,結果是-2, 查找0結果是-1,查找100結果是-4. |
有些操作確實需要花費比較長的時間,如果用戶的操作在這段時間卡死會帶來很差的用戶體驗。有時候,使用多線程技術可以解決這個問題 舉例: CalculatorEngine在構造的時候要初始化所有的Function。由于Function比較多,初始化時間會比較長。這是就用到了多線程技術,在工作線程中做Function的初始化工作,就不影響主線程快速響應用戶的其他操作了。代碼如下:?
| 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函數,是在另一個線程里執行的,不會影響主線程的響應。當然,使用多線程是一個比較有難度的方案,需要充分考慮跨線程訪問和死鎖的問題。
在GcMultiRow實現AutoFilter功能的時候使用了一個類似于延遲執行的方案來提升響應速度。AutoFilter的功能是用戶在輸入的過程中根據用戶的輸入更新篩選的結果。數據量大的時候一次篩選需要較長時間,會影響用戶的連續輸入。使用多線可能是個好的方案,但是使用多線程會增加程序的復雜度。MultiRow的解決方案是當接收到用戶的鍵盤輸入消息的時候,并不立即出發Filter,而是等待0.3秒。如果用戶在連續輸入,會在這0.3秒內再次收到鍵盤消息,就再等0.3秒。直到連續0.3秒內沒有新的鍵盤消息時再觸發Filter。保證了快速響應用戶輸入的目的。
在GcMultiRow的Designer里,經常要根據當前的狀態刷新ToolBar上按鈕的Disable/Enable狀態。一次刷新需要較長的時間。如果用戶連續輸入會有卡頓的感覺,影響用戶體驗。GcMultiRow的優化方案是掛系統的Application.Idle事件。當系統空閑的時候,系統會觸發這個事件。接到這個事件表示此時用戶已經完成了連續的輸入,這時就可以從容的刷新按鈕的狀態了。
例如;在Winform下,觸發一塊區域重畫的時候,一般不適用Refresh而是Invalidate,這樣會觸發異步的刷新。在觸發之前可以多次Invalidate。BeginInvoke,PostMessage也都可以觸發異步的行為。
如WPF的DP DP相對于CLR property來說是很慢的,包括Get和Set都很慢,這和一般質感上Get比較快Set比較慢不一樣。如果一個DP需要被多次讀取的話建議是CLR property做Cache。
有時候,以上提到的方案都沒有辦法快速響應用戶操作,進度條,一直轉圈圈的圖片,提示性文字如"你的操作可能需要較長時間請耐心等待"。都可以提升用戶體驗??梢宰鳛樽詈蠓桨竵砜紤]。
總結
- 上一篇: 多层住宅小区家里的窗户外面抹灰层属于公共
- 下一篇: 如何快速解决家里的窗户漏风?