性能主要指兩個(gè)方面:內(nèi)存消耗和執(zhí)行速度。性能優(yōu)化簡(jiǎn)而言之,就是在不影響系統(tǒng)運(yùn)行正確性的前提下,使之運(yùn)行地更快,完成特定功能所需的時(shí)間更短。
本文以.NET平臺(tái)下的控件產(chǎn)品MultiRow為例,描述C#性能優(yōu)化的實(shí)踐。
性能優(yōu)化原則
·理解需求
MultiRow的一個(gè)性能需求是:“百萬行數(shù)據(jù)綁定下平滑滾動(dòng)。”整個(gè)MultiRow項(xiàng)目的開發(fā)過程一直在考慮這個(gè)目標(biāo)。
·理解瓶頸
99%的性能消耗是由于1%的代碼造成的。大部分性能優(yōu)化都是針對(duì)這1%的瓶頸代碼進(jìn)行的。具體實(shí)施也就分為兩步:“發(fā)現(xiàn)瓶頸”和“消除瓶頸”。
·切忌過度
性能優(yōu)化本身是有成本的。這個(gè)成本不單單體現(xiàn)在做性能優(yōu)化所付出的工作量,還包括為性能優(yōu)化而寫出復(fù)雜的代碼導(dǎo)致額外的維護(hù)成本,比如引入新的Bug,額外的內(nèi)存開銷等。性能優(yōu)化常常需要在收益和成本之間做出權(quán)衡。
如何發(fā)現(xiàn)性能瓶頸
性能優(yōu)化的第一步是發(fā)現(xiàn)性能瓶頸,下面是一些定位性能瓶頸的實(shí)踐。
·如何獲取內(nèi)存消耗
以下代碼可以獲取某個(gè)操作的內(nèi)存消耗。
long start = GC.GetTotalMemory(true);// 在這里寫需要被測(cè)試內(nèi)存消耗的代碼,例如,創(chuàng)建一個(gè)GcMultiRowvar gcMulitRow1 = new GcMultiRow();GC.Collect();// 確保所有內(nèi)存都被GC回收GC.WaitForFullGCComplete();long end = GC.GetTotalMemory(true);long useMemory = end - start;
·如何獲取時(shí)間消耗
以下代碼可以獲取某個(gè)操作時(shí)間消耗。
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;為了獲得更加穩(wěn)定的時(shí)間消耗,這里把一個(gè)操作循環(huán)執(zhí)行了1000次,取時(shí)間消耗的平均值以排除不穩(wěn)定數(shù)據(jù)。
·ANTS Performance PRofiler
ANTS Performance Profiler是款功能強(qiáng)大的性能檢測(cè)軟件。熟練使用這個(gè)工具,我們可以快速準(zhǔn)確的定位到有性能問題的代碼。這是一款收費(fèi)軟件,會(huì)在IL中加入一些鉤子用來記錄時(shí)間,所以在分析時(shí),軟件的執(zhí)行速度會(huì)比實(shí)際運(yùn)行慢一些,獲得的數(shù)據(jù)也因此并不是百分之百的準(zhǔn)確,還要結(jié)合其他技巧來分析程序的性能。
·CodeReview
CodeReview是發(fā)現(xiàn)性能問題的最后手段。CodeReview應(yīng)該對(duì)產(chǎn)品的性能瓶頸盡可能多的關(guān)注,確保該部分邏輯執(zhí)行的盡可能的快。
性能優(yōu)化的方法和技巧
定位了性能問題后,解決的辦法有很多。下面是一些性能優(yōu)化的技巧和實(shí)踐。
·優(yōu)化程序結(jié)構(gòu)
在設(shè)計(jì)時(shí)就應(yīng)該考慮產(chǎn)品結(jié)構(gòu)是否可以達(dá)到性能需求。如果后期發(fā)現(xiàn)了性能問題,調(diào)整結(jié)構(gòu)會(huì)帶來非常大的開銷。
例如:
GcMultiRow要支持100萬行數(shù)據(jù)。假設(shè)每行有10列的話,就需要有1000萬個(gè)單元格,每個(gè)單元格上又有很多的屬性。如果不做任何優(yōu)化,大數(shù)據(jù)量時(shí),一個(gè)GcMultiRow軟件的內(nèi)存開銷會(huì)相當(dāng)?shù)拇蟆cMultiRow采用的方案是使用哈希表來存儲(chǔ)行數(shù)據(jù):只有用戶改過的行放到哈希表里,大部分沒有改過的行都直接使用模板代替。這就達(dá)到了節(jié)省內(nèi)存的目的。
WPF平臺(tái)和Silverlight平臺(tái)的畫法和Winform平臺(tái)不同,是通過組合Visual元素的方法實(shí)現(xiàn)的。SpreadGrid for WPF產(chǎn)品同樣支持百萬級(jí)的數(shù)據(jù)量,但是又不能給每個(gè)單元格都分配一個(gè)View。所以SpreadGrid使用了VirtualizingPanel來實(shí)現(xiàn)畫法。思路是每一個(gè)Visual是一個(gè)Cell的展示模塊,可以和Cell的數(shù)據(jù)模塊分離,這樣就只需要為顯示出來的Cell創(chuàng)建Visual。當(dāng)發(fā)生滾動(dòng)時(shí)會(huì)有一部分Cell滾出屏幕,有一部分Cell滾入屏幕。這時(shí),讓滾出屏幕的Cell和Visual分離,然后再?gòu)?fù)用這部分Visual給新進(jìn)入屏幕的Cell。如此循環(huán),就只需要幾百個(gè)Visual就可以支持很多的Cell。
·緩存
緩存(Cache)是性能優(yōu)化中最常用的手段,針對(duì)需要頻繁的獲取一些數(shù)據(jù),同時(shí)每次獲取數(shù)據(jù)需要的時(shí)間比較長(zhǎng)的場(chǎng)景。如果使用了緩存的優(yōu)化方法,需要特別注意緩存數(shù)據(jù)的同步:如果真實(shí)的數(shù)據(jù)發(fā)生了變化,應(yīng)該及時(shí)的清除緩存數(shù)據(jù),確保不會(huì)因?yàn)榫彺娑褂昧隋e(cuò)誤的數(shù)據(jù)。
使用緩存的情況比較多。最簡(jiǎn)單的情況就是緩存到一個(gè)Field或臨時(shí)變量里。
for(int i = 0; i < gcMultiRow.RowCount; i++){ // Do something; } 以上代碼一般情況下是沒有問題的,但是,如果GcMultiRow的行數(shù)比較大。而RowCount屬性的取值又比較慢的時(shí)候,就需要使用緩存來做性能優(yōu)化。
int rowCount = gcMultiRow.RowCount;for (int i = 0; i < rowCount; i++){// Do something;}使用對(duì)象池也是一個(gè)常見的緩存方案,比使用Field或臨時(shí)變量稍微復(fù)雜一點(diǎn)。例如,在MultiRow中,畫邊線,畫背景,需要用到大量的Brush和Pen。這些GDI對(duì)象每次用之前要?jiǎng)?chuàng)建,用完后要銷毀。創(chuàng)建和銷毀的過程是比較慢的。GcMultiRow使用的方案是創(chuàng)建一個(gè)GDipool。本質(zhì)上是一些Dictionary,使用顏色做Key。所以只有第一次取的時(shí)候需要?jiǎng)?chuàng)建,以后就直接使用以前創(chuàng)建好的。
以下是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; } }·懶構(gòu)造
大多時(shí)候,對(duì)于創(chuàng)建需要花費(fèi)較長(zhǎng)時(shí)間的對(duì)象,往往并不是所有的場(chǎng)景下都需要使用。這時(shí),使用懶構(gòu)造的方法可以有效提高程序啟動(dòng)性能。
舉例來說,對(duì)象A需要內(nèi)部創(chuàng)建對(duì)象B。對(duì)象B的構(gòu)造時(shí)間比較長(zhǎng)。 一般做法:
public class A{ public B _b = new B();}一般做法下,由于構(gòu)造對(duì)象A的同時(shí)要構(gòu)造對(duì)象B,導(dǎo)致A的構(gòu)造速度也變慢了。
優(yōu)化做法:
public class A{ private B _b; public B BProperty { get { if(_b == null) { _b = new B(); } return _b; } }}優(yōu)化后,構(gòu)造A的時(shí)候就不需要?jiǎng)?chuàng)建B對(duì)象,有效的提高了A的構(gòu)造性能。
·優(yōu)化算法
優(yōu)化算法可以有效的提高特定操作的性能。使用一種算法時(shí)應(yīng)該了解算法的適用情況、最好情況和最壞情況。 以GcMultiRow為例,最初MultiRow的排序算法使用了經(jīng)典的快速排序算法。這看起來是沒有問題的。但是,對(duì)于表格軟件,用戶經(jīng)常的操作是對(duì)有序表進(jìn)行排序,如順序和倒序之間切換。而經(jīng)典的快速排序算法的最差情況就是基本有序的情況。所以經(jīng)典快速排序算法不適合MultiRow。
改進(jìn)的快速排序算法使用了3個(gè)中點(diǎn)來代替經(jīng)典快排的一個(gè)中點(diǎn)的算法,每次交換都是從3個(gè)中點(diǎn)中選擇中間值。這樣,亂序和基本有序的情況都不是這個(gè)算法的最壞情況,從而優(yōu)化了性能。
·正確的使用既有數(shù)據(jù)結(jié)構(gòu)
我們現(xiàn)在工作的.NET framework平臺(tái)有很多現(xiàn)成的數(shù)據(jù)結(jié)構(gòu)。我們應(yīng)該了解這些數(shù)據(jù)結(jié)構(gòu),提升我們程序的性能。
例如:
1. String的加運(yùn)算符和StringBuilder: 字符串的操作是我們經(jīng)常遇到的基本操作之一。 我們經(jīng)常會(huì)寫這樣的代碼 string str = str1 + str2。當(dāng)操作的字符串很少的時(shí)候,這樣的操作沒有問題。但是如果大量操作的時(shí)候(例如文本文件的Save/Load, asp.net的Render),這樣做就會(huì)帶來嚴(yán)重的性能問題。這時(shí),我們就應(yīng)該用StringBuilder來代替string的加操作。
2. Dictionary和List: Dictionary和List是最常用的兩種集合類。選擇正確的集合類可以很大的提升程序的性能。為了做出正確的選擇,我們應(yīng)該對(duì)Dictionary和List的各種操作的性能比較了解。 下表中粗略的列出了兩種數(shù)據(jù)結(jié)構(gòu)的性能比較。
操作 | List | Dictionary |
索引 | 快 | 慢 |
Find(Contains) | 慢 | 快 |
Add | 快 | 慢 |
Insert | 慢 | 快 |
Remove | 慢 | 快 |
3. TryGetValue: 對(duì)于Dictionary的取值,比較直接的方法是如下代碼:
if(_dic.ContainKey("Key"){ return _dic["Key"];} 當(dāng)需要大量取值的時(shí)候,這樣的取法會(huì)帶來性能問題。優(yōu)化方法如下:
object value;if(_dic.TryGetValue("Key", out value)){return value;}后一種用法要比前一種用法取值性能提高一倍。
4. 為Dictionary選擇合適的Key: Dictionary的取值性能很大情況下取決于做Key的對(duì)象的Equals和GetHashCode兩個(gè)方法的性能。如果可以的話,使用Int做Key性能最好。如果是一個(gè)自定義的Class做Key的話,最好保證以下兩點(diǎn):1. 不同對(duì)象的GetHashCode重復(fù)率低。2. GetHashCode和Equals方法簡(jiǎn)單,效率高。
5. List的Sort和BinarySearch性能很好,如果能滿足功能需求,推薦直接使用。
List<int> list = new List<int>{3, 10, 15}; list.BinarySearch(10); // 對(duì)于存在的值,結(jié)果是1 list.BinarySearch(8); // 對(duì)于不存在的值,會(huì)使用負(fù)數(shù)表示位置, // 如查找8時(shí),結(jié)果是-2, 查找0結(jié)果是-1,查找100結(jié)果是-4. ·通過異步提升響應(yīng)時(shí)間
1.多線程
有些操作確實(shí)需要花費(fèi)比較長(zhǎng)的時(shí)間。在處理的過程中,如果用戶進(jìn)行操作時(shí)失去響應(yīng),這個(gè)用戶體驗(yàn)是很差的。使用多線程技術(shù)可以解決這個(gè)問題。例如,有一個(gè)類似Excel的計(jì)算引擎,在構(gòu)造的時(shí)候要初始化所有的函數(shù)定義。由于函數(shù)比較多,初始化時(shí)間會(huì)比較長(zhǎng)。這是如果用到了多線程,在工作線程中做函數(shù)定義進(jìn)行的初始化,就不會(huì)影響到UI線程快速響應(yīng)用戶的其他操作了。
代碼如下:
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函數(shù),是在另一個(gè)線程里執(zhí)行的,不會(huì)影響主線程的響應(yīng)。當(dāng)然,使用多線程是一個(gè)比較有難度的方案,需要充分考慮跨線程訪問和死鎖的問題。
2.加延遲時(shí)間
在GcMultiRow實(shí)現(xiàn)AutoFilter功能的時(shí)候使用了一個(gè)類似于延遲執(zhí)行的方案來提升響應(yīng)速度。AutoFilter的功能是用戶在輸入的過程中根據(jù)用戶的輸入更新篩選的結(jié)果。數(shù)據(jù)量大的時(shí)候一次篩選需要較長(zhǎng)時(shí)間,會(huì)導(dǎo)致用戶輸入不流暢,體驗(yàn)不好。使用多線程雖然是個(gè)好方案,但是會(huì)增加程序的復(fù)雜度。MultiRow的解決方案是當(dāng)接收到用戶的鍵盤輸入消息的時(shí)候,并不立即出發(fā)Filter,而是等待0.3秒。如果用戶連續(xù)輸入,會(huì)在這0.3秒內(nèi)再次收到鍵盤消息,放棄上一個(gè)任務(wù),再等0.3秒,直到連續(xù)0.3秒內(nèi)沒有新的鍵盤消息時(shí)再觸發(fā)Filter。這樣就實(shí)現(xiàn)了比較流暢的用戶體驗(yàn)。
3. application.Idle事件
在GcMultiRow的Designer里,經(jīng)常要根據(jù)當(dāng)前的狀態(tài)刷新ToolBar上按鈕的Disable/Enable狀態(tài),一次刷新需要較長(zhǎng)的時(shí)間。這個(gè)又一次影響了用戶輸入的流暢性。GcMultiRow的優(yōu)化方案是通過系統(tǒng)的Application.Idle事件,僅當(dāng)系統(tǒng)空閑的時(shí)候處理刷新邏輯。接到這個(gè)事件時(shí),一般都是用戶已經(jīng)完成了連續(xù)的輸入,這時(shí)就可以從容的刷新按鈕的狀態(tài)了。
4. Refresh, BeginInvoke
平臺(tái)本身也提供了一些異步方案。例如在WinForm下觸發(fā)一塊區(qū)域重畫的時(shí)候,調(diào)用Refresh方法不會(huì)導(dǎo)致立即重畫,而是設(shè)置Invalidate標(biāo)記,觸發(fā)異步的刷新。在控件開發(fā)中,這個(gè)技巧可以有效的提高產(chǎn)品的性能,同時(shí)簡(jiǎn)化實(shí)現(xiàn)復(fù)雜度。
Control.BeginInvoke方法可以被用來觸發(fā)異步的自定義行為。
·進(jìn)度條,提升用戶體驗(yàn)
有時(shí)候,以上提到的方案都沒有辦法快速響應(yīng)用戶操作。進(jìn)度條、一直轉(zhuǎn)圈圈的圖片、提示性文字(如"你的操作可能需要較長(zhǎng)時(shí)間,請(qǐng)耐心等待")等,都可以有效的提升用戶體驗(yàn),可以作為最后方案來考慮。
轉(zhuǎn)自InfoQ:http://www.infoq.com/cn/articles/C-sharp-performance-optimization
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注