程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> C#性能優化實踐

C#性能優化實踐

編輯:C#入門知識

性能優化原則

· 理解需求

MultiRow的一個性能需求是:“百萬行數據綁定下平滑滾動。”整個MultiRow項目的開發過程一直在考慮這個目標。

· 理解瓶頸

 

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方法可以被用來觸發異步的自定義行為。

· 進度條,提升用戶體驗

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


作者簡介:

胡森,葡萄城公司軟件工程師,PowerTools產品系列項目經理。長期專注於 .NET UI 控件產品的設計和開發。致力於為廣大.NET開發者提供強大,高效,易用的控件產品。

感謝趙劼對本文的審校。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved