這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
2013-10-27
Go手動記憶體配置
Go手動記憶體配置
用Go的時候,有時候又想自己管理記憶體。所以決定寫個手動記憶體管理的包吧。就當無聊練練手...
總體設計
兩級分配。較大記憶體以頁為單位分配,每頁4k。分配出去的大塊記憶體只能是1頁,2頁,3頁...較小記憶體使用量buddy演算法結合分配池的方式進行分配。buddy演算法主要是方便回收,對於各種不規則大小則分別維護一個free的鏈表。
整體上基本類似於Go本身使用的記憶體管理演算法,除了沒有引入記憶體回收標記資訊,以及對小對象使用buddy分配演算法。
buddy演算法
管理結點使用的記憶體跟最小最大結點相關。如果最小單元太小,則浪費過多的管理結點。管理結點數目=最小單元數目*2 如果最大單元過大,則需要用更長的整型來記錄大小,單個管理結點的大小增加。
buddy演算法管理的每塊記憶體大小為選擇為4k。更大記憶體則用頁分配器管理。最小單元大小選擇為32B,這樣可以用8位儲存結點大小(8位最多可以表示255個單元結點,4k包含128個大小32的單元結點)。
管理結點使用uint8,一頁中有64個結點,所以4k的管理結點只需要64B,很省!
Gosize [64]uint8
改進的buddy演算法
本來很喜歡buddy分配器的,不過buddy適用性不那麼好,能分配的大小類型只有32B,64B,128B,256B,512B,1024B,2048B,4096B。基本的buddy演算法有個很嚴重的缺點,內部片段浪費很嚴重。比如申請66B的記憶體,會分配128B,太浪費了!
所以我做了一些改動,用不同的最小單元的buddy分配器進行插值。比如說基準的最小單無大小是16,那麼它負責的大小是:
16 32 64 128 256 512 1024 2048
如果我選擇另一個最小單元為24的buddy分配器,它負責的大小是:
24 48 96 192 384 768 1536 3072
二者配合一起用,記憶體利用率會高很多。依此下去,可以弄一些最小單元大小不一的buddy分配器,那麼它就可以彌補2的指數倍導致過多內部片段的問題了。我計算了一個,最小單元使用到56的時候記憶體浪費率已經不會超過1:1.125,大概是1.1111多的樣子。
但是這樣做引出了另一個問題,外部片段。就是各個buddy分配器之間,必須要正好湊滿4096B才不至於浪費。
16 32 64 128 256 512 1024 204824 48 96 192 384 768 1536 3072 (768 + 1280) A40 80 160 320 640 1280 2560 (640 + 1408) B56 112 224 448 896 1792 3584 (896 + 1152) C72 144 288 576 1152 2304 (1152 + 896) D88 176 352 704 1408 2816 (704 1344) (1408 640) E104 208 416 832 1664 3328 (1664 384) F120 240 480 960 1920 3840 () G168 336 672 1344 2688 H
所以我精心設計了一些組合,剛好湊起來4096B的。
組合 1408 1408 1280 E E B組合 1536 1280 1280 A B B組合 896 1152 896 1152 C D C D組合 768 1280 768 1280 A B A B組合 1408 1344 1344 E H H組合 896 1152 C D
事實上只用到E就夠了,也就是前三個組合。所以選用的buddy分配器為:
B 1280E 1408A 1536D 1152C 896
小對象分配池
buddy的使用其實更多是想方便回收時的記憶體合并。為了加快分配速度,維護一個分配池,對每個大小類別維持一個free鏈表,分配時直接從鏈表中劃一個出去,回收看是否需要進行合并。
一個大的buddy管理的塊被切小後,變成好多小對象掛到free鏈表中。
頁分配器
這個就跟Go語言採用同樣的方式弄了。每次分配時是幾個頁大小的記憶體,回收用位元影像的方式來進行合并回收。
好吧,不多廢話,上代碼