C/C++程序为什么比起其它语言开发的程序效率要高,一个很重要的原因就是可以直接操作内存,今天就来讲讲为什么需要内存池的技术。
先看下面两段代码,都是去重复的创建和删除对象0x5FFFFF次,他们的执行后的效率怎么样呢?
程序一:
#include
#include
class CTestClass{
char m_chBuf[4096];
};
int main(){
clock_t count = clock();
for(unsigned int i=0; i<0x5fffff; i++){
CTestClass *p = new CTestClass;
delete p;
}
std::cout << "Interval = " << clock() - count << " ticks" << std::endl;
return 0;
}
程序二:
#include
#include
char buf[4100];
class CTestClass{
char m_chBuf[4096];
public:
void *operator new(size_t size){
return (void *)buf;
}
void operator delete(void *p){}
};
int main(){
clock_t count = clock();
for(unsigned int i=0; i<0x5fffff; i++){
CTestClass *p = new CTestClass;
delete p;
}
std::cout << "Interval = " << clock() - count << " ticks" << std::endl;
return 0;
}
在我的机器上执行的情况是:
从上图的结果可以看出他们执行的效率大约相差8倍,那这是为什么呢?这是因为程序二重载了new运算后避免了使用malloc/free等系统调用,众所周知,系统调用是很耗时的操作。
我们先看下系统内存的布局,如下图:
应用程序动态分配内存的区域就是用户栈和全局区直接的一块区域,这块区域允许我们使用mmap、munmap和brk去向内核申请和释放内存的系统调用,内核刚把应用程序加载时,brk指针指向全局区的高地址,当我们调用brk系统调用,内核把brk指针往高地址移动,同时内核把它所管理的这一块内存返还给用户,用户因此就获得一块可以自由操作的内存;mmap不一样的是直接从动态内存区域直接映射一块未被使用的内存块给用户,该内存块可以通过munmap释放。且看下面一段程序(程序三):
#include
#include
#include
int main(){
void* p = sbrk(0);
clock_t count = clock();
for(unsigned int i=0; i<0x5fffff; i++){
int* p1 = (int*)p;
int ret = brk(p1 + 256); //malloc 1k
p1[256] = 256;
ret = brk(p); // free 1k
}
std::cout <<"Interval = " << clock() - count << "ticks" << std::endl;
return 0;
}
sbrk和brk都可以向内核申请内存和释放,只不过调用的方式不一样而已,上面通过sbrk获得应用程序刚加载的时候brk指针所在的位置,在for循环里通过调用brk函数去申请和释放内存,但是这种通过系统调用去申请和释放内存的效率怎么样?下图是测试的结果
通过这种原始的系统调用去分配和释放内存的效率是相当低的,总体来讲存在以下三个问题:
相关视频推荐
200行代码实现slab,开启内存池的内存管理(准备linux环境)
5种内存泄漏检测方式,让你重新理解内存管理
linux内存管理-庞杂的内存问题,如何理出自己的思路出来
需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
正是因为使用原始的系统调用brk、mmap和munmap存在很多问题,那么libc针对性的给出了优化的方案,如glibc一开始就调用brk或者mmap准备一块比较大的内存,然后把比较大的内存划分成小内存,当我们调用malloc时直接给用户,释放内存时并不一定就归还给内核,而是继续持有,以便调用malloc时能够继续用,而不用去向操作系统要(哦!我的措辞不够严谨,且听下文详细分析)。
glibc的内存块采用chunk管理,并且将大小相似的chunk用链表管理,一个链表被称为一个bin。chunk的结构如下图:
总共是128个bin,前64个bin里,相邻的bin内的chunk大小相差8字节,称为small bin,后面的是large bin,large bin里的chunk按先大小,再最近使用的顺序排列。分配内存时从所有的bins里查找最合适的chunk。如果是特别大的内存则从top chunk分配,如果不够且小于128k则调用brk扩充top chunk的内存,否则直接调用mmap分配内存。
glibc的分配流程是:
具体如图:
malloc流程
glibc的释放流程:
那么glibc存在什么不足呢?
tcmalloc是Google开源的一个内存管理库, 作为glibc malloc的替代品。目前已经在chrome、safari等知名软件中运用。
Talk is cheap, show me the code。
#include
#include
#include
class CTestClass{
char m_chBuf[4096];
};
int main(){
clock_t count = clock();
for(unsigned int i=0; i<0x5fffff; i++){
CTestClass *p = new CTestClass;
delete p;
}
std::cout << "Interval = " << clock() - count << " sec" << std::endl;
return 0;
}
同样的代码引入了tcmalloc分配内存后,效率怎么样呢?
至此,我们做个总结:
那tcmalloc又是如何做到的呢?
我们先来看一张图:
我们先来解释一下,tcmalloc为每一个线程分配了一个ThreadCache区域,每个线程都从属于自己的ThreadCache里分配和释放内存,这样就避免了glibc里多线程分配和释放内存的时候的锁的操作,锁是很有代价的,从上文的对比测试我们也能查看出他们的差异,那么我们总结出今天的第一个设计上的结论:真正高效的并发不是使用互斥锁、原子操作和自旋锁去解决对共享资源的访问,而是不共享任何资源,如果真要共享那么实现线程/进程间的通信。
那么tcmalloc是如何优化内存碎片的问题呢?答案就是CentralCache,当释放内存的时候,ThreadCache依然遵循批量释放的策略,对象积累到一定程度就释放给 CentralCache;CentralCache发现一个 Span的内存完全释放了,就可以把这个 Span 归还给 PageHeap;PageHeap发现一批连续的Page都释放了,就可以归还给操作系统,当ThreadCache的内存不够用的时候,那么就向CentralCache去借内存,然后分配出去,那么我们就能得出一个结论内存可以通过CentralCache在ThreadCache间流动,那么就在glibc的再一次优化了内存利用率,降低了内存碎片率。
最后我们以一位大师的话来总结为什么需要内存池的设计:
为什么需要内存池
页面更新:2024-04-22
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号