高并发内存池(六):补充内容

news/2024/9/29 19:51:00/

目录

有关大于256KB内存的申请和释放处理方法 

处理大于256KB的内存申请

补充内容1

补充内容2

补充内容3 

处理大于256KB的内存释放

新增内容1

新增内容2

测试函数

使用定长内存池替代new

释放对象时不传对象大小

补充内容1

补充内容2

 补充内容3

补充内容4

测试函数

为哈希桶加锁

多线程环境下对比malloc测试

复杂问题的调试技巧

性能瓶颈分析

针对性能瓶颈使用基数树进行优化

基数树代码


有关大于256KB内存的申请和释放处理方法 

内存申请和释放大于256KB也分为两种情况:

  1. 大于256KB但是小于128 * 8 * 1024KB
  2. 大于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);//根据归还的内存地址获取要归还的spanPageCache::GetInstance()->_pageMtx.lock();PageCache::GetInstance()->ReleaseSpanToPageCache(span);//将要归还的span挂在PageCache上,或者返还给堆PageCache::GetInstance()->_pageMtx.unlock();}else{...//还是原来的那两行释放内容}
}

测试函数

//用于测试内存申请和释放大于256KB
void BigAlloc()
{//内存申请和释放大于256KB,但是小于128 * 8 * 1024KBvoid* p1 = ConcurrentAlloc(257 * 1024);ConcurrentFree(p1, 257 * 1024);//内存申请和释放大于128 * 8 * 1024KBvoid* 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中获取一个新的非空spanspan->_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~


http://www.ppmy.cn/news/1532063.html

相关文章

如何使用ssm实现个人日常事务管理系统+vue

TOC ssm718个人日常事务管理系统vue 研究背景 自计算机发展以来给人们的生活带来了改变。第一代计算机为1946年美国设计&#xff0c;最开始用于复杂的科学计算&#xff0c;占地面积、开机时间要求都非常高&#xff0c;经过数十几的改变计算机技术才发展到今天。现如今已是电…

服务保护sentinel

线程隔离 - 线程池隔离&#xff1a;给每个服务调用业务分配一个线程池&#xff0c;利用线程池本身实现隔离效果。 - 信号量隔离&#xff1a;不创建线程池&#xff0c;而是计数器模式&#xff0c;记录业务使用的线程数量&#xff0c;达到信号量上限时&#xff0c;禁止新的请求。…

在Android开发中可以用到的Ui控件有哪些?

目录 1. 文本控件 2. 按钮控件 3. 选择控件 4. 布局控件 5. 图像控件 6. 列表控件 7. 对话框和提示 8. 菜单控件 9. 工具栏和导航控件 10. 进度控件 11. 时间与日期控件 12. 其他控件 13. 高级控件 14. 自定义控件 15. 其他 总结&#xff1a; 在 Android 开发中…

Go基础学习06-Golang标准库container/list(双向链表)深入讲解;延迟初始化技术;Element;List;Ring

基础介绍 单向链表中的每个节点包含数据和指向下一个节点的指针。其特点是每个节点只知道下一个节点的位置&#xff0c;使得数据只能单向遍历。 示意图如下&#xff1a; 双向链表中的每个节点都包含指向前一个节点和后一个节点的指针。这使得在双向链表中可以从前向后或从后…

下载分享抖音视频并转成文本

思路 将分享的链接转义成可以正常链接。通过链接去找到对应的视频链接。在通过返回的html。解析里面视频的资源链接在playAddr字段里面。拿到链接后转义urlPath即可获取到资源信息。下载视频。科大讯飞语音转文本。处理json保存到本地。 部分代码 获取抖音链接&#xff1a; …

学生宿舍管理:Spring Boot技术实现

第1章 绪论 1.1 课题背景 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让信息都可以通过网络传播&#xff0c;搭配信息管理工具可以很好地为人们提供服务。所以各行业&#xff0c;尤其是规模较大的企业和学校等…

OpenCV图像文件读写(4)解码图像数据函数imdecode()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 从内存缓冲区读取图像。 imdecode 函数从指定的内存缓冲区读取图像。如果缓冲区太短或包含无效数据&#xff0c;函数将返回一个空矩阵 (Mat::dat…

更新 Git 软件

更新 Git 软件本身是指将你当前安装的 Git 版本升级到最新版本。不同的操作系统有不同的更新方法。以下是针对 Windows、macOS 和 Linux 的 Git 更新步骤&#xff1a; Windows 检查当前版本&#xff1a; git --version访问官网下载最新版本&#xff1a; 访问 Git 官方网站 (ht…