使用内存管理的好处
在cpp中,我们随意的申请使用空间,就会导致频繁使用系统调用,指针乱飞,内存空间不连续的问题
在go中,虽然go帮我们处理了系统调用的问题,但是依然会导致内存空间不连续
在ram中,连续的内存空间可以提高内存的访问效率
Arena
内存管理的思想是提前申请一块内存,根据我们的程序进行内存的分配,将连续的部分放到一块,提高程序执行效率
一些定义
考虑到 Arena 要配合 KV 引擎食用,在 KV 中我们只加不删,这样就可以让内存池只加不删
在 KV 引擎中数据积攒到一定程度就会持久化成sst文件,使整个 Arena 的内存会得到释放
释放整块内存借用 go 语言的内存管理即可
type Arena struct {
offset uint32 //偏移地址(当前已经存储的数据量)
buf []byte //存储地址
}
如果我们知道一段数据在 buf
中的地址,和这段数据的长度,就可以从 Arena
中读取到这段数据
初始化
offset
指向第一块没被使用过的内存
func newArena(n int64) *Arena {
out := &Arena{
offset: 0,
buf: make([]byte, n),
}
return out
}
申请内存
输入需要申请的内存大小,返回申请完内存后的偏移地址
考虑到我们不能一开始就将内存池中的内存开到非常大,那么我们可以借用
c
p
p
cpp
cpp 中 vector
的思想,动态的扩张内存,当达到内存上界时使其翻倍,这样对于一个申请到一段长度为
n
n
n 的内存的过程,其时间和空间复杂度都是
O
(
n
)
O(n)
O(n) 的
const MaxNodeSize int = 1 << 26 //64MB
func (s *Arena) allocate(n uint32) uint32 { //返回偏移地址
offset := atomic.AddUint32(&s.offset, n)
lenth := len(s.buf)
if lenth-int(offset) < MaxNodeSize { //空间不足翻倍
grow := lenth
if grow > (1 << 30) {
grow = 1 << 30
}
if uint32(grow) < n { // 防止一次扩展不够
grow = int(n)
}
newBuf := make([]byte, lenth+grow)
copy(newBuf, s.buf)
s.buf = newBuf
}
return offset - n
}
接口与内存对齐
对于 64位/32位 的机器,我们让一段数据占用其基本数据大小的整数倍,可以提高内存访问效率,优化程序性能,并有利于数据并行处理。在大型软件和需要高性能计算的应用中,内存对齐可以带来明显的性能提升。
比如我们将一个跳表节点存入内存中,除了要考虑其 level
指针能否存满,还要对其内存进行对齐,那么我们就可以将这两个操作写成一个接口
const MaxHeight int = 30
func (s *Arena) putNode(height int) uint32 {
nodeSize := uint32(unsafe.Sizeof(node{})) //这里我们暂定node会取得跳表跳至最高点使用的内存大小
baseSize := uint32(unsafe.Sizeof(uint32(0))) //取得32/64位机器的基本数据大小
unusedSize := uint32(MaxHeight-height) * baseSize
sz := nodeSize - unusedSize
sz += baseSize - sz%baseSize - 1 //内存对齐
n := s.allocate(sz)
return n
}
重新编码
在 kv 引擎中,
v
a
l
u
e
value
value 的值可能有很多,我们可以将其编码成二进制文件,再写进 Arena
中
编码方式非常简单,具体代码将在整体实现放出