文章目录
空间配置器(了解)
1.什么是空间配置器
空间配置器,顾名思义就是为各个容器高效的管理空间(空间的申请与回收)的,在默默地工作。虽然在常规使用STL时,可能用不到它,但站在学习研究的角度,学习它的实现原理对我们有很大的帮助。
2. 为什么需要空间配置器
前面在模拟实现vector、list、map、unordered_map等容器时,所有需要空间的地方都是通过new申请,delete释放空间的。
new申请空间
1.operator new—>malloc
2.调用析构函数—完成对象的构造
虽然代码可以正常运行,但是有以下不足之处:
- 有些构造函数调用不是必须的—构造函数意义不大还会影响程序性能
- vector和list中都可以存放元素—假设存放10000个元素—>都是一个一个的节点,需要使用12个字节,但实际申请了12+36个字节的空间,造成额外空间的浪费
- 频繁向系统申请小块内存块malloc,容易造成内存碎片
- 频繁向系统申请小块内存,影响程序运行效率,性能会下降
- 空间申请与释放需要用户自己管理,容易造成内存泄漏
- 直接使用malloc与new进行申请,每块空间前有额外空间浪费
- 申请空间失败怎么应对
- 代码结构比较混乱,代码复用率不高
- 未考虑线程安全问题
STL设计原则:通用性+高效率
因此需要设计一块高效的内存管理机制
3. STL空间配置器的实现原理(版本:SGI-STL)
以上提到的几点不足之处,最主要还是:频繁向系统申请小块内存造成的。那什么才算是小块内存?SGI-STL以128作为小块内存与大块内存的分界线,将空间配置器其分为两级结构,一级空间配置器处理大块内存,二级空间配置器处理小块内存。
3.1STL空间配置器如何实现的?
频繁向系统申请一些小的内存块
大于128字节—>大内存块,一级空间配置器
小于等于128字节—>小内存块,二级空间配置器
3.2一级空间配置器
实现原理: malloc+free—>set_new_handle
以下代码来自于stl_alloc.h
template <int inst>
class _malloc_alloc_template
{
private:
static void *oom_malloc(size_t);
public:
// 对malloc的封装
static void * allocate(size_t n)
{
// 申请空间成功,直接返回,失败交由oom_malloc处理
void *result = malloc(n);
if (0 == result)
result = oom_malloc(n);
return result;
}
// 对free的封装
static void deallocate(void *p, size_t /* n */)
{
free(p);
}
// 模拟set_new_handle
// 该函数的参数为函数指针,返回值类型也为函数指针
// void (* set_malloc_handler( void (*f)() ) )()
static void(*set_malloc_handler(void(*f)()))()
{
void(*old)() = _malloc_alloc_oom_handler;
_malloc_alloc_oom_handler = f;
return(old);
}
};
// malloc申请空间失败时代用该函数
template <int inst>
void * _malloc_alloc_template<inst>::oom_malloc(size_t n)//系统内存空间不足进入该函数
{
void(*my_malloc_handler)();
void *result;
for (;;)
{
// 检测用户是否设置空间不足应对措施,如果没有设置,抛异常,模式new的方式
my_malloc_handler = _malloc_alloc_oom_handler;//接收该函数指针,用户提供的函数:空间不足的应对措施
if (0 == my_malloc_handler)//验证该函数指针是否为空,是:直接抛异常
{
_THROW_BAD_ALLOC;
}
// 如果设置,执行用户提供的空间不足应对措施
(*my_malloc_handler)();//调用函数指针对应的函数
// 继续申请空间,可能就会申请成功
result = malloc(n);
if (result)
return(result);
//申请失败,循环继续
}
}
typedef _malloc_alloc_template<0> malloc_alloc;
3.3二级空间配置器
为了解决频繁向系统申请小的内存块造成的缺陷
二级空间配置器专门负责处理小于128字节的小块内存。如何才能提升小块内存的申请与释放的方式呢?SGISTL采用了内存池的技术来提高申请空间的速度以及减少额外空间的浪费,采用哈希桶的方式来提高用户获取空间的速度与高效管理。
1.内存池技术
内存池就是:先申请一块比较大的内存块已做备用,当需要内存时,直接到内存池中去去,当池中空间不够时,再向内存中去取,当用户不用时,直接还回内存池即可。避免了频繁向系统申请小块内存所造成的效率低、内存碎片以及额外浪费的问题。
当用户需要4个字节时,将_start向后移动4个字节
当用户需要8、10个字节时,将_start向后移动18个字节
空间申请:res=_start;_start=+n;
用户用完时会归还,但直接归还并不简单,所以用链表结构—>管理用户归还的内存块
当用户需要再次申请时:最好优先从归还链表中找到需要的大小内存块,除非找不到才到内存池剩余块中找,如上图,假如经过多次从内存池剩余块中申请内存,内存池剩余块最后只有10个字节,当这时用户需要16个字节内存块时,内存池已经给不出来了,因此,最好不要一直申请内存池剩余块中的内存,只要下面归还的内存块链表中能够找到就尽量使用归还的内存块!
大致步骤就是:
-
现在已经归还的内存块中找合适块
-
找到—>是否需要分割
不需要—>直接分配
需要—>分割成需要的大小的内存块 -
未找到—>到内存池中申请
缺陷
效率低—>O(N)—>哈希—>O(1)
分割多次就会导致内存块越来越小,只能解决外部碎片(空间固定为x的倍数:根据系统选择)最少为4的倍数—>32位系统:
最少为8的倍数—>64位系统(会出现浪费过多):128/8
申请归还比较高效
简单描述,仅供参考
void allocate(size_t n)
{
if (n > 128)
;//调用一级空间适配器申请
//1.用n计算桶号
//2.检测该桶中是否有节点(内存块)
//有:使用头删法将第一个内存块返回
//没有:refill(Rount_up(n));
}
void deallocate(void* p, size_t n)
{
if (n > 128)
;
//1.计算桶号
//2.将该块内存挂接到对应的痛
}
void* refill(size_t n/*已经是8的整数倍*/)
{
size_t nobjs = 20;
char* chunk = chunk_alloc(n, nobjs);
if (nobjs == 1)
return chunk;
//1<n<=20
//将第一个内存块保存,最后要返回值给外部用户
//将剩余nobjs-1个内存块挂接到对应的桶中
}
void* chunk_alloc(size_t size, size_t& nobjs/*20*/)
{
size_t totalBytes = nobjs * size;
size_t leftBytes = _finish - _start;
void* res = nullptr;
if (leftBytes >= totalBytes)//内存池空间冲突,可以提供20块
{
res = _start;
_start += totalBytes;
return res;
}
else if (leftBytes >= size)//内存池不能提供20块,至少可以提供一块
{
nobjs = leftBytes / size;
res = _start;
_start += nobjs * size;
return res;
}
else//leftBytes<size 内存池空间不足,连一块都提供不了
{
//1.将内存池中剩余的内存块(<128)--->挂接到对应的桶中
//2.total_get=2*total_bytes+RoundUP(heap_size>>4);
_start = malloc(total_get);
if (_start==nullptr)
{
//1.在哈希桶中找更大的内存块:
//找到了:将该内存块放到内存池中
return chunk_alloc(nobjs,n);
//未找到:
//2.求助一级空间配置器---空间不足的应对措施
}
heap_size += total_get;
finish = _start + total_get;
return chunk_alloc(nobjs,n);
}
}