C#效能最佳化

來源:互聯網
上載者:User
效能是考量一個軟體產品好壞的重要指標,與產品的功能有著同等重要的地位。使用者在選擇一款軟體產品的時候基本都會親身實驗比較同類產品的效能。作為選購那個軟體重要因素之一。

軟體的效能指什麼
1.降級記憶體消耗
在軟體開發中,記憶體消耗一般作為次要的考慮,因為現在的電腦一般都擁有比較大的記憶體,很多情況下,效能最佳化的手段就是空間換取時間。但是,並不是說,我們可以肆無忌憚的揮霍記憶體。如果需要支援在大資料量的用例時,如果記憶體被耗盡,作業系統會發生頻繁的內外存交換。導致執行速度急劇下降
2.提升執行速度

載入速度。

特定操作的響應速度。包括,點擊,鍵盤輸入,滾動,排序過濾等。


效能最佳化的原則

理解需求
以MultiRow產品為例,MultiRow的一個效能需求是:"百萬行資料繫結下平滑滾動。"整個MultiRow項目的開發過程一直要考慮這個目標。

理解瓶頸
根據經驗,99%的效能消耗是由於1%的代碼造成的。所以,大部分效能最佳化都是針對這1%的瓶頸代碼進行的。具體實施也就分為兩步。首先,確定瓶頸,其次消除瓶頸。

切忌過度
首先必須要認識到,效能最佳化本身是有成本的。這個成本不單單體現在做效能最佳化所付出的工作量。還包括為效能最佳化而寫出的複雜代碼,額外的維護成本,會引入新的Bug,額外的記憶體開銷等。 一個常見問題是,一些剛接觸軟體開發的同學會對一些不必要的點生搬硬套效能最佳化技巧或者設計模式,帶來不必要的複雜度。效能最佳化常常需要對收益和成本之間做出權衡。


如何發現效能瓶頸

上一節提到,效能最佳化的第一步就是發現效能瓶頸,這一節主要介紹定位效能瓶頸的一些實踐。

1. 如何擷取記憶體消耗

以下代碼可以擷取某個操作的記憶體消耗。

// 在這裡寫一些可能消耗記憶體的代碼,例如,如果想瞭解建立一個GcMultiRow軟體需要多少記憶體可以執行以下代碼long start = GC.GetTotalMemory(true);var gcMulitRow1 = new GcMultiRow();GC.Collect();// 確保所有記憶體都被GC回收GC.WaitForFullGCComplete();long end = GC.GetTotalMemory(true);long useMemory = end - start;

2. 如何擷取時間消耗
以下代碼可以擷取某個操作時間消耗。

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來確定最終消耗的時間。可以是結果更準確穩定,排除意外資料。

3. 通過CodeReview發現效能問題。
很多情況下,可以通過CodeReview發現效能問題。對於大資料量的迴圈,要格外關注。迴圈內的邏輯應該執行的儘可能的快。4.ANTS Performance Profiler
ANTS Profiler是款功能強大的效能檢測軟體。可以很好的協助我們發現效能瓶頸。使用這款軟體定位效能瓶頸可以起到事半功倍的效果。熟練使用這個工具,我們可以快速準確的定位到有效能問題的代碼。 這個工具很強大,但是也並不是完美無缺的。首先,這是一款收費軟體,部門只有幾個許可號。其次,這個軟體的工作原理是在IL中加入一些鉤子,用來記錄時間。所以在分析時,軟體的執行速度會比實際運行慢一些獲得的資料也因此並不是百分之百的準確,應該把軟體分析的資料作為參考,協助快速定位問題,但是不要完全依賴,還要結合其他技巧來剖析器的效能。

效能最佳化的方法和技巧

定位了效能問題後,解決的辦法有很多。這個章節會介紹一些效能最佳化的技巧和實踐。

1. 最佳化程式結構

對於程式結構,在設計時就應該考慮,評估是否可以達到效能需求。如果後期發現了效能問題需要考慮調整結構會帶來非常大的開銷。舉例:


