C# 效能最佳化實踐)

來源:互聯網
上載者:User

作者 胡森 發佈於 五月 15, 2013

 

效能主要指兩個方面:記憶體消耗和執行速度。效能最佳化簡而言之,就是在不影響系統運行正確性的前提下,使之運行地更快,完成特定功能所需的時間更短。

本文以.NET平台下的控制項產品MultiRow為例,描述C#效能最佳化的實踐。

· 理解需求

MultiRow的一個效能需求是:“百萬行資料繫結下平滑滾動。”整個MultiRow項目的開發過程一直在考慮這個目標。

· 理解瓶頸

99%的效能消耗是由於1%的代碼造成的。大部分效能最佳化都是針對這1%的瓶頸代碼進行的。具體實施也就分為兩步:“發現瓶頸”和“消除瓶頸”。

· 切忌過度

效能最佳化本身是有成本的。這個成本不單單體現在做效能最佳化所付出的工作量,還包括為效能最佳化而寫出複雜的代碼導致額外的維護成本,比如引入新的Bug,額外的記憶體開銷等。效能最佳化常常需要在收益和成本之間做出權衡。

如何發現效能瓶頸

效能最佳化的第一步是發現效能瓶頸,下面是一些定位效能瓶頸的實踐。

· 如何擷取記憶體消耗

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

long start = GC.GetTotalMemory(true);// 在這裡寫需要被測試記憶體消耗的代碼,例如,建立一個GcMultiRowvar 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方法可以被用來觸發非同步自訂行為。

· 進度條,提升使用者體驗

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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.