C#軟件性能優化
1. 性能
衡量一個軟件系統性能的常見指標有:響應時間、負載、資源使用率、並發數。在軟件中有具體的提高性能需求時,我們需分析該系統性能的影響由哪些因素組成,再針對各部分進行性能優化。例如:我們在儀器設備軟件中,從數據讀寫、算法計算、業務過程、通訊環節分析,根據需求的性能指標進行優化。
1.1 優化性能的原則
結構優化,減少數據交互的頻次;
減小數據的交互量;
優化資源,減少創建對象的次數;
提高數據的讀寫速度;
算法與數據結構的優化;
業務過程優化;
通訊性能提升;
數據庫優化;
1.2 常用優化性能的方法
1.2.1 避免不必要的對象創建
如果對象並不會隨每次循環而改變狀態,那么在循環中反復創建對象將帶來性能損耗。高效的做法是將對象提到循環外面創建。
1.2.2 避免不必要的拋出異常
拋出異常和捕獲異常屬於消耗比較大的操作,在可能的情況下,應通過完善程序邏輯避免拋出不必要不必要的異常。
如:預先做參數值的判斷。
如果是為了包裝異常的目的(即加入更多信息后包裝成新異常),那么是合理的。但是有不少代碼,捕獲異常沒有做任何處理就再次拋出,這將無謂地增加一次捕獲異常和拋出異常的消耗。如以下代碼則無謂增加一次捕獲與拋出異常。
Try
{
...
}
Catch(Exception ex)
{
Throw ex;
}
1.2.3 避免裝箱拆箱
C#可以在值類型和引用類型之間自動轉換,方法是裝箱和拆箱。裝箱需要從堆上分配對象並拷貝值,有一定性能消耗。如果這一過程發生在循環中或是作為底層方法被頻繁調用,應當避免。
ArrayList al = new ArrayList();
for ( int i = 0 ; i < 1000 ; i ++ )
{
al.Add(i); // Implicitly boxed because Add() takes an object
}
int f = ( int )al[ 0 ];
則ArrayList可定義為List<int>
1.2.4 使用 StringBuilder 做字符串連接
如果字符串連接次數不是固定的,例如在一個循環中,則應該使用 StringBuilder 類來做字符串連接工作。因為 StringBuilder 內部有一個 StringBuffer ,連接操作不會每次分配新的字符串空間。只有當連接后的字符串超出 Buffer 大小時,才會申請新的 Buffer 空間。典型代碼如下:
StringBuilder sb = new StringBuilder( 256 );
for ( int i = 0 ; i < Results.Count; i ++ )
{
sb.Append (Results[i]);
}
如果連接次數是固定的並且只有幾次,此時應該直接用 + 號連接,保持程序簡潔易讀。實際上,編譯器已經做了優化,會依據加號次數調用不同參數個數的 String.Concat 方法。例如: String str = str1 + str2 + str3 + str4; 會被編譯為 String.Concat(str1, str2, str3, str4)。該方法內部會計算總的 String 長度,僅分配一次,並不會如通常想象的那樣分配三次。作為一個經驗值,當字符串連接操作達到 10 次以上時,則應該使用 StringBuilder。
1.2.5 避免不必要的調用 ToUpper 或 ToLower 方法
String是不變類,調用ToUpper或ToLower方法都會導致創建一個新的字符串。如果被頻繁調用,將導致頻繁創建字符串對象。這違背了前面講到的“避免頻繁創建對象”這一基本原則。
例如,bool.Parse方法本身已經是忽略大小寫的,調用時不要調用ToLower方法。
另一個非常普遍的場景是字符串比較。高效的做法是使用 Compare 方法,這個方法可以做大小寫忽略的比較,並且不會創建新字符串。
還有一種情況是使用 HashTable 的時候,有時候無法保證傳遞 key 的大小寫是否符合預期,往往會把 key 強制轉換到大寫或小寫方法。實際上 HashTable 有不同的構造形式,完全支持采用忽略大小寫的 key: new HashTable(StringComparer.OrdinalIgnoreCase)。
1.2.6 數據緩存
如果在我們的系統中,有一些基礎數據會經常用到,如果頻繁去訪問數據庫或文件系統讀取,會由於頻繁數據訪問、讀取、裝箱成數據對象而消耗時間。我們可以預先將數據讀取出這些基礎數據到緩存后使用。
1.2.7 使用字典
在涉及數據列表中有大量的數據需根據其一個標識進行訪問時,可以使用字典方式操作,這樣相比列表操作,速度會有顯著提升.
1.2.8 對象的引用代替復制
在傳遞數據值時,如果使用復制則會先創建一個新的實例,而引用傳遞的實際是對象指針,可以通過引用訪問數據。
注意:被引用后有可能會被修改,這種修改是否違背開發者的本意。
1.2.9 開啟虛擬加載
在使用WPF的一些控件如:TreeView,ListBox, ListView, TreeView加載大量數據時,會消耗較長的時間,可以使用虛擬化打開來提高加載速度。
具體操作例如:在XAML控件的屬性中加入 VirtualizingPanel.IsVirtualizing="True" 。
1.2.10 使用靜態資源
動態資源涉及到運行時查找和對象的構建, 從而會影響到性能,靜態資源是預定義的資源,可以連接到XAML屬性,它類似於編譯時綁定,不會影響性能。但也需要注意,靜態資源需要在編譯時展示。
例如:一個DataGrid控件使用一個靜態資源 Style="{StaticResource dataGridStyle}"
1.2.11 資源回收
雖然.NET提供了全套的資源管理(無用資源回收:Garbage Collection,簡稱GC),但它不是萬能的,我的系統資源是有限的,內存、文件、連接用完了要釋放。
例如:在有多次數據表讀取使用后,應釋放對象:
Void ReadData()
{
DataTable dt = new DataTable;
FillDataTable(ref dt);
......
// 使用完后
dtData.Dispose();
dtData = null;
GC.Collect();
}
1.2.12 重寫LINQ和Lambdas表達式
如果代碼需要執行很多次的時候,可能需要對LINQ或者Lambdas表達式進行重寫。
下面的例子使用LINQ以及函數式風格的代碼來通過編譯器模型給定的名稱來查找符號。
class Symbol
{
public string Name { get; private set; }
}
class Compiler
{
private List<Symbol> symbols;
public Symbol FindMatchingSymbol(string name)
{
return symbols.FirstOrDefault(s => s.Name == name);
}
}
在此過程中,這么簡單的一行代碼隱藏了基礎內存分配開銷。在上面的展開FirstOrDefault調用的例子中,代碼會調用IEnumerabole<T>接口中的GetEnumerator()方法。將symbols賦值給IEnumerable<Symbol>類型的enumerable 變量,會使得對象丟失了其實際的List<T>類型信息。這就意味着當代碼通過enumerable.GetEnumerator()方法獲取迭代器時,.NET Framework 必須對返回的值(即迭代器,使用結構體實現)類型進行裝箱從而將其賦給IEnumerable<Symbol>類型的(引用類型) enumerator變量。
解決辦法是重寫FindMatchingSymbol方法,將單個語句使用六行代碼替代,這些代碼依舊連貫,易於閱讀和理解,也很容易實現。
public Symbol FindMatchingSymbol(string name)
{
foreach (Symbol s in symbols)
{
if (s.Name == name)
return s;
}
return null;
}
代碼中並沒有使用LINQ擴展方法,lambdas表達式和迭代器,並且沒有額外的內存分配開銷。
1.2.13 減少通訊的頻次,組合指令
以我們XMC5400為例,上下位機指令通迅時,按照約定的通訊協議,遵循Finished與ACK機制,雙方都需對接到的信號作出多次反饋,完成一次指令最少也得超過15ms,如果在一個短時間段時發送大量指令,錯誤率必然會增加,因此,我們采取的方式是,使用組合指令,把原先多條指令合並為一條指令,這樣減少通訊的壓力。組合指令按照大平台原則來組合。
1.2.14 減少通訊數據量,簡化參數
有些平時不經常變的參數放到下位機,作參數簡化,減少每次通訊的字節數。例如不用每次都傳電流參數,速度加速度因為成對出現,可使用速度等級。
1.3 數據庫性能
1.3.1 使用字段數據類型的時候,盡可能的用小的數據類型
1.3.2 針對需要的字段適當的加上索引
1.3.3 應用層面進行優化
例如加上緩存,或者頁面靜態化,讓后面的請求不再查詢數據庫,這樣效率更高,將效率分攤至前端方便擴展應用服務器
1.3.4 多數據庫布署
在有大量的表與數據時,可以使用主從、讀寫分離的方式減輕數據庫服務器的壓力,避免在單台機器上過度消耗資源
1.3.5 垂直分表
如果一張表字段過多且有一部分字段經常讀寫另一部分字段不常讀寫,可以對數據表進行拆分,將不經常讀的內容和經常操作的內容放置不同表中。
1.3.6 不讀取非需內容,降低磁盤IO
1.3.7 可以使用分區表技術,將一個表分成多個不同的文件
1.3.8 盡量不要使用like
1.3.9 搜索時加上主鍵,不用或者少用全文索引
1.3.10 優化相關配置的參數
如最大連接數, 恢復模式。
1.3.11 盡量不使字段為NULL值
1.3.12 盡量不在 where 子句中使用 or 來連接條件
1.3.13 使用Bulk操作
在大批量數據寫入與更新時,可采用BULK操作,記錄越多,越體現該操作的時間性能。
1.4 無需過早優化
有些優化對在整體軟件性能言而並不能在一程度上改善,例如:我們檢索一個僅包含幾個元素的列表,那就並不需要對此再做一個字典或映射;有些功能使用頻率低,而本已經滿足要求的,可不需再做優化。