設計良好的系統,除了架構層面的優良設計外,剩下的大部分就在於如何設計良好的代碼,.NET提供了不少的類型,這些類型很是靈活,也很是好用,好比List,Dictionary、HashSet、StringBuilder、string等等。在大多數狀況下,你們都是看着業務須要直接去用,彷佛並無什麼問題。從個人實際經驗來看,出現問題的狀況確實是少之又少。以前有朋友問我,我有沒有遇到過內存泄漏的狀況,我說我寫的系統沒有,可是同事寫的我遇到過幾回。html
爲了記錄曾經發生的問題,也爲了之後能夠避免相似的問題,總結這篇文章,力圖從數據統計角度總結幾個有效提高.NET性能的方法。編程
本文基於.NET Core 3.0 Preview4,採用[Benchmark]進行測試,若是不瞭解Benchmark,建議瞭解完以後再看本文。架構
在.NET裏,List、Dictionary、HashSet這些集合類型都具備初始容量,當新增的數據大於初始容量時,會自動擴展,可能你們在使用的時候不多注意這個隱藏的細節(此處暫不考慮默認初始容量、加載因子、擴容增量)。框架
自動擴容給使用者的感知是無限容量,若是用的不是很好,可能會帶來一些新的問題。由於每當集合新增的數據大於當前已經申請的容量的時候,會再申請更大的內存容量,通常是當前容量的兩倍。這就意味着咱們在集合操做過程當中可能須要額外的內存開銷。ide
在本次測試中,我用到了四種場景,可能並非很徹底,可是頗有說明性,每一個方法都是循環了1000次,時間複雜度均爲O(1000):函數
- DynamicCapacity:不設置默認長度
- LargeFixedCapacity:默認長度爲2000
- FixedCapacity:默認長度爲1000
- FixedAndDynamicCapacity:默認長度爲100
下圖爲List的測試結果,能夠看到其綜合性能排名是FixedCapacity>LargeFixedCapacity>DynamicCapacity>FixedAndDynamicCapacity性能
下圖爲Dictionary的測試結果,能夠看到其綜合性能排名是FixedCapacity>LargeFixedCapacity>FixedAndDynamicCapacity>DynamicCapacity,在Dictionary場景中,FixedAndDynamicCapacity和DynamicCapacity的兩個方法性能相差並不大,多是量還不夠大測試
下圖爲HashSet的測試結果,能夠看到其綜合性能排名是FixedCapacity>LargeFixedCapacity>FixedAndDynamicCapacity>DynamicCapacity,在HashSet場景中,FixedAndDynamicCapacity和DynamicCapacity的兩個方法性能相差仍是很大的ui
綜上所述:this
一個恰當的容量初始值,能夠有效提高集合操做的效率,若是不太好設置一個準確的數據,能夠申請比實際稍大的空間,可是會浪費內存空間,並在實際上下降集合操做性能,編程的時候須要特別注意。
如下是List的測試源碼,另兩種類型的測試代碼與之基本一致:
1: public class ListTest
2: {
3: private int size = 1000;
4:
5: [Benchmark]
6: public void DynamicCapacity()
7: {
8: List<int> list = new List<int>();
9: for (int i = 0; i < size; i++)
10: {
11: list.Add(i);
12: }
13: }
14:
15: [Benchmark]
16: public void LargeFixedCapacity()
17: {
18: List<int> list = new List<int>(2000);
19: for (int i = 0; i < size; i++)
20: {
21: list.Add(i);
22: }
23: }
24:
25: [Benchmark]
26: public void FixedCapacity()
27: {
28: List<int> list = new List<int>(size);
29: for (int i = 0; i < size; i++)
30: {
31: list.Add(i);
32: }
33: }
34:
35: [Benchmark]
36: public void FixedAndDynamicCapacity()
37: {
38: List<int> list = new List<int>(100);
39: for (int i = 0; i < size; i++)
40: {
41: list.Add(i);
42: }
43: }
44: }
結構體是值類型,引用類型和值類型之間的區別是引用類型在堆上分配並進行垃圾回收,而值類型在堆棧中分配並在堆棧展開時被釋放,或內聯包含類型並在它們的包含類型被釋放時被釋放。 所以,值類型的分配和釋放一般比引用類型的分配和釋放開銷更低。
通常來講,框架中的大多數類型應該是類。 可是,在某些狀況下,值類型的特徵使得其更適合使用結構。
若是類型的實例比較小而且一般生存期較短或者一般嵌入在其餘對象中,則定義結構而不是類。
該類型具備全部如下特徵,能夠定義一個結構:
它邏輯上表示單個值,相似於基元類型(int
, double
,等等)
它的實例大小小於 16 字節
它是不可變的
它不會頻繁裝箱
在全部其餘狀況下,應將類型定義爲類。因爲結構體在傳遞的時候,會被複制,所以在某些場景下可能並不適合提高性能。
以上摘自MSDN,可點擊查看詳情
能夠看到Struct的平均分配時間只有Class的六分之一。
如下爲該案例的測試源碼:
1: public struct UserStructTest
2: {
3: public int UserId { get;set; }
4:
5: public int Age { get; set; }
6: }
7:
8: public class UserClassTest
9: {
10: public int UserId { get; set; }
11:
12: public int Age { get; set; }
13: }
14:
15: public class StructTest
16: {
17: private int size = 1000;
18:
19: [Benchmark]
20: public void TestByStruct()
21: {
22: UserStructTest[] test = new UserStructTest[this.size];
23: for (int i = 0; i < size; i++)
24: {
25: test[i].UserId = 1;
26: test[i].Age = 22;
27: }
28: }
29:
30: [Benchmark]
31: public void TestByClass()
32: {
33: UserClassTest[] test = new UserClassTest[this.size];
34: for (int i = 0; i < size; i++)
35: {
36: test[i] = new UserClassTest
37: {
38: UserId = 1,
39: Age = 22
40: };
41: }
42: }
43: }
字符串是不可變的,每次的賦值都會從新分配一個對象,當有大量字符串操做時,使用string很是容易出現內存溢出,好比導出Excel操做,因此大量字符串的操做通常推薦使用StringBuilder,以提升系統性能。
如下爲一千次執行的測試結果,能夠看到StringBuilder對象的內存分配效率十分的高,固然這是在大量字符串處理的狀況,少部分的字符串操做依然可使用string,其性能損耗能夠忽略
這是執行五次的狀況,能夠發現雖然string的內存分配時間依然較長,可是穩定且錯誤率低
測試代碼以下:
1: public class StringBuilderTest
2: {
3: private int size = 5;
4:
5: [Benchmark]
6: public void TestByString()
7: {
8: string s = string.Empty;
9: for (int i = 0; i < size; i++)
10: {
11: s += "a";
12: s += "b";
13: }
14: }
15:
16: [Benchmark]
17: public void TestByStringBuilder()
18: {
19: StringBuilder sb = new StringBuilder();
20: for (int i = 0; i < size; i++)
21: {
22: sb.Append("a");
23: sb.Append("b");
24: }
25:
26: string s = sb.ToString();
27: }
28: }
析構函數標識了一個類的生命週期已調用完畢時,會自動清理對象所佔用的資源。析構方法不帶任何參數,它其實是保證在程序中會調用垃圾回收方法 Finalize(),使用析構函數的對象不會在G0中處理,這就意味着該對象的回收可能會比較慢。一般狀況下,不建議使用析構函數,更推薦使用IDispose,並且IDispose具備恰好的通用性,能夠處理託管資源和非託管資源。
如下爲本次測試的結果,能夠看到內存平均分配效率的差距仍是很大的
測試代碼以下:
1: public class DestructionTest
2: {
3: private int size = 5;
4:
5: [Benchmark]
6: public void NoDestruction()
7: {
8: for (int i = 0; i < this.size; i++)
9: {
10: UserTest userTest = new UserTest();
11: }
12: }
13:
14: [Benchmark]
15: public void Destruction()
16: {
17: for (int i = 0; i < this.size; i++)
18: {
19: UserDestructionTest userTest = new UserDestructionTest();
20: }
21: }
22: }
23:
24: public class UserTest: IDisposable
25: {
26: public int UserId { get; set; }
27:
28: public int Age { get; set; }
29:
30: public void Dispose()
31: {
32: Console.WriteLine("11");
33: }
34: }
35:
36: public class UserDestructionTest
37: {
38: ~UserDestructionTest()
39: {
40:
41: }
42:
43: public int UserId { get; set; }
44:
45: public int Age { get; set; }
46: }