|
|
|
|
|
為什么要關(guān)注代碼性能?因?yàn)榇a性能會影響程序的執(zhí)行效率,在客戶端就表現(xiàn)為等待時間的長短,這就影響到用戶的使用體驗(yàn),因此,我們開發(fā)人員不得不對代碼性能給予足夠的重視。本文中,我將總結(jié)11個提高C#代碼性能的技巧。
1、循環(huán)并行化
foreach
是當(dāng)今最常用的循環(huán),僅次于 for
循環(huán)。它提供了靈活性,不用擔(dān)心循環(huán)計(jì)數(shù),它會運(yùn)行到集合的長度。
在foreach
循環(huán)中,迭代在同一線程中一個接一個地執(zhí)行,因此總執(zhí)行時間將隨集合大小線性增長。
使用 .Net Framework 4.0 提供給開發(fā)者的并行版本的foreach
循環(huán),性能可以有很大的提升。
Parallel.ForEach
可用于實(shí)現(xiàn) IEnumerable<T>
接口的任何集合,就像常規(guī)的 foreach
循環(huán)一樣。Parallel.ForEach
將代替開發(fā)人員做很多工作:它根據(jù)機(jī)器上可用的核心將集合分成塊,在單獨(dú)的線程中調(diào)度和執(zhí)行塊,然后組合結(jié)果。
以下是 foreach
循環(huán)的并行版本與普通版本之間的性能比較:
如果集合很小并且單次迭代的執(zhí)行時間很快,則將 foreach
切換為 Parallel
。Parallel.ForEach
可能使性能變差,因?yàn)樗ㄟ^拆分和收集結(jié)果增加了管理循環(huán)的成本。
2、使用 StringBuilder 而不是字符串連接
在 C# 中,字符串連接在每次調(diào)用時都會創(chuàng)建一個新的字符串對象。如果你連接大量字符串,這可能效率低下。為避免這種情況,請改用 StringBuilder
。
例子:
var sb = new StringBuilder();
sb.Append("Hello ");
sb.Append("World");
var result = sb.ToString();
3、使用 LINQ 進(jìn)行過濾和排序
在 C# 中,LINQ
提供了用于篩選、排序和操作集合的強(qiáng)大工具。LINQ
比手動遍歷集合并對每個項(xiàng)目執(zhí)行操作更有效。
例子:
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var result = numbers.Where(n => n % 2 == 0).OrderByDescending(n => n);
4、一次性物化 LINQ 查詢
何為一次性物化 LINQ
查詢?即是不要使用表達(dá)式樹來保存結(jié)果,而是盡可能通過執(zhí)行來提取結(jié)果。
使用 IEnumerable
或 IQueryable
接口編寫 LINQ
查詢時,開發(fā)人員可以具體化(調(diào)用 ToList
或 ToArray
類似方法)或不立即具體化查詢(延遲加載)。
如果查詢不是通過調(diào)用 ToList/ToArray
方法實(shí)現(xiàn)的,多次迭代集合將影響應(yīng)用程序的性能。
這是一個示例,我們在其中測試延遲加載 LINQ
查詢與物化查詢的性能。
[MemoryDiagnoser]
public class LinqTest {
[Benchmark]
public void NoMaterializedTest() {
var list = Enumerable.Range(0, 50000000);
var filteredlist = list.Where(element => element % 100000 == 0);
foreach(var element in filteredlist) {
//some logic here
}
foreach(var element in filteredlist) {
// some logic here
}
foreach(var element in filteredlist) {
// some logic here
}
}
[Benchmark]
public void MaterializedTest() {
var list = Enumerable.Range(0, 5000000);
var filteredlist = list.Where(element => element % 10000 == 0).ToList();
//ToList() method will execute the expression tree and get the result for future purpose.
foreach(var element in filteredlist) {
// some logic here
}
foreach(var element in filteredlist) {
// some logic here
}
foreach(var element in filteredlist) {
// some logic here
}
}
public static void Main() {
//Execute LinqTest class methods to check the benchmark
var result = BenchmarkRunner.Run < LinqTest > ();
}
在這里可以看到差異,與延遲加載 LINQ
查詢相比,物化查詢的性能提高了三倍。盡管這種方法并不總是有效,因?yàn)槲覀兛赡懿粫啻螆?zhí)行相同的結(jié)果集。在具體化 LINQ
查詢之前始終檢查大小寫。
5、異步編程
異步編程是一種允許你的代碼在等待長時間運(yùn)行的操作完成時繼續(xù)執(zhí)行的技術(shù)。這在處理 I/O 操作時特別有用,例如讀取和寫入文件或發(fā)出 Web 請求。
在 C# 中,異步編程是使用 async
和 await
關(guān)鍵字實(shí)現(xiàn)的。
例子:
async Task<string> ReadFileAsync(string filePath)
{
using (var reader = new StreamReader(filePath))
{
return await reader.ReadToEndAsync();
}
}
6、對大型集合使用 IEnumerable 而不是列表
在 C# 中,列表是存儲和操作對象集合的有效方式。但是,將 IEnumerable
用于廣泛的集合會更有效,因?yàn)樗苊饬藙?chuàng)建和維護(hù) List
對象的開銷。
例子:
public IEnumerable<int> GetNumbers()
{
for (int i = 0; i < 1000000; i++)
{
yield return i;
}
}
foreach (var number in GetNumbers())
{
Console.WriteLine(number);
}
7、使用枚舉標(biāo)志進(jìn)行位運(yùn)算
在 C# 中,枚舉可以用作標(biāo)志,這使您可以對它們執(zhí)行按位運(yùn)算。這在將多個值存儲在單個變量中時非常有用。
例子:
[Flags]
public enum Colors
{
None = 0,
Red = 1,
Green = 2,
Blue = 4
}
var colors = Colors.Red | Colors.Green;
8、對小型數(shù)據(jù)結(jié)構(gòu)使用結(jié)構(gòu)而不是類
在 C# 中,結(jié)構(gòu)是值類型,類是引用類型。結(jié)構(gòu)分配在棧上,而類分配在堆上。對于小型數(shù)據(jù)結(jié)構(gòu),使用結(jié)構(gòu)可以更有效,因?yàn)樗苊饬硕逊峙浜屠占拈_銷。
例子:
public struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
var p = new Point(10, 20);
9、使用泛型
泛型是 C# 中的一項(xiàng)強(qiáng)大功能,可讓你創(chuàng)建可重用的代碼。通過使用泛型類型參數(shù)定義類或方法,你可以創(chuàng)建類型安全、可重用的組件,該組件可以處理各種數(shù)據(jù)類型。這可以通過減少你需要編寫的重復(fù)代碼來幫助提高性能。
下面是使用泛型創(chuàng)建可重用排序方法的示例:
public static void Sort<T>(T[] array) where T : IComparable<T>
{
for (int i = 0; i < array.Length; i++)
{
for (int j = i + 1; j < array.Length; j++)
{
if (array[j].CompareTo(array[i]) < 0)
{
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
}
}
10、使用接口實(shí)現(xiàn)松散耦合
在C#中,接口為組件之間的松散耦合提供了一種強(qiáng)大的機(jī)制。通過定義服務(wù)或功能的接口,您可以確保組件可以輕松換出或更換,而不會影響系統(tǒng)的其余部分。這種方法可以通過最小化更改對整個系統(tǒng)的影響來提高代碼性能。
例如,考慮以下定義用于訪問數(shù)據(jù)的通用存儲庫的接口:
interface IRepository<T> {
void Save(T entity);
T Get(int id);
}
這個接口可以定義一個通用的數(shù)據(jù)訪問層,可以很容易地替換或更新而不影響系統(tǒng)的其余部分。
11、盡可能使用 Struct 而不是 Class
大多數(shù)時候我看到開發(fā)人員創(chuàng)建類時沒有對 Class
和 Struct
進(jìn)行很好的比較。開發(fā)人員通常可能需要分配一個數(shù)組或 List<T>
在內(nèi)存中存儲數(shù)萬個對象。這個任務(wù)可以使用類或結(jié)構(gòu)來解決。
正如我們所看到的, ListOfClassObjectTest
和ListOfStructsObjectTest
之間的唯一區(qū)別,是第一個測試創(chuàng)建類的實(shí)例,而第二個測試創(chuàng)建結(jié)構(gòu)的實(shí)例。
如下示例中,PointClass
和 PointStruct
的代碼是相同的:
//Reference type. Stored on Heap.
public class PointClass {
public int X {
get;
set;
}
public int Y {
get;
set;
}
}
//Value type. Stored on Stack
public struct PointStruct {
public int X {
get;
set;
}
public int Y {
get;
set;
}
}
[MemoryDiagnoser]
public class ClassVsStruct {
[Benchmark]
public void ListOfClassObjectsTest() {
const int length = 1000000;
var items = new List < PointClass > (length);
for (int i = 0; i < length; i++) {
items.Add(new PointClass() {
X = i, Y = i
});
}
}
[Benchmark]
public void ListOfStructsObjectTest() {
const int length = 1000000;
var items = new List < PointStruct > (length);
for (int i = 0; i < length; i++) {
items.Add(new PointStruct() {
X = i, Y = i
});
}
}
}
可以看到,使用結(jié)構(gòu)的代碼比使用類的代碼運(yùn)行速度快 15 倍。
對于類,CLR 必須將一百萬個對象分配給托管堆并將它們的引用存儲回List<T>
集合。在結(jié)構(gòu)的情況下,將有唯一的對象分配到托管堆中 ,它是List<T>
集合的實(shí)例。一百萬個結(jié)構(gòu)將嵌入到該單個集合實(shí)例中。
總結(jié)
以上11點(diǎn)希望對各位開發(fā)者提升代碼質(zhì)量有所幫助,通過這些開發(fā)技巧,開發(fā)人員可以創(chuàng)建執(zhí)行速度更快、占用系統(tǒng)資源更少的應(yīng)用程序。