天天看點

C#性能優化實踐軟體的性能指什麼性能優化的原則如何發現性能瓶頸性能優化的方法和技巧

性能是考量一個軟體産品好壞的重要名額,與産品的功能有着同等重要的地位。使用者在選擇一款軟體産品的時候基本都會親身試驗比較同類産品的性能。作為選購那個軟體重要因素之一。

降低記憶體消耗

在軟體開發中,記憶體消耗一般作為次要的考慮,因為現在的計算機一般都擁有比較大的記憶體,很多情況下,性能優化的手段就是空間換取時間。但是,并不是說,我們可以肆無忌憚的揮霍記憶體。如果需要支援在大資料量的用例時,如果記憶體被耗盡,作業系統會發生頻繁的内外存交換。導緻執行速度急劇下降。

提升執行速度

加載速度。

特定操作的響應速度。包括,點選,鍵盤輸入,滾動,排序過濾等。

了解需求

以MultiRow産品為例,MultiRow的一個性能需求是:"百萬行資料綁定下平滑滾動。"整個MultiRow項目的開發過程一直要考慮這個目标。

了解瓶頸

根據經驗,99%的性能消耗是由于1%的代碼造成的。是以,大部分性能優化都是針對這1%的瓶頸代碼進行的。具體實施也就分為兩步。首先,确定瓶頸,其次消除瓶頸。

切忌過度

首先必須要認識到,性能優化本身是有成本的。這個成本不單單展現在做性能優化所付出的工作量。還包括為性能優化而寫出的複雜代碼,額外的維護成本,會引入新的Bug,額外的記憶體開銷等。 一個常見問題是,一些剛接觸軟體開發的同學會對一些不必要的點生搬硬套性能優化技巧或者設計模式,帶來不必要的複雜度。性能優化常常需要對收益和成本之間做出權衡。

上一節提到,性能優化的第一步就是發現性能瓶頸,這一節主要介紹定位性能瓶頸的一些實踐。

如何擷取記憶體消耗

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

  

如何擷取時間消耗

以下代碼可以擷取某個操作時間消耗。

 

這裡把一個操作循環執行了1000次,最後再把消耗的時間除以1000來确定最終消耗的時間。可以是結果更準确穩定,排除意外資料。

通過CodeReview發現性能問題。

很多情況下,可以通過CodeReview發現性能問題。對于大資料量的循環,要格外關注。循環内的邏輯應該執行的盡可能的快。

ANTS Performance Profiler

ANTS Profiler是款功能強大的性能檢測軟體。可以很好的幫助我們發現性能瓶頸。使用這款軟體定位性能瓶頸可以起到事半功倍的效果。熟練使用這個工具,我們可以快速準确的定位到有性能問題的代碼。 這個工具很強大,但是也并不是完美無缺的。首先,這是一款收費軟體,部門隻有幾個許可号。其次,這個軟體的工作原理是在IL中加入一些鈎子,用來記錄時間。是以在分析時,軟體的執行速度會比實際運作慢一些獲得的資料也是以并不是百分之百的準确,應該把軟體分析的資料作為參考,幫助快速定位問題,但是不要完全依賴,還要結合其他技巧來分析程式的性能。

定位了性能問題後,解決的辦法有很多。這個章節會介紹一些性能優化的技巧和實踐。

優化程式結構

對于程式結構,在設計時就應該考慮,評估是否可以達到性能需求。如果後期發現了性能問題需要考慮調整結構會帶來非常大的開銷。舉例:

GcMultiRowGcMultiRow要支援100萬行資料,假設每行有10列的話,就需要有1000萬個單元格,每個單元格上又有很多的屬性。如果不做任何優化的話,大資料量時,一個GcMultiRow軟體的記憶體開銷會相當的大。GcMultiRow采用的方案是使用哈希表來存儲行資料。隻有使用者改過的行放到哈希表裡,而對于大部分沒有改過的行都直接使用模闆代替。就達到了節省記憶體的目的。

Spread for WPF/Silverlight (SSL)WPF的畫法和Winform不同,是通過組合View元素的方法實作的。SSL同樣支援百萬級的資料量,但是又不能給每個單元格都配置設定一個View。是以SSL使用了VirtualizePanel來實作畫法。思路是每一個View是一個Cell的展示子產品。可以和Cell的資料子產品分離。這樣。隻需要為顯示出來的Cell建立View。當發生滾動時會有一部分Cell滾出螢幕,有一部分Cell滾入螢幕。這時,讓滾出螢幕的Cell和View分離。然後再複用這部分View給新進入螢幕的Cell。如此循環。這樣隻需要幾百個View就可以支援很多的Cell。

緩存

緩存(Cache)是性能優化中最常用的優化手段.适用的情況是頻繁的擷取一些資料,而每次擷取這些資料需要的時間比較長。這時,第一次擷取的時候會用正常的方法,并且在擷取之後把資料緩存下來。之後就使用緩存的資料。 如果使用了緩存的優化方法,需要特别注意緩存資料的同步,就是說,如果真實的資料發生了變化,應該及時的清除緩存資料,確定不會因為緩存而使用了錯誤的資料。 舉例:

使用緩存的情況比較多。最簡單的情況就是緩存到一個Field或臨時變量裡。

使用對象池也是一個常見的緩存方案,比使用Field或臨時變量稍微複雜一點。 例如,在MultiRow中,畫邊線,畫背景,需要用到大量的Brush和Pen。這些GDI對象每次用之前要建立,用完後要銷毀。建立和銷毀的過程是比較慢的。GcMultiRow使用的方案是建立一個GDIPool。本質上是一些Dictionary,使用顔色做Key。是以隻有第一次取的時候需要建立,以後就直接使用以前建立好的。以下是GDIPool的代碼:

懶構造

有時候,有的對象建立需要花費較長時間。而這個對象可能并不是所有的場景下都需要使用。這時,使用賴構造的方法可以有效提高性能。 舉例:對象A需要内部建立對象B。對象B的構造時間比較長。 一般做法:

一般做法下由于構造對象A的同時要構造對象B導緻了A的構造速度也變慢了。優化做法:

優化算法

優化算法可以有效的提高特定操作的性能,使用一種算法時應該了解算法的适用情況,最好情況和最壞情況。 以GcMultiRow為例,最初MultiRow的排序算法使用了經典的快速排序算法。這看起來是沒有問題的,但是,對于表格軟體,使用者經常的操作是對有序表進行排序,如順序和倒序之間切換。而經典的快速排序算法的最差情況就是基本有序的情況。是以經典快速排序算法不适合MultiRow。最後通過改的排序算法解決了這個問題。改進的快速排序算法使用了3個中點來代替經典快排的一個中點的算法。每次交換都是從3個中點中選擇一個。這樣,亂序和基本有序的情況都不是這個算法的最壞情況,進而優化了性能。

了解Framework提供的資料結構

我們現在工作的.net framework平台,有很多現成的資料資料結構。我們應該了解這些資料結構,提升我們程式的性能: 舉例:

string 的加運算符 VS StringBuilder: 字元串的操作是我們經常遇到的基本操作之一。 我們經常會寫這樣的代碼 string str = str1 + str2。當操作的字元串很少的時候,這樣的操作沒有問題。但是如果大量操作的時候(例如文本檔案的Save/Load, Asp.net的Render),這樣做就會帶來嚴重的性能問題。這時,我們就應該用StringBuilder來代替string的加操作。

Dictionary VS List Dictionary和List是最常用的兩種集合類。選擇正确的集合類可以很大的提升程式的性能。為了做出正确的選擇,我們應該對Dictionary和List的各種操作的性能比較了解。 下表中粗略的列出了兩種資料結構的性能比較。

操作

List

Dictionary

索引

Find(Contains)

Add

Insert

Remove

TryGetValue 對于Dictionary的取值,比較直接的方法是如下代碼:

當需要大量取值的時候,這樣的取法會帶來性能問題。優化方法如下:

使用TryGetValue可以比先Contain再取值提高一倍的性能。

為Dictionary選擇合适的Key。 Dictionary的取值性能很大情況下取決于做Key的對象的Equals和GetHashCode兩個方法的性能。如果可以的話使用Int做Key性能最好。如果是一個自定義的Class做Key的話,最好保證以下兩點:1. 不同對象的GetHashCode重複率低。2. GetHashCode和Equals方法立即簡單,效率高。

List的Sort和BinarySearch性能很好,如果能滿足功能需求的話推薦直接使用,而不是自己重寫。

通過異步提升響應時間

多線程

有些操作确實需要花費比較長的時間,如果使用者的操作在這段時間卡死會帶來很差的使用者體驗。有時候,使用多線程技術可以解決這個問題 舉例: CalculatorEngine在構造的時候要初始化所有的Function。由于Function比較多,初始化時間會比較長。這是就用到了多線程技術,在工作線程中做Function的初始化工作,就不影響主線程快速響應使用者的其他操作了。代碼如下:

這裡比較慢的操作就是EnsureFunctions函數,是在另一個線程裡執行的,不會影響主線程的響應。當然,使用多線程是一個比較有難度的方案,需要充分考慮跨線程通路和死鎖的問題。

加延遲時間

在GcMultiRow實作AutoFilter功能的時候使用了一個類似于延遲執行的方案來提升響應速度。AutoFilter的功能是使用者在輸入的過程中根據使用者的輸入更新篩選的結果。資料量大的時候一次篩選需要較長時間,會影響使用者的連續輸入。使用多線可能是個好的方案,但是使用多線程會增加程式的複雜度。MultiRow的解決方案是當接收到使用者的鍵盤輸入消息的時候,并不立即出發Filter,而是等待0.3秒。如果使用者在連續輸入,會在這0.3秒内再次收到鍵盤消息,就再等0.3秒。直到連續0.3秒内沒有新的鍵盤消息時再觸發Filter。保證了快速響應使用者輸入的目的。

Application.Idle事件

在GcMultiRow的Designer裡,經常要根據目前的狀态重新整理ToolBar上按鈕的Disable/Enable狀态。一次重新整理需要較長的時間。如果使用者連續輸入會有卡頓的感覺,影響使用者體驗。GcMultiRow的優化方案是挂系統的Application.Idle事件。當系統空閑的時候,系統會觸發這個事件。接到這個事件表示此時使用者已經完成了連續的輸入,這時就可以從容的重新整理按鈕的狀态了。

Invalidate, BeginInvoke. PostEvent 平台本身也提供了一些異步方案。

例如;在Winform下,觸發一塊區域重畫的時候,一般不适用Refresh而是Invalidate,這樣會觸發異步的重新整理。在觸發之前可以多次Invalidate。BeginInvoke,PostMessage也都可以觸發異步的行為。

了解平台特性

如WPF的DP DP相對于CLR property來說是很慢的,包括Get和Set都很慢,這和一般質感上Get比較快Set比較慢不一樣。如果一個DP需要被多次讀取的話建議是CLR property做Cache。

進度條,提升使用者體驗

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