现在我们项目基本逻辑完成了,但是这个简化版的 tcmalloc 中我们生成一个 span 对象或者其它的对象,用的是 new,这底层调用的还是 malloc 呀,所以我们需要改进一下,不应该从 malloc 来开辟空间,所以我们可以使用前面实现过一个定长内存池来开辟内存,我们测试过它的效率是要比 malloc 快的!
因为使用 new 大部分的位置都是在 PageCache 中,所以我们在 PageCache 类中定义一个定长内存池,如下所示:
// PageCahce.h
#pragma once
#include "Common.h"
#include "FixedLenMemPool.hpp"
// 单例模式:饿汉方式
class PageCache
{
private:
SpanList _spanlists[PAGELIST_NUMS];
std::mutex _mtx;
static PageCache _page_instance;
std::unordered_map<page_t, Span*> _tables;
fixed_size_pool<Span> _fixed_pool; // 定长内存池
public:
//...
}; 然后将各函数中使用 new 的地方改成用定长内存池来开辟空间,下面举其中一个例子:(下面将其它部分省略了,具体看开辟空间的那部分代码)
// PageCache.cpp
// 获取一个k页大小的span
Span* PageCache::new_span(size_t k)
{
// 如果k大于128的话,则直接向系统申请内存即可
if (k > PAGELIST_NUMS - 1)
{
void* ptr = SystemAlloc(k);
//Span* span = new Span;
Span* span = _fixed_pool.apply();
//...
}
//...
// 2. 没有的话向更大的页大小的哈希桶中查找是否有可用的span对象
for (int i = k + 1; i < PAGELIST_NUMS; ++i)
{
if (!_spanlists[i].empty())
{
//...
//Span* back = new Span;
Span* back = _fixed_pool.apply();
//...
}
}
//...
//Span* newspan = new Span;
Span* newspan = _fixed_pool.apply();
//...
} 此外,我们还需要在 Concurrent.h 中也改一下:
// Concurrent.h
// 向高并发内存池申请空间的接口
static void* ConcurrentAlloc(size_t size)
{
// 如果申请大小超过了256KB,则直接向页缓存申请内存
if (size > THREAD_MAX_SIZE)
{
//...
}
else
{
// 通过TLS机制,每个线程都能不加锁的获取到属于自己的ThreadCache对象,并且对于单个来说是全局的!
if (tls_thread_cache == nullptr)
{
//tls_thread_cache = new ThreadCache;
static fixed_size_pool<ThreadCache> tcmalloc; // 创建一个ThreadCache的定长内存池,进行内存申请
tls_thread_cache = tcmalloc.apply();
}
return tls_thread_cache->allocate(size);
}
}Ⅱ. 简化释放内存接口参数
因为我们在释放内存的时候调用 ConcurrentFree(),但是还得传大小,这不太符合需求呀,应该给一个指针就能让其释放大小的,所以我们可以在 Span 类中添加一个成员变量 _size 用于记录当前对象的大小,如下所示:
// Common.h
// 管理以页为单位的大内存块
struct Span
{
page_t _pid = 0; // 大块内存起始页的页号
size_t _num = 0; // 页的个数
Span* _next = nullptr; // 双向链表结构
Span* _prev = nullptr;
size_t _use_count = 0; // 当前分配给ThreadCache对象的小内存块个数
void* _freelist = nullptr; // 当前大内存块对应的空闲链表
bool _is_used = false; // 表示当前对象是否被线程使用着
size_t _size = 0; // 表示当前span对象的大小
}; 然后我们只需要在申请 Span 的时候,也就是两个时候,一个是页缓存申请的时候,一个是超过了规定页数大小后向系统申请的时候。我们只需要在申请之后将大小设置进对应的 Span 对象即可!
下面是超过了规定页数大小后向系统申请的时候设置大小:
// ConcurrentAlloc.h
// 向高并发内存池申请空间的接口
static void* ConcurrentAlloc(size_t size)
{
// 如果申请大小超过了256KB,则直接向页缓存申请内存
if (size > THREAD_MAX_SIZE)
{
size_t align = AlignClass::get_align(size);
size_t page = size >> PAGE_SHIFT;
PageCache::get_instance()->get_mutex().lock();
Span* newspan = PageCache::get_instance()->new_span(page);
newspan->_is_used = true;
newspan->_size = size; // 在这里设置大小
PageCache::get_instance()->get_mutex().unlock();
//...
}
else
{
//...
}
} 下面是页缓存申请的时候设置大小:
// CentralCache.cpp
// 获取一个非空的span对象
Span* CentralCache::get_span(SpanList& list, size_t size)
{
//...
// 3. 走到这里说没有空闲span了,只能找page cache要
/*
此时先计算要申请多少页,然后再去申请,并且这个过程要进行加锁!
之所以不到new_span()函数中去加锁,其实是因为内部有递归调用自己的操作,所以我们就统一在这里处理加锁问题!
当然如果new_span()函数中使用的是递归锁,或者不使用递归调用自己的操作,那是可用在其内部处理锁问题的!
*/
PageCache::get_instance()->get_mutex().lock();
Span* newspan = PageCache::get_instance()->new_span(AlignClass::get_nums_of_page(size));
newspan->_is_used = true;
newspan->_size = size; // 在这里设置大小
PageCache::get_instance()->get_mutex().unlock();
//...
} 此时我们就可以改写一下释放内存的接口了,如下所示:
// ConcurrentAlloc.h
// 释放内存的接口(实际上并不会真的释放,只是还给内存池管理了)
static void ConcurrentFree(void* ptr)
{
// 通过内存块地址获取对应的span对象和大小
Span* span = PageCache::get_instance()->get_span_from_pageID(ptr);
size_t size = span->_size;
// 如果释放大小超过了256KB,则直接向页缓存释放内存
if (size > THREAD_MAX_SIZE)
{
// 别忘了加锁
PageCache::get_instance()->get_mutex().lock();
PageCache::get_instance()->merge_memory_from_CentralCache(span);
PageCache::get_instance()->get_mutex().unlock();
}
else
{
assert(tls_thread_cache != nullptr);
tls_thread_cache->deallocate(ptr, size);
}
} 这里还有一个问题,就是当多个线程在调用 get_span_from_pageID() 函数的时候,就是查哈希表的时候,其实是需要加锁的,不加锁的话就有数据不一致的问题,因为 stl 容器本身就是线程不安全的,所以需要加锁,如下所示:
// PageCache.cpp
// 根据传入的内存块地址返回对应的span对象的指针
Span* PageCache::get_span_from_pageID(void* ptr)
{
// 1. 先根据地址求出其所属的页号
page_t pid = ((page_t)ptr >> PAGE_SHIFT);
// 2. 找到对应的页号对应的span指针进行返回
std::unique_lock<std::mutex> lock(_mtx); // 注意查找过程是需要加锁的,并且最好是守卫锁
auto it = _tables.find(pid);
if (it != _tables.end())
return it->second;
else
{
assert(false);
return nullptr;
}
}