内存池项目
一些共同的知识
std::array<std::atomic<void*>, FREE_LIST_SIZE> centralFreeList_;
这个就是创建了一个centralFreeList_,他是一个array数组,它里面的类型都是atomic<void*>的,表示原子指针,操作不可以被打断。长度是FREE_LIST_SIZE.
std::this_thread::yield();
这里的意思就是线程让步。用于自旋锁,如果检测到原子标志被占用,就线程让步,让这个线程处于就绪态,避免忙等待。等过一会这个线程再过来询问【线程不会消失,只是进入就绪态,让CPU可以去做别的事情】
void* next = *reinterpret_cast<void**>(result);
//一样的,result是void*类型,里面存的是下一个节点的地址,也是void*类型,
//如果直接*(result)那么里面的就变成void型了
//所以要先把result转成void**类型,再取其中的值,才是正确的下一个空闲内存节点的地址
- 使用
std::memory_order_release
内存序,确保当前线程对centralFreeList_[index]
的写操作不会被重新排序到其他写操作之后。 static_cast
只能在相关的数据类型之间转换,例如int double,父类子类之间的类型转换,reinterpret_cast
是一种低级别的类型转换,用于在不同类型之间进行强制转换,即使这些类型之间没有直接的关联。
V2
threadcache
- 使用thread_local确保每个线程创建自己独立的threadcache。 优势:各自的线程申请内存都通过各自的线程实例threadcache来获取内存,不会出现线程之间的争夺进而发生资源的浪费。
- 整个设计都不包含锁(还是因为不存在多线程抢夺资源的情况)
- 处理最频繁的请求,应对高性能
- 接口:
void* allocate(size_t size);
申请填写申请大小
void deallocate(void* ptr, size_t size);
释放填写释放位置和大小- 从中心缓存获取内存,填写index就好
void* fetchFromCentralCache(size_t index);
归还内存到中心缓存,填写ptr和大小,那个bytes我好像设置了但没用到,会根据size计算index
void returnToCentralCache(void* start, size_t size, size_t bytes);
threadcache结构
每一个线程都会有一个这个线程缓存的实例
centralcache
用到的库:atomic类
给threadCache层分内存,是通过本层的空闲列表来分配的,thread层要多大的内存,传递一个索引(不同的索引代表不同的大小)
central就利用头插法返回对应的内存块地址。
central层是只有一个哈希桶,桶的每一个下标连接了很多内存块。
申请内存
当central层没有对应索引的内存的时候,向page层索取,通过需要申请的size字节数来计算需要的页数,然后申请到的内存按照申请的字节数/需要的字节数 = 获取到的对应index的内存块数量。将这些连接起来,头赋给哈希桶的头,然后就还是头删法解决了。
接收thread释放的内存
从thread收回来的,如果大于规定的一个内存块的大小,那么是从系统申请的,直接返还给系统;
反之就是thread从central申请的,头插法就好了。 在插入之前,如果同时进行的是不同index的插回,那么就直接根据不同的index插回就好了;如果多个进程插回一个index内存块,那么这个时候自旋锁和让步就发挥作用,每一次接收内存块回收的时候,都会使用test_and_set,来实现线程间的同步。
为什么不适用mutex而是原子操作的自旋锁?
特性 | test_and_set 自旋锁 | std::mutex 阻塞锁 |
---|---|---|
实现方式 | 基于原子操作,无需操作系统支持 | 基于操作系统,可能涉及系统调用 |
性能 | 锁持有时间短时性能更高 | 锁持有时间长时性能更高 |
线程等待 | 忙等待(消耗 CPU) | 阻塞(线程挂起,不消耗 CPU) |
适用场景 | 锁持有时间非常短,线程切换开销较高的场景 | 锁持有时间较长,线程切换开销可以接受的场景 |
复杂度 | 实现简单,轻量级 | 实现复杂,功能更强大 |
centralcache的数据结构如下
跟threadcache是一样的。中间层。
pageCache
-
负责直接与操作系统交互,通过
mmap
申请大块内存,以页为单位进行内存管理(4KB,4K字节) -
是内存池与操作系统之间的桥梁
-
central层传递给page层需要的页数,进行分配。以页进行分配的好处:
-
硬件层面的优化,现代计算机内存管理单元就是页为单位,可以与硬件的页面管理对齐;并且操作系统所分配的娶你内存空间也是以页为基本单位,获得更好的性能
-
所有的分配都对齐到页边界,便于后续回收之后的合并,这样可以形成多个地址连续的页,也就是一大块内存,可以减少外部内存碎片
-
-
span是一个自定义的页结构体,存储了1. 对应的页内存块的地址 2. 页数 3. 下一块连续内存页的地址
-
页面合并的好处:减少外部内存碎片,同时便于管理,因为多个小span合并成了一个大span,并且当span足够大的时候,可以选择归还给操作系统
page层数据结构
为什么是找>=numpages的空闲链表,而不是==的呢?
在page层是允许这种情况,在没有==请求的numpages时,允许分配>numpages的span,切割出需要的numpage页后返回。
这么做的好处是,如果只允许分配完全匹配的span,反而会导致内存利用率降低;通过允许分配更大的内存可以减少向系统申请内存的频率
合并sapn是通过另一map类型的spanmap_实现的
这个存储了内存地址到span的映射void* ,同时还有span*。记录了已分配的和空闲的span的信息。
可以减少向系统申请内存的频率
合并sapn是通过另一map类型的spanmap_实现的
这个存储了内存地址到span的映射void* ,同时还有span*。记录了已分配的和空闲的span的信息。