1.1 GcMultiRowGcMultiRow要支援100萬行資料,假設每行有10列的話,就需要有1000萬個儲存格,每個儲存格上又有很多的屬性。如果不做任何最佳化的話,大資料量時,一個GcMultiRow軟體的記憶體開銷會相當的大。GcMultiRow採用的方案是使用雜湊表來儲存行資料。只有使用者改過的行放到雜湊表裡,而對於大部分沒有改過的行都直接使用模板代替。就達到了節省記憶體的目的。


1.2 Spread for WPF/Silverlight (SSL)WPF的畫法和Winform不同,是通過組合View元素的方法實現的。SSL同樣支援百萬級的資料量,但是又不能給每個儲存格都分配一個View。所以SSL使用了VirtualizePanel來實現畫法。思路是每一個View是一個Cell的展示模組。可以和Cell的資料模組分離。這樣。只需要為顯示出來的Cell建立View。當發生滾動時會有一部分Cell滾出螢幕,有一部分Cell滾入螢幕。這時,讓滾出螢幕的Cell和View分離。然後再複用這部分View給新進入螢幕的Cell。如此迴圈。這樣只需要幾百個View就可以支援很多的Cell。


2. 緩衝

緩衝(Cache)是效能最佳化中最常用的最佳化手段.適用的情況是頻繁的擷取一些資料,而每次擷取這些資料需要的時間比較長。這時,第一次擷取的時候會用正常的方法,並且在擷取之後把資料緩衝下來。之後就使用緩衝的資料。 如果使用了緩衝的最佳化方法,需要特別注意快取資料的同步,就是說,如果真實的資料發生了變化,應該及時的清除快取資料,確保不會因為緩衝而使用了錯誤的資料。 舉例:


2.1 使用緩衝的情況比較多。最簡單的情況就是緩衝到一個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;}


2.2 使用對象池也是一個常見的緩衝方案,比使用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;    } }

2.3 懶構造
有時候,有的對象建立需要花費較長時間。而這個對象可能並不是所有的情境下都需要使用。這時,使用賴構造的方法可以有效提高效能。 舉例:對象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對象,只有需要使用的時候才需要構造B對象。


2.4 最佳化演算法 最佳化演算法可以有效提高特定操作的效能,使用一種演算法時應該瞭解演算法的適用情況,最好情況和最壞情況。 以GcMultiRow為例,最初MultiRow的排序演算法使用了經典的快速排序演算法。這看起來是沒有問題的,但是,對於表格軟體,使用者經常的操作是對有序表進行排序,如順序和倒序之間切換。而經典的快速排序演算法的最差情況就是基本有序的情況。所以經典快速排序演算法不適合MultiRow。最後通過改的排序演算法解決了這個問題。改進的快速排序演算法使用了3個中點來代替經典快排的一個中點的演算法。每次交換都是從3個中點中選擇一個。這樣,亂序和基本有序的情況都不是這個演算法的最壞情況,從而最佳化了效能。


2.5 瞭解Framework提供的資料結構 我們現在工作的.net framework平台,有很多現成的資料資料結構。我們應該瞭解這些資料結構,提升我們程式的效能:
舉例:
2.5.1 string 的加運算子 VS StringBuilder: 字串的操作是我們經常遇到的基本操作之一。 我們經常會寫這樣的代碼 string str = str1 + str2。當操作的字串很少的時候,這樣的操作沒有問題。但是如果大量操作的時候(例如文字檔的Save/Load, Asp.net的Render),這樣做就會帶來嚴重的效能問題。這時,我們就應該用StringBuilder來代替string的加操作。

2.5.2 Dictionary VS List Dictionary和List是最常用的兩種集合類。選擇正確的集合類可以很大的提升程式的效能。為了做出正確的選擇,我們應該對Dictionary和List的各種操作的效能比較瞭解。2.5.3TryGetValue 對於Dictionary的取值,比較直接的方法是如下代碼:

