1.1 池化技术
2.1 内存碎片问题
造成堆利用率很低的一个主要原因就是内存碎片化。如果有未使用的存储器,但是这块存储器不能用来满足分配的请求,这时候就会产生内存碎片化问题。内存碎片化分为内部碎片和外部碎片。
3.1 为什么要使用内存池
3.2 内存池的演变
4.1 项目介绍
我写这个项目呢,主要是为了学习,参考的是tc_malloc,项目设计分为三层结构:
注:怎么实现每个线程都拥有自己唯一的线程缓存呢?
为了避免加锁带来的效率,在Thread Cache中使用(tls)thread local storage保存每个线程本地的Thread Cache的指针,这样Thread Cache在申请释放内存是不需要锁的。因为每一个线程都拥有了自己唯一的一个全局变量。
TLS分为静态的和动态的:
静态的TLS是:直接定义
动态的TLS是:调用系统的API去创建的,我们这个项目里面用到的就是静态的TLS
https://blog.csdn.net/evilswords/article/details/8191230
https://blog.csdn.net/yusiguyuan/article/details/22938671
4.2 设计Thread Cache
ThreadCache.h:
#pragma once
#include "Common.h"
class ThreadCache
{
private:
Freelist _freelist[NLISTS];//自由链表
public:
//申请和释放内存对象
void* Allocate(size_t size);
void Deallocate(void* ptr, size_t size);
//从中心缓存获取对象
void* FetchFromCentralCache(size_t index, size_t size);
//释放对象时,链表过长时,回收内存回到中心堆
void ListTooLong(Freelist* list, size_t size);
};
//静态的,不是所有可见
//每个线程有个自己的指针, 用(_declspec (thread)),我们在使用时,每次来都是自己的,就不用加锁了
//每个线程都有自己的tlslist
_declspec (thread) static ThreadCache* tlslist = nullptr;
申请内存:
释放内存:
//专门用来计算大小位置的类
class SizeClass
{
public:
//获取Freelist的位置
inline static size_t _Index(size_t size, size_t align)
{
size_t alignnum = 1 << align; //库里实现的方法
return ((size + alignnum - 1) >> align) - 1;
}
inline static size_t _Roundup(size_t size, size_t align)
{
size_t alignnum = 1 << align;
return (size + alignnum - 1)&~(alignnum - 1);
}
public:
// 控制在12%左右的内碎片浪费
// [1,128] 8byte对齐 freelist[0,16)
// [129,1024] 16byte对齐 freelist[16,72)
// [1025,8*1024] 128byte对齐 freelist[72,128)
// [8*1024+1,64*1024] 1024byte对齐 freelist[128,184)
inline static size_t Index(size_t size)
{
assert(size <= MAX_BYTES);
// 每个区间有多少个链
static int group_array[4] = { 16, 56, 56, 56 };
if (size <= 128)
{
return _Index(size, 3);
}
else if (size <= 1024)
{
return _Index(size - 128, 4) + group_array[0];
}
else if (size <= 8192)
{
return _Index(size - 1024, 7) + group_array[0] + group_array[1];
}
else//if (size <= 65536)
{
return _Index(size - 8 * 1024, 10) + group_array[0] + group_array[1] + group_array[2];
}
}
// 对齐大小计算,向上取整
static inline size_t Roundup(size_t bytes)
{
assert(bytes <= MAX_BYTES);
if (bytes <= 128){
return _Roundup(bytes, 3);
}
else if (bytes <= 1024){
return _Roundup(bytes, 4);
}
else if (bytes <= 8192){
return _Roundup(bytes, 7);
}
else {//if (bytes <= 65536){
return _Roundup(bytes, 10);
}
}
//动态计算从中心缓存分配多少个内存对象到ThreadCache中
static size_t NumMoveSize(size_t size)
{
if (size == 0)
return 0;
int num = (int)(MAX_BYTES / size);
if (num < 2)
num = 2;
if (num > 512)
num = 512;
return num;
}
// 根据size计算中心缓存要从页缓存获取多大的span对象
static size_t NumMovePage(size_t size)
{
size_t num = NumMoveSize(size);
size_t npage = num*size;
npage >>= PAGE_SHIFT;
if (npage == 0)
npage = 1;
return npage;
}
};
4.4 设计Thread Cache
CentralCache.h:
#pragma once
#include "Common.h"
//上面的ThreadCache里面没有的话,要从中心获取
/*
进行资源的均衡,对于ThreadCache的某个资源过剩的时候,可以回收ThreadCache内部的的内存
从而可以分配给其他的ThreadCache
只有一个中心缓存,对于所有的线程来获取内存的时候都应该是一个中心缓存
所以对于中心缓存可以使用单例模式来进行创建中心缓存的类
对于中心缓存来说要加锁
*/
//设计成单例模式
class CentralCache
{
public:
static CentralCache* Getinstence()
{
return &_inst;
}
//从page cache获取一个span
Span* GetOneSpan(SpanList& spanlist, size_t byte_size);
//从中心缓存获取一定数量的对象给threa cache
size_t FetchRangeObj(void*& start, void*& end, size_t n, size_t byte_size);
//将一定数量的对象释放给span跨度
void ReleaseListToSpans(void* start, size_t size);
private:
SpanList _spanlist[NLISTS];
private:
CentralCache(){}//声明不实现,防止默认构造,自己创建
CentralCache(CentralCache&) = delete;
static CentralCache _inst;
};
申请内存:
释放内存:
特别关心:什么是span?一个span是由多个页组成的一个span对象。一页大小是4k。
//Span是一个跨度,既可以分配内存出去,也是负责将内存回收回来到PageCache合并
//是一链式结构,定义为结构体就行,避免需要很多的友元
struct Span
{
PageID _pageid = 0;//页号
size_t _npage = 0;//页数
Span* _prev = nullptr;
Span* _next = nullptr;
void* _list = nullptr;//链接对象的自由链表,后面有对象就不为空,没有对象就是空
size_t _objsize = 0;//对象的大小
size_t _usecount = 0;//对象使用计数,
};
特别关心:关于spanlist,设计为一个双向链表,插入删除效率较高。
//和上面的Freelist一样,各个接口自己实现,双向带头循环的Span链表
class SpanList
{
public:
Span* _head;
std::mutex _mutex;
public:
SpanList()
{
_head = new Span;
_head->_next = _head;
_head->_prev = _head;
}
~SpanList()//释放链表的每个节点
{
Span * cur = _head->_next;
while (cur != _head)
{
Span* next = cur->_next;
delete cur;
cur = next;
}
delete _head;
_head = nullptr;
}
//防止拷贝构造和赋值构造,将其封死,没有拷贝的必要,不然就自己会实现浅拷贝
SpanList(const SpanList&) = delete;
SpanList& operator=(const SpanList&) = delete;
//左闭右开
Span* Begin()//返回的一个数据的指针
{
return _head->_next;
}
Span* End()//最后一个的下一个指针
{
return _head;
}
bool Empty()
{
return _head->_next == _head;
}
//在pos位置的前面插入一个newspan
void Insert(Span* cur, Span* newspan)
{
Span* prev = cur->_prev;
//prev newspan cur
prev->_next = newspan;
newspan->_next = cur;
newspan->_prev = prev;
cur->_prev = newspan;
}
//删除pos位置的节点
void Erase(Span* cur)//此处只是单纯的把pos拿出来,并没有释放掉,后面还有用处
{
Span* prev = cur->_prev;
Span* next = cur->_next;
prev->_next = next;
next->_prev = prev;
}
//尾插
void PushBack(Span* newspan)
{
Insert(End(), newspan);
}
//头插
void PushFront(Span* newspan)
{
Insert(Begin(), newspan);
}
//尾删
Span* PopBack()//实际是将尾部位置的节点拿出来
{
Span* span = _head->_prev;
Erase(span);
return span;
}
//头删
Span* PopFront()//实际是将头部位置节点拿出来
{
Span* span = _head->_next;
Erase(span);
return span;
}
void Lock()
{
_mutex.lock();
}
void Unlock()
{
_mutex.unlock();
}
};
特别关心:怎么才能将Thread Cache中的内存对象还给它原来的span呢?
答:可以在Page Cache中维护一个页号到span的映射,当Span Cache给Central Cache分配一个span时,将这个映射更新到unordered_map中去,在Thread Cache还给Central Cache时,可以查这个unordered_map找到对应的span。
4.5 设计Page Cache
PageCache.h
#pragma once
#include "Common.h"
//对于Page Cache也要设置为单例,对于Central Cache获取span的时候
//每次都是从同一个page数组中获取span
//单例模式
class PageCache
{
public:
static PageCache* GetInstence()
{
return &_inst;
}
Span* AllocBigPageObj(size_t size);
void FreeBigPageObj(void* ptr, Span* span);
Span* _NewSpan(size_t n);
Span* NewSpan(size_t n);//获取的是以页为单位
//获取从对象到span的映射
Span* MapObjectToSpan(void* obj);
//释放空间span回到PageCache,并合并相邻的span
void ReleaseSpanToPageCache(Span* span);
private:
SpanList _spanlist[NPAGES];
//std::map _idspanmap;
std::unordered_map _idspanmap;
std::mutex _mutex;
private:
PageCache(){}
PageCache(const PageCache&) = delete;
static PageCache _inst;
};
申请内存:
释放内存:
4.6 向系统申请内存
几个内存池库的对比: https://blog.csdn.net/junlon2006/article/details/77854898
tcmalloc源码学习: https://www.cnblogs.com/persistentsnail/p/3442185.html
TCMALLOC 源码阅读: https://blog.csdn.net/math715/article/details/80654167
https://github.com/Winter-Win/ConcurrentMemoryPool