性能是考量一個控件產(chǎn)品好壞的重要指標(biāo),與產(chǎn)品的功能有著同等重要的地位。用戶在選擇一款控件產(chǎn)品的時候基本都會親身試驗(yàn)比較同類產(chǎn)品的性能。作為選購那個控件重要因素之一。
站在用戶的角度思考問題,與客戶深入溝通,找到南安網(wǎng)站設(shè)計(jì)與南安網(wǎng)站推廣的解決方案,憑借多年的經(jīng)驗(yàn),讓設(shè)計(jì)與互聯(lián)網(wǎng)技術(shù)結(jié)合,創(chuàng)造個性化、用戶體驗(yàn)好的作品,建站類型包括:成都網(wǎng)站制作、做網(wǎng)站、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣、主機(jī)域名、虛擬主機(jī)、企業(yè)郵箱。業(yè)務(wù)覆蓋南安地區(qū)。
控件的性能指什么
- 降低內(nèi)存消耗
在控件開發(fā)中,內(nèi)存消耗一般作為次要的考慮,因?yàn)楝F(xiàn)在的計(jì)算機(jī)一般都擁有比較大的內(nèi)存,很多情況下,性能優(yōu)化的手段就是空間換取時間。但是,并不是說,我們可以肆無忌憚的揮霍內(nèi)存。如果需要支持在大數(shù)據(jù)量的用例時,如果內(nèi)存被耗盡,操作系統(tǒng)會發(fā)生頻繁的內(nèi)外存交換。導(dǎo)致執(zhí)行速度急劇下降。 - 提升執(zhí)行速度
- 加載速度。
- 特定操作的響應(yīng)速度。包括,點(diǎn)擊,鍵盤輸入,滾動,排序過濾等。
性能優(yōu)化的原則
- 理解需求
以MultiRow產(chǎn)品為例,MultiRow的一個性能需求是:"百萬行數(shù)據(jù)綁定下平滑滾動。"整個MultiRow項(xiàng)目的開發(fā)過程一直要考慮這個目標(biāo)。 - 理解瓶頸
根據(jù)經(jīng)驗(yàn),99%的性能消耗是由于1%的代碼造成的。所以,大部分性能優(yōu)化都是針對這1%的瓶頸代碼進(jìn)行的。具體實(shí)施也就分為兩步。首先,確定瓶頸,其次消除瓶頸。 - 切忌過度
首先必須要認(rèn)識到,性能優(yōu)化本身是有成本的。這個成本不單單體現(xiàn)在做性能優(yōu)化所付出的工作量。還包括為性能優(yōu)化而寫出的復(fù)雜代碼,額外的維護(hù)成本,會引入新的Bug,額外的內(nèi)存開銷等。 一個常見問題是,一些剛接觸控件開發(fā)的同學(xué)會對一些不必要的點(diǎn)生搬硬套性能優(yōu)化技巧或者設(shè)計(jì)模式,帶來不必要的復(fù)雜度。性能優(yōu)化常常需要對收益和成本之間做出權(quán)衡。
如何發(fā)現(xiàn)性能瓶頸
上一節(jié)提到,性能優(yōu)化的第一步就是發(fā)現(xiàn)性能瓶頸,這一節(jié)主要介紹定位性能瓶頸的一些實(shí)踐。
- 如何獲取內(nèi)存消耗
以下代碼可以獲取某個操作的內(nèi)存消耗。
// 在這里寫一些可能消耗內(nèi)存的代碼,例如,如果想了解創(chuàng)建一個GcMultiRow控件需要多少內(nèi)存可以執(zhí)行以下代碼
long start = GC.GetTotalMemory(true);
var gcMulitRow1 = new GcMultiRow();
GC.Collect();
// 確保所有內(nèi)存都被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;
這里把一個操作循環(huán)執(zhí)行了1000次,最后再把消耗的時間除以1000來確定最終消耗的時間??梢允墙Y(jié)果更準(zhǔn)確穩(wěn)定,排除意外數(shù)據(jù)。
- 通過CodeReview發(fā)現(xiàn)性能問題。
很多情況下,可以通過CodeReview發(fā)現(xiàn)性能問題。對于大數(shù)據(jù)量的循環(huán),要格外關(guān)注。循環(huán)內(nèi)的邏輯應(yīng)該執(zhí)行的盡可能的快。 - Auts Performance Profiler
Auts Profiler是款功能強(qiáng)大的性能檢測軟件。可以很好的幫助我們發(fā)現(xiàn)性能瓶頸。使用這款軟件定位性能瓶頸可以起到事半功倍的效果。熟練使用這個工具,我們可以快速準(zhǔn)確的定位到有性能問題的代碼。 這個工具很強(qiáng)大,但是也并不是完美無缺的。首先,這是一款收費(fèi)軟件,部門只有幾個許可號。其次,這個軟件的工作原理是在IL中加入一些鉤子,用來記錄時間。所以在分析時,軟件的執(zhí)行速度會比實(shí)際運(yùn)行慢一些獲得的數(shù)據(jù)也因此并不是百分之百的準(zhǔn)確,應(yīng)該把軟件分析的數(shù)據(jù)作為參考,幫助快速定位問題,但是不要完全依賴,還要結(jié)合其他技巧來分析程序的性能。
性能優(yōu)化的方法和技巧
定位了性能問題后,解決的辦法有很多。這個章節(jié)會介紹一些性能優(yōu)化的技巧和實(shí)踐。
- 優(yōu)化程序結(jié)構(gòu)
對于程序結(jié)構(gòu),在設(shè)計(jì)時就應(yīng)該考慮,評估是否可以達(dá)到性能需求。如果后期發(fā)現(xiàn)了性能問題需要考慮調(diào)整結(jié)構(gòu)會帶來非常大的開銷。舉例:
- GcMultiRowGcMultiRow要支持100萬行數(shù)據(jù),假設(shè)每行有10列的話,就需要有1000萬個單元格,每個單元格上又有很多的屬性。如果不做任何優(yōu)化的話,大數(shù)據(jù)量時,一個GcMultiRow控件的內(nèi)存開銷會相當(dāng)?shù)拇蟆cMultiRow采用的方案是使用哈希表來存儲行數(shù)據(jù)。只有用戶改過的行放到哈希表里,而對于大部分沒有改過的行都直接使用模板代替。就達(dá)到了節(jié)省內(nèi)存的目的。
- Spread for WPF/Silverlight (SSL)WPF的畫法和Winform不同,是通過組合View元素的方法實(shí)現(xiàn)的。SSL同樣支持百萬級的數(shù)據(jù)量,但是又不能給每個單元格都分配一個View。所以SSL使用了VirtualizePanel來實(shí)現(xiàn)畫法。思路是每一個View是一個Cell的展示模塊。可以和Cell的數(shù)據(jù)模塊分離。這樣。只需要為顯示出來的Cell創(chuàng)建View。當(dāng)發(fā)生滾動時會有一部分Cell滾出屏幕,有一部分Cell滾入屏幕。這時,讓滾出屏幕的Cell和View分離。然后再復(fù)用這部分View給新進(jìn)入屏幕的Cell。如此循環(huán)。這樣只需要幾百個View就可以支持很多的Cell。
- 緩存
緩存(Cache)是性能優(yōu)化中最常用的優(yōu)化手段.適用的情況是頻繁的獲取一些數(shù)據(jù),而每次獲取這些數(shù)據(jù)需要的時間比較長。這時,第一次獲取的時候會用正常的方法,并且在獲取之后把數(shù)據(jù)緩存下來。之后就使用緩存的數(shù)據(jù)。 如果使用了緩存的優(yōu)化方法,需要特別注意緩存數(shù)據(jù)的同步,就是說,如果真實(shí)的數(shù)據(jù)發(fā)生了變化,應(yīng)該及時的清除緩存數(shù)據(jù),確保不會因?yàn)榫彺娑褂昧隋e誤的數(shù)據(jù)。 舉例:
- 使用緩存的情況比較多。最簡單的情況就是緩存到一個Field或臨時變量里。
for(int i = 0; i < gcMultiRow.RowCount; i++)
{
// Do something;
}
以上代碼一般情況下是沒有問題的,但是,如果GcMultiRow的行數(shù)比較大。而RowCount屬性的取值又比較慢的時候就需要使用緩存來做性能優(yōu)化。
int rowCount = gcMultiRow.RowCount; for (int i = 0; i < rowCount; i++) { // Do something; } - 使用對象池也是一個常見的緩存方案,比使用Field或臨時變量稍微復(fù)雜一點(diǎn)。 例如,在MultiRow中,畫邊線,畫背景,需要用到大量的Brush和Pen。這些GDI對象每次用之前要創(chuàng)建,用完后要銷毀。創(chuàng)建和銷毀的過程是比較慢的。GcMultiRow使用的方案是創(chuàng)建一個GDIPool。本質(zhì)上是一些Dictionary,使用顏色做Key。所以只有第一次取的時候需要創(chuàng)建,以后就直接使用以前創(chuàng)建好的。以下是GDIPool的代碼:
public static class GDIPool
{
Dictionary<Color, Brush > _cacheBrush = new Dictionary();
Dictionary _cachePen = new Dictionary();
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)造
有時候,有的對象創(chuàng)建需要花費(fèi)較長時間。而這個對象可能并不是所有的場景下都需要使用。這時,使用賴構(gòu)造的方法可以有效提高性能。 舉例:對象A需要內(nèi)部創(chuàng)建對象B。對象B的構(gòu)造時間比較長。 一般做法:
public class A { public B _b = new B(); }
一般做法下由于構(gòu)造對象A的同時要構(gòu)造對象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的時候就不需要創(chuàng)建B對象,只有需要使用的時候才需要構(gòu)造B對象。 - 優(yōu)化算法
優(yōu)化算法可以有效的提高特定操作的性能,使用一種算法時應(yīng)該了解算法的適用情況,最好情況和最壞情況。 以GcMultiRow為例,最初MultiRow的排序算法使用了經(jīng)典的快速排序算法。這看起來是沒有問題的,但是,對于表格控件,用戶經(jīng)常的操作是對有序表進(jìn)行排序,如順序和倒序之間切換。而經(jīng)典的快速排序算法的最差情況就是基本有序的情況。所以經(jīng)典快速排序算法不適合MultiRow。最后通過改的排序算法解決了這個問題。改進(jìn)的快速排序算法使用了3個中點(diǎn)來代替經(jīng)典快排的一個中點(diǎn)的算法。每次交換都是從3個中點(diǎn)中選擇一個。這樣,亂序和基本有序的情況都不是這個算法的最壞情況,從而優(yōu)化了性能。 - 了解Framework提供的數(shù)據(jù)結(jié)構(gòu)
我們現(xiàn)在工作的.net framework平臺,有很多現(xiàn)成的數(shù)據(jù)數(shù)據(jù)結(jié)構(gòu)。我們應(yīng)該了解這些數(shù)據(jù)結(jié)構(gòu),提升我們程序的性能: 舉例:
- string 的加運(yùn)算符 VS StringBuilder: 字符串的操作是我們經(jīng)常遇到的基本操作之一。 我們經(jīng)常會寫這樣的代碼 string str = str1 + str2。當(dāng)操作的字符串很少的時候,這樣的操作沒有問題。但是如果大量操作的時候(例如文本文件的Save/Load, Asp.net的Render),這樣做就會帶來嚴(yán)重的性能問題。這時,我們就應(yīng)該用StringBuilder來代替string的加操作。
- Dictionary VS List Dictionary和List是最常用的兩種集合類。選擇正確的集合類可以很大的提升程序的性能。為了做出正確的選擇,我們應(yīng)該對Dictionary和List的各種操作的性能比較了解。 下表中粗略的列出了兩種數(shù)據(jù)結(jié)構(gòu)的性能比較。
操作 | List | Dictionary |
索引 | 快 | 慢 |
Find(Contains) | 慢 | 快 |
Add | 快 | 慢 |
Insert | 慢 | 快 |
Remove | 慢 | 快 |
- TryGetValue 對于Dictionary的取值,比較直接的方法是如下代碼:
if(_dic.ContainKey("Key") { return _dic\["Key"\]; }
當(dāng)需要大量取值的時候,這樣的取法會帶來性能問題。優(yōu)化方法如下:
object value; if(_dic.TryGetValue("Key", out value)) { return value; }
使用TryGetValue可以比先Contain再取值提高一倍的性能。
- 為Dictionary選擇合適的Key。 Dictionary的取值性能很大情況下取決于做Key的對象的Equals和GetHashCode兩個方法的性能。如果可以的話使用Int做Key性能最好。如果是一個自定義的Class做Key的話,最好保證以下兩點(diǎn):1. 不同對象的GetHashCode重復(fù)率低。2. GetHashCode和Equals方法立即簡單,效率高。
- List的Sort和BinarySearch性能很好,如果能滿足功能需求的話推薦直接使用,而不是自己重寫。
List list = new List{3, 10, 15}; list.BinarySearch(10); // 對于存在的值,結(jié)果是1 list.BinarySearch(8); // 對于不存在的值,會使用負(fù)數(shù)表示位置,如查找8時,結(jié)果是-2, 查找0結(jié)果是-1,查找100結(jié)果是-4.
- 通過異步提升響應(yīng)時間
- 多線程
有些操作確實(shí)需要花費(fèi)比較長的時間,如果用戶的操作在這段時間卡死會帶來很差的用戶體驗(yàn)。有時候,使用多線程技術(shù)可以解決這個問題 舉例: CalculatorEngine在構(gòu)造的時候要初始化所有的Function。由于Function比較多,初始化時間會比較長。這是就用到了多線程技術(shù),在工作線程中做Function的初始化工作,就不影響主線程快速響應(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ù),是在另一個線程里執(zhí)行的,不會影響主線程的響應(yīng)。當(dāng)然,使用多線程是一個比較有難度的方案,需要充分考慮跨線程訪問和死鎖的問題。
- 加延遲時間
在GcMultiRow實(shí)現(xiàn)AutoFilter功能的時候使用了一個類似于延遲執(zhí)行的方案來提升響應(yīng)速度。AutoFilter的功能是用戶在輸入的過程中根據(jù)用戶的輸入更新篩選的結(jié)果。數(shù)據(jù)量大的時候一次篩選需要較長時間,會影響用戶的連續(xù)輸入。使用多線可能是個好的方案,但是使用多線程會增加程序的復(fù)雜度。MultiRow的解決方案是當(dāng)接收到用戶的鍵盤輸入消息的時候,并不立即出發(fā)Filter,而是等待0.3秒。如果用戶在連續(xù)輸入,會在這0.3秒內(nèi)再次收到鍵盤消息,就再等0.3秒。直到連續(xù)0.3秒內(nèi)沒有新的鍵盤消息時再觸發(fā)Filter。保證了快速響應(yīng)用戶輸入的目的。 - Application.Idle事件
在GcMultiRow的Designer里,經(jīng)常要根據(jù)當(dāng)前的狀態(tài)刷新ToolBar上按鈕的Disable/Enable狀態(tài)。一次刷新需要較長的時間。如果用戶連續(xù)輸入會有卡頓的感覺,影響用戶體驗(yàn)。GcMultiRow的優(yōu)化方案是掛系統(tǒng)的Application.Idle事件。當(dāng)系統(tǒng)空閑的時候,系統(tǒng)會觸發(fā)這個事件。接到這個事件表示此時用戶已經(jīng)完成了連續(xù)的輸入,這時就可以從容的刷新按鈕的狀態(tài)了。 - Invalidate, BeginInvoke. PostEvent 平臺本身也提供了一些異步方案。
例如;在Winform下,觸發(fā)一塊區(qū)域重畫的時候,一般不適用Refresh而是Invalidate,這樣會觸發(fā)異步的刷新。在觸發(fā)之前可以多次Invalidate。BeginInvoke,PostMessage也都可以觸發(fā)異步的行為。
- 了解平臺特性
如WPF的DP DP相對于CLR property來說是很慢的,包括Get和Set都很慢,這和一般質(zhì)感上Get比較快Set比較慢不一樣。如果一個DP需要被多次讀取的話建議是CLR property做Cache。 - 進(jìn)度條,提升用戶體驗(yàn)
有時候,以上提到的方案都沒有辦法快速響應(yīng)用戶操作,進(jìn)度條,一直轉(zhuǎn)圈圈的圖片,提示性文字如"你的操作可能需要較長時間請耐心等待"。都可以提升用戶體驗(yàn)??梢宰鳛樽詈蠓桨竵砜紤]。
標(biāo)題名稱:C#性能優(yōu)化實(shí)踐
文章轉(zhuǎn)載:
http://weahome.cn/article/jechhj.html