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