[转]STL的内存分配器
题记:内存管理一直是C/C++程序的红灯区。关于内存管理的话题,大致有两类侧重点,一类是内存的正确使用,例如C++中new和delete应该成对出现,用RAII技巧管理内存资源,auto_ptr等方面,很多C/C++书籍中都使用技巧的介绍。另一类是内存管理的实现,如linux内核的slab分配器,STL中的allocator实现,以及一些特定于某种对象的内存管理等。最近阅读了一些内存管理实现方面的资料和源码,整理了一下,汇编成一个系列介绍一些常用的内存管理策略。
1. STL容器简介
STL提供了很多泛型容器,如vector,list和map。程序员在使用这些容器时只需关心何时往容器内塞对象,而不用关心如何管理内存,需要用多少内存,这些STL容器极大地方便了C++程序的编写。例如可以通过以下语句创建一个vector,它实际上是一个按需增长的动态数组,其每个元素的类型为int整型:
stl::vector<int> array;
拥有这样一个动态数组后,用户只需要调用push_back方法往里面添加对象,而不需要考虑需要多少内存:
array.push_back(10);
array.push_back(2);
vector会根据需要自动增长内存,在array退出其作用域时也会自动销毁占有的内存,这些对于用户来说是透明的,stl容器巧妙的避开了繁琐且易出错的内存管理工作。
2. STL的默认内存分配器
隐藏在这些容器后的内存管理工作是通过STL提供的一个默认的allocator实现的。当然,用户也可以定制自己的allocator,只要实现allocator模板所定义的接口方法即可,然后通过将自定义的allocator作为模板参数传递给STL容器,创建一个使用自定义allocator的STL容器对象,如:
stl::vector<int, UserDefinedAllocator> array;
大多数情况下,STL默认的allocator就已经足够了。这个allocator是一个由两级分配器构成的内存管理器,当申请的内存大小大于128byte时,就启动第一级分配器通过malloc直接向系统的堆空间分配,如果申请的内存大小小于128byte时,就启动第二级分配器,从一个预先分配好的内存池中取一块内存交付给用户,这个内存池由16个不同大小(8的倍数,8~128byte)的空闲列表组成,allocator会根据申请内存的大小(将这个大小round up成8的倍数)从对应的空闲块列表取表头块给用户。
这种做法有两个优点:
1)小对象的快速分配。小对象是从内存池分配的,这个内存池是系统调用一次malloc分配一块足够大的区域给程序备用,当内存池耗尽时再向系统申请一块新的区域,整个过程类似于批发和零售,起先是由allocator向总经商批发一定量的货物,然后零售给用户,与每次都总经商要一个货物再零售给用户的过程相比,显然是快捷了。当然,这里的一个问题时,内存池会带来一些内存的浪费,比如当只需分配一个小对象时,为了这个小对象可能要申请一大块的内存池,但这个浪费还是值得的,况且这种情况在实际应用中也并不多见。
2)避免了内存碎片的生成。程序中的小对象的分配极易造成内存碎片,给操作系统的内存管理带来了很大压力,系统中碎片的增多不但会影响内存分配的速度,而且会极大地降低内存的利用率。以内存池组织小对象的内存,从系统的角度看,只是一大块内存池,看不到小对象内存的分配和释放。
实现时,allocator需要维护一个存储16个空闲块列表表头的数组free_list,数组元素i是一个指向块大小为8*(i+1)字节的空闲块列表的表头,一个指向内存池起始地址的指针start_free和一个指向结束地址的指针end_free。空闲块列表节点的结构如下:
union obj {
union obj *free_list_link;
char client_data[1];
};
这个结构可以看做是从一个内存块中抠出4个字节大小来,当这个内存块空闲时,它存储了下个空闲块,当这个内存块交付给用户时,它存储的是用户的数据。因此,allocator中的空闲块链表可以表示成:
obj* free_list[16];
3. 分配算法
allocator分配内存的算法如下:
算法:allocate
输入:申请内存的大小size
输出:若分配成功,则返回一个内存的地址,否则返回NULL
{
if(size大于128){ 启动第一级分配器直接调用malloc分配所需的内存并返回内存地址;}
else {
将size向上round up成8的倍数并根据大小从free_list中取对应的表头free_list_head;
if(free_list_head不为空){
从该列表中取下第一个空闲块并调整free_list;
返回free_list_head;
} else {
调用refill算法建立空闲块列表并返回所需的内存地址;
}
}
}
算法: refill
输入:内存块的大小size
输出:建立空闲块链表并返回第一个可用的内存块地址
{
调用chunk_alloc算法分配若干个大小为size的连续内存区域并返回起始地址chunk和成功分配的块数nobj;
if(块数为1)直接返回chunk;
否则
{
开始在chunk地址块中建立free_list;
根据size取free_list中对应的表头元素free_list_head;
将free_list_head指向chunk中偏移起始地址为size的地址处, 即free_list_head=(obj*)(chunk+size);
再将整个chunk中剩下的nobj-1个内存块串联起来构成一个空闲列表;
返回chunk,即chunk中第一块空闲的内存块;
}
}
算法:chunk_alloc
输入:内存块的大小size,预分配的内存块块数nobj(以引用传递)
输出:一块连续的内存区域的地址和该区域内可以容纳的内存块的块数
{
计算总共所需的内存大小total_bytes;
if(内存池中足以分配,即end_free - start_free >= total_bytes) {
则更新start_free;
返回旧的start_free;
} else if(内存池中不够分配nobj个内存块,但至少可以分配一个){
计算可以分配的内存块数并修改nobj;
更新start_free并返回原来的start_free;
} else { //内存池连一块内存块都分配不了
先将内存池的内存块链入到对应的free_list中后;
调用malloc操作重新分配内存池,大小为2倍的total_bytes加附加量,start_free指向返回的内存地址;
if(分配不成功) {
if(16个空闲列表中尚有空闲块)
尝试将16个空闲列表中空闲块回收到内存池中再调用chunk_alloc(size, nobj);
else {
调用第一级分配器尝试out of memory机制是否还有用;
}
}
更新end_free为start_free+total_bytes,heap_size为2倍的total_bytes;
调用chunk_alloc(size,nobj);
}
}
算法:deallocate
输入:需要释放的内存块地址p和大小size
{
if(size大于128字节)直接调用free(p)释放;
else{
将size向上取8的倍数,并据此获取对应的空闲列表表头指针free_list_head;
调整free_list_head将p链入空闲列表块中;
}
}
假设这样一个场景,free_list[2]已经指向了大小为24字节的空闲块链表,如图1所示,当用户向allocator申请21字节大小的内存块时,allocaotr会首先检查free_list[2]并将free_list[2]所指的内存块分配给用户,然后将表头指向下一个可用的空闲块,如图2所示。注意,当内存块在链表上是,前4个字节是用作指向下一个空闲块,当分配给用户时,它是一块普通的内存区。
图1 某时刻allocator的状态
图2 分配24字节大小的内存块
4. 小结
STL中的内存分配器实际上是基于空闲列表(free list)的分配策略,最主要的特点是通过组织16个空闲列表,对小对象的分配做了优化。
1)小对象的快速分配和释放。当一次性预先分配好一块固定大小的内存池后,对小于128字节的小块内存分配和释放的操作只是一些基本的指针操作,相比于直接调用malloc/free,开销小。
2)避免内存碎片的产生。零乱的内存碎片不仅会浪费内存空间,而且会给OS的内存管理造成压力。
3)尽可能最大化内存的利用率。当内存池尚有的空闲区域不足以分配所需的大小时,分配算法会将其链入到对应的空闲列表中,然后会尝试从空闲列表中寻找是否有合适大小的区域,
但是,这种内存分配器局限于STL容器中使用,并不适合一个通用的内存分配。因为它要求在释放一个内存块时,必须提供这个内存块的大小,以便确定回收到哪个free list中,而STL容器是知道它所需分配的对象大小的,比如上述:
stl::vector<int> array;
array是知道它需要分配的对象大小为sizeof(int)。一个通用的内存分配器是不需要知道待释放内存的大小的,类似于free(p)。
[转]STL的内存分配器的更多相关文章
- 从零开始写STL-内存部分-内存分配器allocator
从零开始写STL-内存部分-内存分配器allocator 内存分配器是什么? 一般而言,c++的内存分配和释放是这样操作的 >>class Foo{ //...}; >>Foo ...
- 14.6.4 Configuring the Memory Allocator for InnoDB 配置InnoDB 内存分配器
14.6.4 Configuring the Memory Allocator for InnoDB 配置InnoDB 内存分配器 当InnoDB 被开发时,内存分配提供了操作系统和 run-time ...
- Nah Lock: 一个无锁的内存分配器
概述 我实现了两个完全无锁的内存分配器:_nalloc 和 nalloc. 我用benchmark工具对它们进行了一组综合性测试,并比较了它们的指标值. 与libc(glibc malloc)相比, ...
- linux内存管理--伙伴系统和内存分配器
3.1页框的管理 所有的页框描述符都存放在mem_map数组中. 3.1.1page数据结构 struct page { page_flags_t flags; //标志 atomic_t _coun ...
- 14.4.4 Configuring the Memory Allocator for InnoDB InnoDB 配置内存分配器
14.4.4 Configuring the Memory Allocator for InnoDB InnoDB 配置内存分配器 当InnoDB 被开发, 内分配齐 提供了与操作系统和运行库往往缺乏 ...
- CoreCLR源码探索(三) GC内存分配器的内部实现
在前一篇中我讲解了new是怎么工作的, 但是却一笔跳过了内存分配相关的部分. 在这一篇中我将详细讲解GC内存分配器的内部实现. 在看这一篇之前请必须先看完微软BOTR文档中的"Garbage ...
- FDG内存分配器笔记
FDG: 大规模并行系统中的动态内存分配器由于需要全局同步(记账) ,导致性能急剧下降. 代码解析 1.superblock 类中包含两个变量,两个函数.默认superblock大小为2048 ite ...
- [内存管理]连续内存分配器(CMA)概述
作者:Younger Liu, 本作品采用知识共享署名-非商业性使用-相同方式共享 3.0 未本地化版本许可协议进行许可. 原文地址:http://lwn.net/Articles/396657/ 1 ...
- Mesh内存分配器的mmap小技巧
最近看了一篇内存分配器的论文,原理很简单,但是里面的数学论证还没看懂,这次先简单写一下原理和用到的API. 内存分配器是用于封装操作系统提供的底层API,给应用程序提供动态内存的.内存不断申请释放后, ...
随机推荐
- 一个Java对象到底占多大内存
最近在读<深入理解Java虚拟机>,对Java对象的内存布局有了进一步的认识,于是脑子里自然而然就有一个很普通的问题,就是一个Java对象到底占用多大内存? 在网上搜到了一篇博客讲的非常好 ...
- MySql的大小写问题
原来Linux下的MySQL默认是区分表名大小写的,通过如下设置,可以让MySQL不区分表名大小写:1.用root登录,修改 /etc/my.cnf:2.在[mysqld]节点下,加入一行: lowe ...
- PHP SPL使用
转载文章:php遗落的宝石 Rafael Dohms 上面的篇文章 让我惊艳了下,忍不住就翻译了下来,同时补充了部分内容. SPL,PHP 标准库(Standard PHP Library) ,此从 ...
- struts2 Convention插件零配置,使用注解开发
从struts21开始,struts2不再推荐使用codebehind作为零配置插件,而是改用Convention插件来支持零配置.与以前相比较,Convention插件更彻底. 使用Conventi ...
- hiho #1079 : 离散化
描述 小Hi和小Ho在回国之后,重新过起了朝7晚5的学生生活,当然了,他们还是在一直学习着各种算法~ 这天小Hi和小Ho所在的学校举办社团文化节,各大社团都在宣传栏上贴起了海报,但是贴来贴去,有些海报 ...
- 【转】android ListView详解---- 不错不错
原文网址:http://www.cnblogs.com/allin/archive/2010/05/11/1732200.html 由于google doc 很多人都打不开,故更新了源码下载地址 [源 ...
- sql server 删除索引的语句
DROP INDEX index_name ON talbe_nameDROP INDEX IX_TBlueyBook_10 ON 表名
- 高效算法——A 归并排序
In this problem, you have to analyze a particular sorting algorithm. The algorithm processes a seque ...
- apt局域网源搭建
1, 准备Packages.gz
- 用Myeclipse 编写struts.xml时,自动提示
之所以不自动提示,是因为这个xml它不知道自己的xml格式是什么有哪些标签,所以不知道该怎么提示 所以就要给它引入格式,所以要引入XSD或者DTD文件 1.首先打开MyEclipse的窗口,选择“Wi ...