List<T> 在 .net 的程式裡應該可算是隨處可見。它將 Array 包裝起來,讓我們在使用時可以達到動態增加元素的效果。
然而,這種便利性嚴格來說是有代價的。如果你使用的情境,是會不斷的大量新增元素,並且也沒有在初始時就給定 capacity 的話,List<T> 的動態 extend 可能會使用過多的記憶體與 GC 更大的負擔。
通常都是初始的時候沒有給 capacity 才會在之後 Add 新元素時需要做 extend 的動作。
在 new list 時沒有給 capacity,預設會有 0 的容量。然後當程式第一次 Add 的時候,就會馬上去從 heap allocate 4 個 list 元素的位置出來(如果是 reference type 的話,只是存放指標位置的記憶體空間)。
再接下來的 Add 都會去判斷元素的容量夠不夠。當他不夠的時候,就會以現在的 容量 乘以 2,再去 heap 找一塊可以用的空間重新 allocate。
而細節就是這段 allocate 的動作。因為每次的重新 allocate,它除了要重新 allocate 空間,還要再做一次搬的動作,把舊資料搬到這塊新的記憶體空間去。最後再把舊的記憶體空間直接捨棄,等著被 GC。
從這個順序來看,每一次所謂動態 extend,都有三件很浪費資源的事情。
除了程式比較好寫跟閱讀性較好之外,暫時想不到什麼其他優點……
就是剛才提到的會較浪費資源,尤其是當你元素越多或是頻繁的 new list 物件來使用時,這些問題就會慢慢的浮現出來。
在使用時,儘量先給定 capacity 大小(如果可以的話)。但這也是要 by 情況來看。如果要處理的資料偏少量,或是資料筆數難以事先判定的話,這個想法也是有困難的。
用 List 來接大量物件算是一把雙面刃。因為在 happy flow 的使用情境下,讓它自己動態處理記憶的部份,其實都不會有問題。
可是當某一支程式效能就是要計較到記憶體與 GC 使用時,也許可考慮換個方向來思考。例如,如果要處理的資料是經過某段邏輯處理就不再需要,那可能也不需要一次把那麼大的資料都放到記憶體裡。
這時候如果改用 stream 或是 RingBuffer 的資料結構來解,可能會會更漂亮一些。
之前在練習 golang 時候,有讀到像 Slice 這種集合物件在 extend 的時候,做法也是類似。
像是 goloan 在元素只有在某個數值前(AI 說是 256)是直接 allocate 2 倍空間;但當元素超過 256 個後,每次的 extend 就會從 2 倍改回 1.5 倍(新版好像又調成 1.25 倍了)。
簡單來說,這種所謂的動態其實背後一定有它的代價。所謂的方便使用確實是適合一版的 happly flow 開發。但當你今天是要讓程式跑到一個極致時,這些細節可能都要自己操控好會比較好。