|
|
|
|
|
測試發(fā)現(xiàn),C#中 List<Struct>
的分配速度比 List<Class>
快 15 倍,這是一個(gè)令人驚詫的結(jié)果,但我想知道為什么會(huì)這樣!
struct比class快15倍
下面是測試代碼:
public class PointClass
{
public int X { get; set; }
public int Y { get; set; }
}
public struct PointStruct
{
public int X { get; set; }
public int Y { get; set; }
}
[Benchmark]
public void ListOfClassesTest()
{
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 ListOfStructsTest()
{
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 });
}
ListOfStructsTest
和ListOfClassesTest
方法幾乎相同。第一個(gè)方法分配一百萬個(gè)PointClass
實(shí)例并將它們添加到列表中,而第二個(gè)方法分配一百萬個(gè)PointStruct
實(shí)例并將它們也添加到列表中。類型PointClass
和PointStruct
具有相同的成員,但唯一的小而關(guān)鍵的區(qū)別是,PointClass
是一個(gè)類,而是PointStruct
一個(gè)結(jié)構(gòu)。
測試結(jié)果非常令人印象深刻:
ListOfStructsTest
方法比ListOfClassesTest
方法快 15 倍以上。
下面我們試著分析一下為什么會(huì)出現(xiàn)如此巨大的時(shí)差。
在堆上分配引用類型實(shí)例和結(jié)構(gòu)實(shí)例的區(qū)別
對我們來說,第一件事是理解在堆上分配一個(gè)引用類型實(shí)例和在堆棧上分配一個(gè)結(jié)構(gòu)實(shí)例之間的區(qū)別。
public void Test()
{
var obj = new object(); //引用類型分配
int x = 12; //值類型分配
}
在托管堆中為引用類型分配內(nèi)存的時(shí)間通常是快速操作,對象被連續(xù)分配和存儲(chǔ)。公共語言運(yùn)行時(shí)具有指向內(nèi)存中第一個(gè)空閑空間的指針,分配新對象涉及將新對象的大小添加到指針。
將對象放在托管堆上后,其地址將寫回在堆棧上創(chuàng)建的引用obj
。
總的來說整個(gè)過程還是挺便宜的,然而,為引用類型的對象分配內(nèi)存的過程并不總是那么容易,并且可能涉及額外的繁重部分。
如果引用類型大于 85K 字節(jié),運(yùn)行時(shí)將花費(fèi)更多時(shí)間在大對象堆中尋找合適的位置來存儲(chǔ)對象,因?yàn)槟抢锏膬?nèi)存是碎片化的(空閑塊或地址空間中的“空洞”)。
在小對象堆中沒有更多可用空間來存儲(chǔ)應(yīng)用程序請求的對象的情況下,引用類型對象分配很慢。當(dāng)發(fā)生這種情況時(shí),公共語言運(yùn)行時(shí)需要運(yùn)行垃圾收集過程。如果垃圾收集器沒有釋放足夠的內(nèi)存,運(yùn)行時(shí)會(huì)請求額外的虛擬內(nèi)存頁面。
如何在堆棧上分配一個(gè)值類型實(shí)例?
為值類型分配內(nèi)存幾乎是即時(shí)操作,分配時(shí)間幾乎不依賴于值類型的大小。運(yùn)行時(shí)唯一應(yīng)該做的就是創(chuàng)建一個(gè)適當(dāng)大小的堆棧幀來存儲(chǔ)值類型和修改堆棧指針。
要點(diǎn)是,將值類型的實(shí)例放在堆棧上是快速的,更重要的是,與在堆上分配引用類型對象相比,它在時(shí)間上是一個(gè)確定性的過程。
實(shí)例分析
現(xiàn)在讓我們回到我們的例子。
當(dāng)一個(gè)引用類型的一百萬個(gè)實(shí)例被分配時(shí),它們被一個(gè)一個(gè)地推入托管堆,并且引用被存儲(chǔ)回集合實(shí)例。實(shí)際上,會(huì)有100萬+1個(gè)對象進(jìn)入內(nèi)存。
然而,當(dāng)一個(gè)值類型的一百萬個(gè)實(shí)例被分配時(shí),只有一個(gè)對象被推入托管堆,它是一個(gè)集合的實(shí)例。一百萬個(gè)結(jié)構(gòu)將嵌入到List<T>
實(shí)例中。創(chuàng)建實(shí)例后,運(yùn)行時(shí)唯一要做的就是用數(shù)據(jù)填充List<T>
。
在為大型集合選擇結(jié)構(gòu)而不是類時(shí),開發(fā)人員不僅受益于快速分配時(shí)間,還受益于發(fā)布時(shí)間。
如果開發(fā)人員分配了一百萬個(gè)PointClass
實(shí)例,在“標(biāo)記和清理”階段,垃圾收集器將不得不掃描一百萬個(gè)對象并檢查每個(gè)對象是否還有引用。然后,在“壓縮”階段,垃圾收集器將不得不移動(dòng)一百萬個(gè)對象。最終,存儲(chǔ)在List<PointClass>
實(shí)例中的地址應(yīng)該更新為新地址。這是很多工作。
但對于垃圾收集器而言,當(dāng)開發(fā)人員分配一百萬個(gè)List<PointStruct>
實(shí)例時(shí)情況會(huì)好得多,因?yàn)槔占鞅仨毷褂猛泄芏阎械奈ㄒ粚?shí)例PointStruct
。
總結(jié)
結(jié)構(gòu)(struct)可以比類(class)更高效,但在用struct
或其他方式替換class
關(guān)鍵字之前,請始終仔細(xì)分析你的具體情況。程序員的工作不是盲目地遵循建議或最佳實(shí)踐,而是選擇最合適的工具、方法、方式來以最佳方式解決各自的獨(dú)特案例。
相關(guān)文章