Lab1-pmm
这里不是实验报告,可以随便吐槽和说很多废话吧(大概)
也没有泄漏啥代码,纯纯的唠嗑,应该不违反学术诚信。
感觉这次实验被我做难了....事实上只需要链表就可以实现所有的操作,buddy system 纯纯的没必要,写出来也不容易 debug (也许熟练工可以做到一次对,但是这有啥用呢)
途中卡 smp=8 的时候尝试过 bitmap 和 slab。 bitmap
就是把一个大页分配成64份,用一个 uint64_t
状压使用情况,每次直接 lowbit(bitmap)
的得到未使用的小块。然后只需要维护两个链表:全满 和 存在空闲。那么一次
fast path 只需要 bit flip 就行了,但事实上过不了。
slab 直接看下面的
设计
我区分 thread_list 和 local_list 的设计参考了微软 mimalloc 的实现
分配
将 512MiB 的堆区大致分成 8 份,每份用一棵线段树维护以 4KiB 为最小单位的大块内存区间,这样每个 CPU 都有恰好一棵树。因为大块内存的分配需要对齐,且最大分配 8MiB 的内存,因此线段树的端点需要同时 8MiB 对齐和 4KiB 对齐。
对于大小超过 4KiB 的内存分配直接从该CPU的线段树上分配。注意到可能出现该 CPU 分配、其它 CPU 释放的情况,因此每个 CPU 的树在访问时都需要上锁,并且释放时需要释放到分配地。
对于大小不超过 4KiB 的内存分配,先按照 2^k 向上对齐。这里每个 CPU 都有 8 条链表,每个链表专门用于分配固定大小的内存块(例如 16KiB 32KiB 64KiB 128KiB...),这样不同大小的内存分配可以并行(好像没什么卵用,因为每个 CPU 目前都是单线程的)
根据对齐后的大小进入该 CPU 对应大小的链表中,此时每个链表下又分出两个子链表 local 和 thread
local 表示本 CPU 分配且本 CPU 释放的内存块
thread 表示本 CPU 分配,但是由其它 CPU 释放的内存块
这样做的好处是 local 子链表不需要上锁,因为每个 CPU 都可以看作是单线程的,不存在数据竞争。对于跨 CPU 释放的情形,最坏情况是剩余 n-1 个 CPU 争抢 thread 链表的一把锁。
每次分配小内存,按照如下顺序分配:
查询local链表,有即分配,这是无锁的,且只涉及一个指针的读写操作。
查询thread链表,上锁,移动空余块到local链表,解锁。这部分只涉及到几个指针的读写操作,因此很快。
所属线段树上锁,申请大块空间(16KiB),等分成固定大小,插入到local链表中,解锁。这是 slow-path。
从上到下分别是 快 -> 慢 的顺序
释放
对于大小超过 4KiB 的内存块将直接找到所在区间对应的线段树,上锁,释放,解锁。
对于大小不超过 4KiB 的小块内存释放,直接找到其分配时所属的 CPU、分配大小,然后分两类情况
本地分配本地释放,插入到 local 链表中
本地分配外地释放,上锁,插入到 thread 链表中,解锁
注意到要做到上述事情需要记录一些元信息。起初我是通过粗暴地翻倍分配空间、把 header 存在相邻的块中。这样如果每次都分配 129 KiB 的空间,那么就会产生 75% 的浪费。后面想到每个小块内存都是从一个特定大块中划分出来的,且线段树保证了大块内存是按块对齐的。因此直接将给定的地址低位清零找到对应大块的起始位置,在这里留出一个小块记录元信息即可。但是实现完这个之后oj就重测了,也不知道这样子做能不能过掉最后一个 low usage。
还有一个问题是如何区分大块释放(访问线段树)和小块释放(直接修改链表)。一个办法是在元信息中加入MAGIC NUMBER,这样就可以以很小的出错概率区分二者了。
印象深刻的Bug
最初的oj有smp=8的狂野case,卡了整整一周都卡不过去,写了8棵线段树都卡不过去,最后的解决方案是干掉case,这样大家就都可以过了,皆大欢喜(摔键盘)