"我有时难过,却还有些抚慰和感动。"
一、我们来谈谈空间适配器
(1) 什么是空间配置器?
STL的六大组件,容器、算法、迭代器、适配器、仿函数,最后一个也就是"空间适配器"。
所谓"空间适配器",顾名思义,就是对STL中各个容器的内存进行高效的管理。也许你会说,诶,我写了这么多的C++代码,为什么没有这个概念呢?或者说,为什么我们根本没有见到!这个空间适配器呢?
然而事实上,不是说,我们没有使用,而是在我们使用诸如vector\list\deque时,我们的空间配置器是在默默地为我们进行工作。
(2) 为什么需要空间适配器呢?
我们使用堆上的空间,管它三七二十一,直接malloc 或者new不就得了?为什么还需要在空间申请的过程里,添加这一个适配器呢?对于使用者而言不麻烦吗?对于设计者而言,不也会给他们带来麻烦嘛?
是的,如果我们仅单单从学习语言的角度来看,似乎无脑用malloc、new并没有啥有待商榷的地方,毕竟那本来就是提供给应用层调用的函数。
但如果站在系统层面上,也许你在语言层调用malloc、new只是看到了单单的函数调用,并你接收到了来自函数的返回值"void*",你就可以针对这一块对空间上的内存块进行操作,仅此而已。你根本不知道操作系统在底层为你的行为做了哪些操作。
唔,大概在底层,操作系统会为你做如下的事情:
如果是在Windos下,malloc\new在底层会去调用 VirtualAlloc 向操作系统申请堆空间,如果是在Linux下,malloc、new会在底层调用 brk或者 mmap得到堆空间的起始地址。
这似乎很符合我们的预期,与maloc、new相比,不就多调用了一次函数而已? 但事实真的是这样嘛?
我们以在Linux环境下申请、释放空间例举:
我们以ARM64架构下来划分虚拟进程地址空间,编制从全0~全F。
高16位(0xFFFF 0000 0000 0000 ~ 0xFFFF FFFF FFFF FFFF) --> 内核地址空间
低16位(0x0000 0000 0000 0000 ~ 0x0000 FFFF FFFF FFFF) --> 用户地址空间
不管我们使用什么样的函数,一旦涉及到要使用系统资源,例如: 堆空间、文件描述符、套接字描述符…… 其底层都需要访问系统提供的接口函数。然而,用户是不能直接执行内核代码的,而是需要切换成 内核用户才能执行代码,这个过程也叫做 "陷入内核",将用户态切换为内核态。显然,这个过程是很耗时的。
不仅如此,Linux有自己内部的内存管理系统,如"伙伴系统",它需不需要维护系统堆空间上的资源?需要!难道它直接就把那块内存块扔给 用户?需不需要调剩余内存块的结构呢? 它需不需要对释放完的内存块进行管理,以避免内存碎片问题……
有了上面的论述,空间适配器的出现,也就具有必然性。
malloc\new的不足之处
① 空间申请与释放需要用户自己管理,容易造成内存泄漏。
② 频繁向系统申请小块内存块,容易造成内存碎片。
③ 频繁向系统申请小块内存,影响程序运行效率。
④ 未考虑线程安全问题。
⑤ 代码结构比较混乱,代码复用率不高。
因此需要设计一块高效的内存管理机制。
二、空间配置器窥探源码
(1) 空间适配器原理
以上提及的用new、malloc最主要的一个问题是,"频繁"二字,在SGI版本中,空间配置器
以128作为 小块内存 与 大块内存的分割线。由此,其空间分配的结构分为两个的等级:一级空间配置器用于处理 大块内存的申请、释放,二级空间配置器用于处理 小块内存的申请、释放。
(2) 具体实现
一级适配器:
一级适配器原理很简单,就是一个对malloc、free简单的封装。
例如这里一个simple_alloc 使用这个适配器。
二级适配器:
我们在前面说SGI版本的Alloc,对于二级适配器而言,是一个管理这1~128这个范围的内存块。那么如何管理这一堆切小的小块内存呢,SGI采用了哈希桶的方式进行管理。但是,1~128,难道需要我们用开128个桶的空间来管理嘛?答案是否定的,第一个是从使用上来说,大多数开辟空间的大小都是4的正数倍,其次是,如果对内存空间的管理过于细腻,必定会造成一定空间浪费的问题。因此,SGI-STL将用户申请的内存块,向上对齐按照8byte。
此时,我们原本需要128个桶来唯一标识一个内存块对应的挂接位置,变为只需要16个桶。
// 计算对齐后的 大小
static size_t ROUND_UP(size_t bytes) {
return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1));
}
// 计算该字节大小 对应哪个桶
static size_t FREELIST_INDEX(size_t bytes) {
return (((bytes) + __ALIGN-1)/__ALIGN - 1);
}
STI-STL提供了两个函数,分别可用来计算 对齐字节大小 和 内存块挂接的桶位置。
refill与chunk_alloc:
二级适配器还考虑了多线程环境下,Alloc的场景。
(3) 空间适配器与具体容器
三、 如何理解STL?
STL的六大组件包括,算法、迭代器、容器、适配器、分配器(空间配置器)、仿函数。这几者有何关联呢?
总结:
空间配置器其底层技术就是采用的池化技术,可以说就是一个小型的内存池。能在频繁申请小块内存的场景中,提高一定的性能。
STL六大组件:算法、迭代器、容器、适配器、分配器(空间配置器)、仿函数。
本篇到此结束,感谢你的阅读
祝你好运,向阳而生~