.Net性能優化實踐

原文地址http://www.infoq.com/cn/articles/C-sharp-performance-optimization?utm_source=infoq&utm_medium=related_content_link&utm_campaign=relatedContent_news_clk算法


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

本文以.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方法能夠被用來觸發異步的自定義行爲。

· 進度條,提高用戶體驗

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

相關文章
相關標籤/搜索