本章介紹了本書其它部分未涉及到的一些編碼和設計原則。包含了一些.NET的應用場景,有些不會造成太大危害,有些則會造成明顯的問題。剩下的則根據你的使用方法會產生不同的效果。如果要對本章節出現的原則做一個總結,那就是:
過度的最佳化會影響程式碼的抽象
這意味著,當你希望更高的優化效能,你需要了解每個層次程式碼的實作細節。本章會有很多相關介紹。
類別的實例都是在堆上分配的,透過指標的引用進行存取。傳遞這些物件代價很低,因為它只是一個指標(4或8直接)的拷貝。然而,物件也有一些固定開銷:8或16位元組(32或64位元系統)。這些開銷包括指向方法表的指標和用於其它目的同步欄位。但是,如果透過調試工具查看一個空物件佔用的內存,這會發現大了13或24位元組(32位元或64位元系統)。這是.NET的記憶體對齊機制所導致的。
而結構體則沒上面的開銷,它的記憶體使用量就是欄位大小的綜合。如果結構體是方法(函數)裡聲明的局部變量,則它在堆疊上分配控制項。如果結構體被宣告為類別的一部分,這結構體使用的記憶體這是該類別的記憶體佈局裡的一部分(因此它會分配在堆上)。但你將結構體傳遞給方法(函數)時,他將對位元組資料做複製。因為它不在堆上,結構體是不會導致垃圾回收的。
因此這裡有一個折中。你可以找到各種關於結構體尺寸大小的建議,但這裡我不會告訴你一個確切的數字。在大多數情況下,你結構體需要保持一個比較小的尺寸,特別是他們需要經常被傳遞,你需要確保結構體的大小不會造成太大的問題。唯一能確定的是,你需要根據自己的應用場景來分析。
有些情況下,效率的差異還蠻大的。當一個物件開銷看起來不是很多,但是對比一個物件陣列和結構體陣列就可以看出差別。在32位元系統下,假設一個資料結構包含16位元組的數據,數組長度是100w。
使用物件陣列所佔用的空間
8位元組陣列開銷+
(4位元組指標位址X1,000,000)+
((8位元組頭+16位元組資料)X1, 000,000)
=28MB
使用結構體陣列所佔用的空間
8位元組陣列開銷+
(16位元組資料X1,000,100)
=16MB
如果使用64位元系統,物件陣列則使用40MB,而結構體陣列仍然是16MB。
可以看到,在一個結構數組中,相同大小的資料佔用的記憶體小。隨著物件數組裡物件的增加,也會增加GC的壓力。
除了空間,還有CPU效率問題。 CPU有多級快取。越靠近CPU的快取越小,但存取速度也會更快,對於順序保存的資料越容易最佳化。
對於一個結構體數組,他們在記憶體裡都是連續的值。存取結構體數組裡資料很簡單,只要找到正確的位置就可以得到對應的值。這就意味著在大數組資料做迭代存取有巨大的差異。如果該值已經在CPU的告訴快取中,它的存取速度是要比存取RAM快一個數量級。
如果要存取物件陣列裡的某一項,需要先取得該物件的指標引用,再去堆裡存取。迭代物件數組的時候,就會造成資料指標在堆裡跳轉,頻繁更新CPU的緩存,進而浪費了許多存取CPU緩存資料機會。
在很多時候,透過改進資料保存在記憶體的位置,降低CPU存取記憶體的開銷是使用結構體的一個主要原因,它可以顯著的提升效能。
因為結構體使用的時候總是會被複製,所以編碼時要很小心,否則你會產生一些有趣的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; }
問題是在最後一行,你試圖修改列表裡Point元素的某個值,這個操作是不行的,因為points[0]返回的是原始值的一個副本。正確的修改值的方式是
Point p = points[0]; p.x = 3; points[0] = p;
但是,你可以採取更嚴格的編碼策略:不要修改結構體。一旦結構體創建,永遠不要改變他的值。這可以消除了上面的編譯問題,並簡化了結構體的使用規則。
我之前提到,結構體應該保持小的體積,並且避免花費大量的時間來複製他們,但是偶爾也會使用一些大的結構體。例如一個最終商業流程細節的對象,裡面需要保存大量的時間戳:
class Order { public DateTime ReceivedTime { get; set; } public DateTime AcknowledgeTime { get; set; } public DateTime ProcessBeginTime { get; set; } public DateTime WarehouseReceiveTime { get; set; } public DateTime WarehouseRunnerReceiveTime { get; set; } public DateTime WarehouseRunnerCompletionTime { get; set; } public DateTime PackingBeginTime { get; set; } public DateTime PackingEndTime { get; set; } public DateTime LabelPrintTime { get; set; } public DateTime CarrierNotifyTime { get; set; } public DateTime ProcessEndTime { get; set; } public DateTime EmailSentToCustomerTime { get; set; } public DateTime CarrerPickupTime { get; set; } // lots of other data ... }
為了簡化程式碼,我們可以將時間的資料劃分到自己的子結構裡,這樣我們可以透過這樣的方式訪問Order物件:
Order order = new Order(); Order.Times.ReceivedTime = DateTime.UtcNow;
我們可以把資料全部放到自己的類別裡:
class OrderTimes { public DateTime ReceivedTime { get; set; } public DateTime AcknowledgeTime { get; set; } public DateTime ProcessBeginTime { get; set; } public DateTime WarehouseReceiveTime { get; set; } public DateTime WarehouseRunnerReceiveTime { get; set; } public DateTime WarehouseRunnerCompletionTime { get; set; } public DateTime PackingBeginTime { get; set; } public DateTime PackingEndTime { get; set; } public DateTime LabelPrintTime { get; set; } public DateTime CarrierNotifyTime { get; set; } public DateTime ProcessEndTime { get; set; } public DateTime EmailSentToCustomerTime { get; set; } public DateTime CarrerPickupTime { get; set; } } class Order { public OrderTimes Times; }
但是,这样会为每个Order对象引入额外的12或者24字节的开销。如果你需要将OrderTimes对象作为一个整体传入各种方法函数里,这也许是有一定道理的,但为什么不把Order对象传入方法里呢?如果你同时有数千个Order对象,则可能会导致更多的垃圾回收,这是额外的对象增加的引用导致的。
相反,将OrderTime更改为结构体,通过Order上的属性(例如:Order.Times.ReceivedTime)访问OrderTImes结构体的各个属性,不会导致结构体的副本(.NET会对这个访问做优化)。这样OrderTimes结构体基本上成为Order类的内存布局的一部分,几乎和没有子结构体一样了,你拥有了更加漂亮的代码。
这种技术确实违反了不可变的结构体原理,但这里的技巧就是将OrderTimes结构的字段视为Order对象的字段。你不需要将OrderTimes结构体作为一个实体进行传递,它只是一个代码组织方式。
以上是總結一些編碼和設計原則實例的詳細內容。更多資訊請關注PHP中文網其他相關文章!