目录
有关大于256KB内存的申请和释放处理方法
处理大于256KB的内存申请
补充内容1
补充内容2
补充内容3
处理大于256KB的内存释放
新增内容1
新增内容2
测试函数
使用定长内存池替代new
释放对象时不传对象大小
补充内容1
补充内容2
补充内容3
补充内容4
测试函数
为哈希桶加锁
多线程环境下对比malloc测试
复杂问题的调试技巧
性能瓶颈分析
针对性能瓶颈使用基数树进行优化
基数树代码
有关大于256KB内存的申请和释放处理方法
内存申请和释放大于256KB也分为两种情况:
- 大于256KB但是小于128 * 8 * 1024KB
- 大于128 * 8 * 1024KB
内存申请大于256KB但小于128 * 8 * 1024KB:因为PageCache中存放的span所管理的最大页数为128,即span可分配的最大内存为128 * 8 * 1024 KB,所以当可以直接向PageCache申请一个合适的span(该span管理的页数为此次内存申请对齐后大小 / 页大小)
内存申请大于128 * 8 *1024 KB:PageCache中已经没有合适的span了,直接向堆上申请
处理大于256KB的内存申请
补充内容1
在RoundUp函数中新增用于处理大于256KB内存申请的内存对齐判断
(_RoundUp函数不变)
//用于内存对齐
static inline size_t RoundUp(size_t size)
{
if (size <= 128)
{
return _RoundUp(size, 8);
}
else if (size <= 1024)
{
return _RoundUp(size, 16);
}
else if (size <= 8 * 1024)
{
return _RoundUp(size, 128);
}
else if (size <= 64 * 1024)
{
return _RoundUp(size, 1024);
}
else if (size <= 256 * 1024)
{
return _RoundUp(size, 8 * 1024);
}
else
{
return _RoundUp(size, 1 << PAGE_SHIFT);//对齐数为当前规定的页的大小
}
}
补充内容2
在NewSpan开始处中新增获取管理页数大于128的span的判断
//所需页数大于128PageCache无法分配,需要向堆申请
if (k > NPAGES - 1)
{
void* ptr = SystemAlloc(k);//从堆获取一块内存
Span* span = new Span;//申请一个span结点
span->_PageId = (size_t)ptr >> PAGE_SHIFT;//新span中的页号为ptr指向的内存地址 / 页大小
span->_n = k;//新span中的页数为n个
_idSpanMap[span->_PageId] = span;//存放映射关系,即使该span不会被挂在PageCache上
return span;
}
补充内容3
在ConcurrentAlloc函数新增size > MAX_BYTES的判断
//申请内存
static void* ConcurrentAlloc(size_t size)
{
if (size > MAX_BYTES)
{
size_t alignSize = SizeClass::RoundUp(size);//内存对齐
size_t kpage = alignSize >> PAGE_SHIFT;//计算所需页数
PageCache::GetInstance()->_pageMtx.lock();
Span* span = PageCache::GetInstance()->NewSpan(kpage);//向PageCache申请管理kpage个页的span,若kpage > 128则需要经过补充内容2中的内容,否则还是按照原NewSpan执行
void* ptr = (void*)(span->_PageId << PAGE_SHIFT);//通过页号计算地址
PageCache::GetInstance()->_pageMtx.unlock();
return ptr;//返回从PageCache分配给的内存空间的地址
}
else
{
...//获取TLS的那部分内容
}
}
处理大于256KB的内存释放
新增内容1
在ReleaseSpanToPageCache开始处新增当归还的span的_n > 128的判断
//大于128页的span直接还给堆
if (span->_n > NPAGES - 1)
{
void* ptr = (void*)(span->_PageId << PAGE_SHIFT);
SystemFree(ptr);
delete span;
return;
}
新增内容2
在ConcurrentFree中新增用于size > MAX_BYTES的判断
//释放内存
static void ConcurrentFree(void* ptr,size_t size)
{
if (size > MAX_BYTES)
{
Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);//根据归还的内存地址获取要归还的span
PageCache::GetInstance()->_pageMtx.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);//将要归还的span挂在PageCache上,或者返还给堆
PageCache::GetInstance()->_pageMtx.unlock();
}
else
{
...//还是原来的那两行释放内容
}
}
测试函数
//用于测试内存申请和释放大于256KB
void BigAlloc()
{
//内存申请和释放大于256KB,但是小于128 * 8 * 1024KB
void* p1 = ConcurrentAlloc(257 * 1024);
ConcurrentFree(p1, 257 * 1024);
//内存申请和释放大于128 * 8 * 1024KB
void* p2 = ConcurrentAlloc(129 * 8 * 1024);
ConcurrentFree(p2, 129 * 8 * 1024);
}
使用定长内存池替代new
基本概念:定长内存池在申请内存时是直接向堆申请的,没有使用malloc,效率得到提升,而目前我们在本项目中用到到了很多new的操作,其本质还是malloc,因此我们要用定长内存池中的New()和Delete()函数来代替new和delete,进行内存申请和释放,从而提高程序执行效率
准备工作:在某个涉及new或者delete的类中新增ObjectPool< ?>类型的私有成员变量,下面以PageCache为例
class PageCache
{
public:
...
private:
...
ObjectPool<Span> _spanPool;//<>中的类型根据需要进行更改
...
};
替换方式:将PageCache.cpp中所有使用new的地方都换成_spanPool.New(),将所有使用delet的位置都换为_spanPool.Delete( ? )(?表示要删除的对象的名称可能是span也可能是kspan之类的)
//Span* kSpan = new Span;
Span* kSpan = _spanPool.New();
//delete span;
_spanPool.Delete(span);
易忽略位置
1、ConcurrentAlloc.h中的new ThreadCache
//pTLSThreadCache = new ThreadCache;
static ObjectPool<ThreadCache> tcPool;//static修饰保证只在当前文件中可以被访问
pTLSThreadCache = tcPool.New();
!!!重点!!!
注意事项:pTLSThreadCache是每个线程独有的一个对象,但是为其申请空间的tcPool不是,它是一个静态的对象,整个进程中独一份,被当前进程中的所有线程共享,多线程情况下会出现线程安全问题,所以这里也需要加锁(不加的话有小概率不崩溃,即轮到t2执行时_memory不为空)
解决办法:在ObjPool类中新增公有成员变量_poolMtx,同时在pTLSThreadCache = tcPool.New()的两侧加锁
tcPool._poolMtx.lock();
pTLSThreadCache = tcPool.New();
tcPool._poolMtx.unlock();
补充:SpanList类中为了创建头结点的new Span不用替换,因为头节点通常在
SpanList
对象的整个生命周期内存在,并且不会像其他 Span 对象那样频繁创建和销毁。使用_spanPool
进行内存管理主要是为了优化频繁分配和回收的对象,而头节点的长生命周期使得使用_spanPool
的优势不明显
释放对象时不传对象大小
基本概念:在之前释放内存时我们不仅要传入释放的内存的地址,还要存放要释放的内存的大小,过于麻烦,所以最好只传递一个指针即可释放内存
ConcurrentFree(p1, 6);
补充内容1
在span类中新增一个表示当前span中管理的内存的大小的成员变量_objSize
struct Span
{
...
size_t _objSize = 0;//当前span管理的内存大小
};
补充内容2
在ConcurrentFree中,将MapObjectToSpan的位置进行移动,并获取当前span的_objSize
//释放内存
static void ConcurrentFree(void* ptr)
{
Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);
size_t size = span->_objSize;
if (size > MAX_BYTES)
{
...
}
else
{
...
}
}
补充内容3
在ConcurrentAlloc中从堆上获取到一个span后,补充该span的_objSize
//申请内存
static void* ConcurrentAlloc(size_t size)
{
if (size > MAX_BYTES)
{
...
Span* span = PageCache::GetInstance()->NewSpan(kpage);
span->_objSize = size;
...
}
else
{
...
}
}
补充内容4
CentralCache中的NewSpan后,填充该span的_objSize
//为指定位置桶下的SpanList申请一个非空的span
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
...
Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));//从PageCache中获取一个新的非空span
span->_isUse = true;
span->_objSize = size;
....
}
注意事项:记得最后把ConcurrentFree的第二个形参删除
测试函数
void WithNoSize()
{
void* p1 = ConcurrentAlloc(257 * 1024);
ConcurrentFree(p1);
void* p2 = ConcurrentAlloc(129 * 8 * 1024);
ConcurrentFree(p2);
}
为哈希桶加锁
基本概念:C++的标准模板库(STL)提供的容器在多线程环境下并不保证线程安全,因此在多个线程同时访问或修改同一个容器时,通常需要自行添加同步机制(如互斥锁)以确保数据的一致性和避免竞态条件,因此当我们尝试在本项目中使用哈希桶记录span与页号的映射关系时,需要及时的加锁
加锁位置:参与读写哈希桶的函数有NewSpan、MapObjectToSpan和ReleaseSpanToPageCache,它们都在PageCache.cpp中,其中在CentralCache.cpp中使用这三个函数时,只有MapObjectToSpan没有添加锁,这就可能导致多个线程在CentralCache中同时访问MapObjectToSpan函数并同时访问哈希桶造成线程安全问题,所以要在MapObjectToSpan函数执行到访问哈希桶的操作前加锁
//地址->页号->span的映射
Span* PageCache::MapObjectToSpan(void* obj)
{
size_t id = ((size_t)obj >> PAGE_SHIFT);
std::unique_lock<std::mutex> lock(_pageMtx);
auto ret = _idSpanMap.find(id);
if (ret != _idSpanMap.end())
{
return ret->second;
}
else
{
assert(false);
return nullptr;
}
}
关于std::unique_lock<std::mutex> lock(_pageMtx):是一种通过RAII方式管理互斥锁的机制,确保在多线程环境中对共享资源的安全访问。它自动处理锁的获取和释放,减少了手动管理锁可能带来的错误风险,同时提供了较高的灵活性,适用于各种复杂的同步场景
多线程环境下对比malloc测试
新增Benchmark.cpp源文件
#include<cstdio>
#include<iostream>
#include<vector>
#include<thread>
#include<mutex>
#include"ConcurrentAlloc.h"
using namespace std;
void BenchmarkMalloc(size_t ntimes, size_t nworks, size_t rounds)//ntime一轮申请和释放内存的次数,round是跑多少轮,nworks是线程数
{
std::vector<std::thread> vthread(nworks);
std::atomic<size_t> malloc_costtime = 0;
std::atomic<size_t> free_costtime = 0;
for (size_t k = 0; k < nworks; ++k)
{
vthread[k] = std::thread([&, k]() {
std::vector<void*> v;
v.reserve(ntimes);
for (size_t j = 0; j < rounds; ++j)
{
size_t begin1 = clock();
for (size_t i = 0; i < ntimes; i++)
{
//v.push_back(malloc(16));
v.push_back(malloc((16 + i) % 8192 + 1));
}
size_t end1 = clock();
size_t begin2 = clock();
for (size_t i = 0; i < ntimes; i++)
{
free(v[i]);
}
size_t end2 = clock();
v.clear();
malloc_costtime += (end1 - begin1);
free_costtime += (end2 - begin2);
}
});
}
for (auto& t : vthread)
{
t.join();
}
printf("%u个线程并发执行%u轮次,每轮次malloc %u次: 花费:%u ms\n",
nworks, rounds, ntimes, malloc_costtime.load());
printf("%u个线程并发执行%u轮次,每轮次free %u次: 花费:%u ms\n",
nworks, rounds, ntimes, free_costtime.load());
printf("%u个线程并发malloc&free %u次,总计花费:%u ms\n",
nworks, nworks * rounds * ntimes, malloc_costtime.load() + free_costtime.load());
}
// 单轮次申请释放次数 线程数 轮次
void BenchmarkConcurrentMalloc(size_t ntimes, size_t nworks, size_t rounds)
{
std::vector<std::thread> vthread(nworks);
std::atomic<size_t> malloc_costtime = 0;
std::atomic<size_t> free_costtime = 0;
for (size_t k = 0; k < nworks; ++k)
{
vthread[k] = std::thread([&]() {
std::vector<void*> v;
v.reserve(ntimes);
for (size_t j = 0; j < rounds; ++j)
{
size_t begin1 = clock();
for (size_t i = 0; i < ntimes; i++)
{
//v.push_back(ConcurrentAlloc(16));
v.push_back(ConcurrentAlloc((16 + i) % 8192 + 1));
}
size_t end1 = clock();
size_t begin2 = clock();
for (size_t i = 0; i < ntimes; i++)
{
ConcurrentFree(v[i]);
}
size_t end2 = clock();
v.clear();
malloc_costtime += (end1 - begin1);
free_costtime += (end2 - begin2);
}
});
}
for (auto& t : vthread)
{
t.join();
}
printf("%u个线程并发执行%u轮次,每轮次concurrent alloc %u次: 花费:%u ms\n",
nworks, rounds, ntimes, malloc_costtime.load());
printf("%u个线程并发执行%u轮次,每轮次concurrent dealloc %u次: 花费:%u ms\n",
nworks, rounds, ntimes, free_costtime.load());
printf("%u个线程并发concurrent alloc&dealloc %u次,总计花费:%u ms\n",
nworks, nworks * rounds * ntimes, malloc_costtime.load() + free_costtime.load());
}
int main()
{
size_t n = 10000;
cout << "==========================================================" << endl;
BenchmarkConcurrentMalloc(n, 10, 10);
cout << endl << endl;
BenchmarkMalloc(n, 10, 10);
cout << "==========================================================" << endl;
return 0;
}
性能瓶颈分析
针对性能瓶颈使用基数树进行优化
基数树代码
~over~