前往
大廳
主題

【Unity / System / 工具】自製通用物件池優化,順便學一下O(1)的神器:LinkedList / ArrayList

%%鼠 拒收病婿 | 2021-06-19 00:08:06 | 巴幣 2312 | 人氣 739

前言:
在上一篇【Unity C#小工具】簡單通用物件池 中提出的GCManager腳本,感謝@派大星教授的指導,今天做了些修正後更新在Github上了。

最近讀優化的東西剛好看到ArrayList竟然是O(1),當下還懷疑自己有沒有看錯,所以藉機了解一下。


主要修正的點:
以前回收語法是:
LinkedListNode<object> to_remove_obj = dicts[_key].Find(obj);
如同@派大星教授說的:
《上面這樣.Find()又要尋找整個Linklist, 時間依舊O(N), 使用Linkedlist時正確應該是把Node給使用者自行管理》

原本是希望有了這個工具,使用者就可以透過Key去取得物件就好,如果還多了"保存"的義務的話,勢必要多些使用者的麻煩。所以我就加了個dict去保管node。
static Dictionary<string, Vector2> s_m_registerScale = new Dictionary<string, Vector2>();
以內容物為key,在Instancitate時更新對應的node。

在回收時能直接使用。

因此使用者呼叫方法依舊,不用做其他改變。


[1]. When to use linkedlist over arraylist in java
LinkedList :
使用doubly-linked list結構 (頭接著上一個的尾端)
get(int index): O(n)、平均 n / 4。 只讀頭尾是O(1) 。

add(int index, E element):O(n),平均n/4。只寫頭尾是O(1) 。

remove(int index) :O(n),平均n/4。 只刪頭尾是O(1) 。

適合重複使用的資料鏈,且需頻繁插入、移除。

比ArrayList 多一些overhead,所以占用比較多的空間。




ArrayList :
Dynamically re-sizing array.

get(int index) :O(1)。
原理:假設一個int是4 bytes,起頭位址是1000,則若要訪問第20個,1000+4*20=1080,只要算一次。。

add(E element): 攤銷O(1),Array擴充最遭情況是O(n)。因為每次擴充都要安排一個新的、更大的array,然後把舊的資料複製到新的array上。

add(int index, E element):O(n)、平均O(n/2)

remove(int index):O(n),平均O(n/2)。

一開始要多給更空的空間,不然新增空間很慢。

理論上ArrayList 平均會比LinkedList 快一些,但差距不太大。

使用LinkedList 的好處是刪除頭尾是O(1),ArrayList 是O(n)。

如果記憶體大小很重要,請避免使用LinkedList 。


Algorithm           ArrayList   LinkedList
seek front            O(1)         O(1)
seek back             O(1)         O(1)
seek to index         O(1)         O(N)
insert at front       O(N)         O(1)
insert at back        O(1)         O(1)
insert after an item  O(N)         O(1)


後記:
了解兩者差別後,以Object pool來說,是大量生成一定數量的物件,之後就是取值而已,照理來說應該用ArrayList而不是LinkedList


參考資料:



送禮物贊助創作者 !
0
留言

創作回應

派大星
其實比較是術語問題啦 每個語言都在那發明自己術語 像C# List 其實內部就是動態Array而已 (加上一些設計好的API介面add, remove, ..., 還有自動管理擴容) C++還叫做"Vector", Java叫做"ArrayList", 是覺得知道內部是啥就好, 這些語言設計用詞很容易混淆和溝通困難(像是沒學過C#的人聽到List可能會直覺認為是在講LinkedList )
2021-06-21 16:43:18
派大星
然後你講的, "超過原本長度大小,就得安排一個新的、更大的陣列位置" , 其實不會多浪費的,因為擴容的方法是倍增法double sizing(通常), 擴容瞬間cost是O(N), 但在多次add分攤下來會是O(1)。假設現在容量有8個, 也剛好塞滿8個元素, 這時候你用add, 這時內部會分配新的記憶體16, 拷貝原本8個過去, 再插入, 之後的連續插入Add, 10, 11, 12...都不用分配,直到9,10,11,...16用滿。依此類推。這樣你一直連續插入Add, 假設總共觸發N次分配記憶體拷貝的Cost是1+2+4+8+...+2^(N-1) = (2^N) - 1 (等比幾何數列公式),而此時你call Add的次數(也就是容量最大Size為) 2^(N-1), Add的單次平均花費 = Add 總共次數/ N次擴容花費 = 2^(N-1) / (2^N) - 1 ~= 1/2 = O(1) 所以Add是O(1)
2021-06-21 16:54:27
派大星
" 好奇list宣告不用給長度,那預設是多長呢? " 看程式語言預設實作,像是上面的分析,一開始1+2+4的擴容顯得很蠢很浪費, 或許可以一開始直接分配一個小數目的Capacity, 如40個(Capacity = 40), Size = 0, 之後你call Add 40以後,才擴容,Capacity = 80, Size = 40, 但根據語言實作Default會不一樣。
2021-06-21 16:58:35
派大星
但一般在Constructor都可以直接要求一開始的Capacity, 如C#的List 。可以參考https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.-ctor?view=net-5.0#System_Collections_Generic_List_1__ctor_System_Int32_
2021-06-21 16:59:07
派大星
哦可以看一下這個條目: https://en.wikipedia.org/wiki/Dynamic_array Geometric expansion and amortized cost (上面Add的O(1),是多次插入後, "分攤Cost"去算的平均單次, 術語amortized cost) 。當然如果我們只在特別關注"單次Add", 那連續插入的Cost就是,O(1), O(1), O(1), O(1), 突然O(N), O(1), O(1), O(1).....突然O(N) ....這種看法, 當然要知道這種突然O(N)冒出來行為,在有些程式是不被允許的。(像是考慮到N=很大,程式突然卡住一下,雖然"平均"而言很順是O(1))。這點要注意一下。Amortized Cost分析和傳統Cost分析的差異~
2021-06-21 17:14:02
追蹤 創作集

作者相關創作

更多創作