本章介紹了本書其它部分未涉及到的一些編碼和設(shè)計(jì)原則。包含了一些.NET的應(yīng)用場(chǎng)景,有些不會(huì)造成太大危害,有些則會(huì)造成明顯的問題。剩下的則根據(jù)你的使用方法會(huì)產(chǎn)生不同的效果。如果要對(duì)本章節(jié)出現(xiàn)的原則做一個(gè)總結(jié),那就是:
過度的優(yōu)化會(huì)影響代碼的抽象
這意味著,當(dāng)你希望更高的優(yōu)化性能,你需要了解每個(gè)層次代碼的實(shí)現(xiàn)細(xì)節(jié)。本章會(huì)有很多相關(guān)介紹。
類 vs 結(jié)構(gòu)體
類的實(shí)例都是在堆上分配的,通過指針的引用進(jìn)行訪問。傳遞這些對(duì)象代價(jià)很低,因?yàn)樗皇且粋€(gè)指針(4或者8直接)的拷貝。然而,對(duì)象也有一些固定開銷:8或16字節(jié)(32或64位系統(tǒng))。這些開銷包括指向方法表的指針和用于其它目的同步字段。但是,如果通過調(diào)試工具查看一個(gè)空對(duì)象占用的內(nèi)存,這會(huì)發(fā)現(xiàn)大了13或者24字節(jié)(32位或64位系統(tǒng))。這是.NET的內(nèi)存對(duì)齊機(jī)制導(dǎo)致的。
而結(jié)構(gòu)體則沒上面的開銷,它的內(nèi)存使用量就是字段大小的綜合。如果結(jié)構(gòu)體是方法(函數(shù))里聲明的局部變量,則它在堆棧上分配控件。如果結(jié)構(gòu)體被聲明為類的一部分,這結(jié)構(gòu)體使用的內(nèi)存這是該類的內(nèi)存布局里的一部分(因此它會(huì)分配在堆上)。但你將結(jié)構(gòu)體傳遞給方法(函數(shù))時(shí),他將對(duì)字節(jié)數(shù)據(jù)做復(fù)制。因?yàn)樗辉诙焉?,結(jié)構(gòu)體是不會(huì)導(dǎo)致垃圾回收的。
因此這里有一個(gè)折中。你可以找到各種關(guān)于結(jié)構(gòu)體尺寸大小的建議,但這里我不會(huì)告訴你一個(gè)確切的數(shù)字。在大多數(shù)情況下,你結(jié)構(gòu)體需要保持一個(gè)比較小的尺寸,特別是他們需要經(jīng)常被傳遞,你需要保證結(jié)構(gòu)體的大小不會(huì)造成太大的問題。唯一能確定的是,你需要根據(jù)自己的應(yīng)用場(chǎng)景進(jìn)行分析。
有些情況下,效率的差別還是蠻大的。當(dāng)一個(gè)對(duì)象開銷看起來不是很多,但是對(duì)比一個(gè)對(duì)象數(shù)組和結(jié)構(gòu)體數(shù)組就可以看出差別。在32位系統(tǒng)下,假設(shè)一個(gè)數(shù)據(jù)結(jié)構(gòu)包含16字節(jié)的數(shù)據(jù),數(shù)組長(zhǎng)度是100w。
使用對(duì)象數(shù)組占用的空間
8字節(jié)數(shù)組開銷+
(4字節(jié)指針地址X1,000,000)+
((8字節(jié)頭部+16字節(jié)數(shù)據(jù))X1,000,000)
=28MB
使用結(jié)構(gòu)體數(shù)組占用的空間
8字節(jié)數(shù)組開銷+
(16字節(jié)數(shù)據(jù)X1,000,100)
=16MB
如果使用64位系統(tǒng),對(duì)象數(shù)組則使用40MB,而結(jié)構(gòu)體數(shù)組仍然是16MB。
可以看到,在一個(gè)結(jié)構(gòu)數(shù)組中,相同大小的數(shù)據(jù)占用的內(nèi)存小。隨著對(duì)象數(shù)組里對(duì)象的增加,還會(huì)增加GC的壓力。
除了空間,還有CPU效率問題。CPU有多級(jí)緩存。越靠近CPU的緩存越小,但訪問速度也會(huì)更快,對(duì)于順序保存的數(shù)據(jù)越容易優(yōu)化。
對(duì)于一個(gè)結(jié)構(gòu)體數(shù)組,他們?cè)趦?nèi)存里都是連續(xù)的值。訪問結(jié)構(gòu)體數(shù)組里數(shù)據(jù)很簡(jiǎn)單,只要找到正確的位置就可以得到對(duì)應(yīng)的值。這就意味著在大數(shù)組數(shù)據(jù)做迭代訪問有巨大的差異。如果該值已經(jīng)在CPU的告訴緩存中,它的訪問速度是要比訪問RAM要快一個(gè)數(shù)量級(jí)。
如果要訪問對(duì)象數(shù)組里的某一項(xiàng),需要先獲得該對(duì)象的指針引用,再去堆里訪問。迭代對(duì)象數(shù)組的時(shí)候,就會(huì)造成數(shù)據(jù)指針在堆里跳轉(zhuǎn),頻繁更新CPU的緩存,進(jìn)而浪費(fèi)了很多訪問CPU緩存數(shù)據(jù)機(jī)會(huì)。
在很多時(shí)候,通過改進(jìn)數(shù)據(jù)保存在內(nèi)存的位置,降低CPU訪問內(nèi)存的開銷是使用結(jié)構(gòu)體的一個(gè)主要原因,它可以顯著的提升性能。
因?yàn)榻Y(jié)構(gòu)體使用的時(shí)候總是被復(fù)制,所以編碼時(shí)要很小心,否則你會(huì)產(chǎn)生一些有趣的bug。例如下面的栗子,你是無法通過編譯的:
struct Point { public int x; public int y; }public static void Main(){ List<Point> points = new List<Point>(); points.Add(new Point() {x = 1, y = 2}); points[0].x = 3; }
問