背景
v4
中使用了鏈表存儲了不同大小的內存塊的方式進行內存池的實現(參考這篇v4內存複用機制),實際測試中發現內存浪費比較嚴重,因此如何設計出使用效率高,操作簡潔的內存池就成了 v5
的一個任務。
使用 make
使用 go 原生的內存分配,意味着交給 GC
來回收,在m7s
中測試發現gc
佔據非常大的開銷。
自定義內存分配
C 風格的內存分配
void * mem = malloc(100)
free(mem)
這種分配方式最廣爲人知,也是最簡潔易懂的,因此如果能實現這種方式,是最佳的。
設想一下
func (ma *MemoryAllocator) Malloc(size int) (memory []byte) {
}
func (ma *MemoryAllocator) Free(memory []byte) {
}
問題:如何在 Free
的時候知道是哪塊內存?如果把這個字節數組直接存儲就會回到 v4
的版本,顯然不是我們想要的。 我們想要的是在一塊大的數組中切割分配,這樣纔能有效利用內存。
切片分配
假設有一個大數組,用來緩存內存,防止 GC
var mem = make([]byte,65535)
分配內存,就是切片
s1 := mem[0:1024]
分配第二塊內存s2
s2 := mem[1024:2048]
最簡單的方式就是記錄一個已經分配的索引,第一次爲 0
,第二次爲 1024
type MemoryAllocator struct {
start int64
memory []byte
}
func (ma *MemoryAllocator) Malloc(size int) (memory []byte) {
memory = ma.memory[ma.start:size]
ma.start+= size
return
}
回收
內存切出去容易,如何回收呢?
ma.Free(s2)
如何知道 s2
屬於哪一部分呢?即使知道,如何修改原來的結構體使得下次分配可以利用回收過的內存呢?
使用附加信息
這種方式,就和 v4
一樣,將額外的信息隨同分配的內存給出去,回收的時候再一起帶回來,但是不夠簡潔,我們希望回收的時候就是傳[]byte
判斷指針
我們知道同一塊內存的底層的指針值肯定是相同的,即使切片被淺複製也一樣。
ptr := uintptr(unsafe.Pointer(&mem[0]))
當然,有可能傳入的切片長度爲 0,可以用下面的方法規避
ptr := uintptr(unsafe.Pointer(&mem[:1][0]))
有了這個指針值,我只需要和內存池的起始指針進行比較,就可以得到在內存池中的偏移。從而爲回收奠定基礎。
標記
爲了回收再利用,需要對可以分配的內存信息進行存儲,這個信息就是標記偏移地址段,即 start:end。 可以用鏈表存儲,[start,end],[start,end],[start,end],[start,end]
每一段代表可用的內存。當回收內存時,只需按照大小順序插入這個鏈表即可
用數組也可以,但是由於數組對隨機插入性能較差,因此用鏈表更合適
當然如果前一個 end 等於下一個 start,就可以合併: 例如[1:1024],[1024,2048]
就可以合併成 [1:2048]
,相當於碎片整理。
實現
type MemoryAllocator struct {
start int64
memory []byte
Size int
blocks *List[Block]
}
func NewMemoryAllocator(size int) (ret *MemoryAllocator) {
ret = &MemoryAllocator{
Size: size,
memory: make([]byte, size),
blocks: NewList[Block](),
}
ret.start = int64(uintptr(unsafe.Pointer(&ret.memory[0])))
ret.blocks.PushBack(Block{0, size})
return
}
具體代碼可以到倉庫 github.com/langhuihui/monibuca
的 v5
分支裏面找到
進階
單個內存分配器可分配的內存有限,那麼一個可以不斷增長的需求如何滿足呢? 可以實現動態創建內存分配器的高階內存分配器就可以解決了
type ScalableMemoryAllocator []*MemoryAllocator
原理也很簡單,不夠就創建,Free 的時候就挨個查找。 優化後內存不再呈現鋸齒狀了