if(_dic.ContainKey("Key"){    return _dic\["Key"\];}


當需要大量取值的時候,這樣的取法會帶來效能問題。最佳化方法如下:

object value;if(_dic.TryGetValue("Key", out value)){    return value;}

使用TryGetValue可以比先Contain再取值提高一倍的效能。


2.5.4 為Dictionary選擇合適的Key。 Dictionary的取值效能很大情況下取決於做Key的對象的Equals和GetHashCode兩個方法的效能。如果可以的話使用Int做Key效能最好。如果是一個自訂的Class做Key的話,最好保證以下兩點:1. 不同對象的GetHashCode重複率低。2. GetHashCode和Equals方法立即簡單,效率高。

2.5.5 List的Sort和BinarySearch效能很好,如果能滿足功能需求的話推薦直接使用,而不是自己重寫。

List<int> list = new List<int>{3, 10, 15};list.BinarySearch(10); // 對於存在的值,結果是1list.BinarySearch(8); // 對於不存在的值,會使用負數表示位置,如尋找8時,結果是-2, 尋找0結果是-1,尋找100結果是-4.

複製代碼

2.6 通過非同步提升回應時間
2.6.1 多線程
有些操作確實需要花費比較長的時間,如果使用者的操作在這段時間卡死會帶來很差的使用者體驗。有時候,使用多線程技術可以解決這個問題 舉例: 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函數,是在另一個線程裡執行的,不會影響主線程的響應。當然,使用多線程是一個比較有難度的方案,需要充分考慮跨線程訪問和死結的問題。

2.6.2 加延遲時間
在GcMultiRow實現AutoFilter功能的時候使用了一個類似於順延強制的方案來提升響應速度。AutoFilter的功能是使用者在輸入的過程中根據使用者的輸入更新篩選的結果。資料量大的時候一次篩選需要較長時間,會影響使用者的連續輸入。使用多線可能是個好的方案,但是使用多線程會增加程式的複雜度。MultiRow的解決方案是當接收到使用者的鍵盤輸入訊息的時候,並不立即出發Filter,而是等待0.3秒。如果使用者在連續輸入,會在這0.3秒內再次收到鍵盤訊息,就再等0.3秒。直到連續0.3秒內沒有新的鍵盤訊息時再觸發Filter。保證了快速響應使用者輸入的目的。

2.6.3 Application.Idle事件
在GcMultiRow的Designer裡,經常要根據當前的狀態重新整理ToolBar上按鈕的Disable/Enable狀態。一次重新整理需要較長的時間。如果使用者連續輸入會有卡頓的感覺,影響使用者體驗。GcMultiRow的最佳化方案是掛系統的Application.Idle事件。當系統閒置時候,系統會觸發這個事件。接到這個事件表示此時使用者已經完成了連續的輸入,這時就可以從容的重新整理按鈕的狀態了。

2.6.4 Invalidate, BeginInvoke. PostEvent 平台本身也提供了一些非同步方案。
例如;在Winform下,觸發一塊地區重畫的時候,一般不適用Refresh而是Invalidate,這樣會觸發非同步重新整理。在觸發之前可以多次Invalidate。BeginInvoke,PostMessage也都可以觸發非同步行為。

2.7 瞭解平台特性
如WPF的DP DP相對於CLR property來說是很慢的,包括Get和Set都很慢,這和一般質感上Get比較快Set比較慢不一樣。如果一個DP需要被多次讀取的話建議是CLR property做Cache。

2.8 進度條,提升使用者體驗
有時候,以上提到的方案都沒有辦法快速響應使用者操作,進度條,一直轉圈圈的圖片,提示性文字如"你的操作可能需要較長時間請耐心等待"。都可以提升使用者體驗。可以作為最後方案來考慮。


目前已有很多使用C#編寫的開發工具,其中值得一提的是ComponentOne Studio Enterprise,這是一款專註於公司專屬應用程式的.NET全功能控制項套包,支援WinForms、WPF、UWP、ASP.NET MVC等多個平台,協助在縮減成本的同時,提前交付豐富的案頭、Web和移動公司專屬應用程式。

  • 相關文章

    聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.