c++ - 第25节 - STL之空间配置器

目录

1.什么是空间配置器

2.为什么需要空间配置器

3.SGI-STL空间配置器实现原理

4.STL空间配置器的使用


1.什么是空间配置器

空间配置器,顾名思义就是 为各个容器高效的管理空间(空间的申请与回收),在默默地工作。虽然在常规使用STL时,可能用不到它,但站在学习研究的角度,学习它的实现原理对我们有很大的帮助。

2.为什么需要空间配置器

前面在模拟实现vector、list、map、unordered_map等容器时,所有需要空间的地方都是通过new申请的,虽然代码可以正常运行,但是有以下不足之处:
\bullet 空间申请与释放需要用户自己管理,容易造成内存泄漏
\bullet 频繁向系统申请小块内存块,容易造成内存碎片
\bullet 频繁向系统申请小块内存,影响程序运行效率
\bullet 直接使用malloc与new进行申请,每块空间前有额外空间浪费(因为每块空间都要开额外的内存空间记录这块空间的大小)
\bullet 申请空间失败怎么应对
\bullet 代码结构比较混乱,代码复用率不高
\bullet 未考虑线程安全问题

因此需要设计一块高效的内存管理机制。


3.SGI-STL空间配置器实现原理

在windows和Linux下都有直接向堆申请空间的接口,windows下接口为VirtualAlloc,Linux下接口为brk。malloc是封装了irtualAlloc和brk在VirtualAlloc和brk之上,malloc本身其实也是一个内存池,该内存池是面向整个程序的。空间配置器对malloc进行封装 ,其是在malloc之上针对STL容器来设计的,空间配置器是一个内存池,该内存池仅面向STL的容器。

第二节中提到的几点不足之处,最主要还是:频繁向系统申请小块内存造成的。那什么才算是小块内存?SGI-STL以128字节作为小块内存与大块内存的分界线,将空间配置器其分为两级结构,一级空间配置器处理大块内存,二级空间配置器处理小块内存。

下面我们根据STL配置器的源码来进行介绍。 

一级空间配置器__malloc_alloc_template:

__malloc_alloc_template中allocate成员函数进行malloc的封装,deallocate成员函数进行free的封装,如下图所示。所以说一级空间配置器其实就是malloc和free,唯一的区别就是一级空间配置器中如果malloc空间失败会去调用oom_malloc函数。

oom_malloc函数的功能是先利用my_malloc_handler函数释放内存空间,如果函数指针my_malloc_handler为空则无法释放内存空间,抛异常,如果函数指针my_malloc_handler不为空则可以释放空间,调用my_malloc_handler释放空间然后再尝试malloc,如果malloc成功返回空间的指针结果。如下图所示。

总结:一级空间配置器和operator new很像,是malloc和free的封装,其特点是申请内存失败了抛异常。

二级空间配置器__default_alloc_template:

__default_alloc_template类中allocate成员函数中如果大于__MAX_BYTES,__MAX_BYTES是一个枚举常量为128,如果大于128则调用一级空间配置器来处理,如果小于128则在此处的二级空间配置器处理,如下图所示。

注:这里可以看出STL容器申请空间其实会直接找二级空间配置器,如果申请空间大于128字节才会跳到一级空间配置器中。

二级空间配置器allocate成员函数,如下图所示,free_list是一个哈希表。三个成员变量start_free、end_free和heap_size用来支持内存池,内存池是提前开辟好的128字节空间,其中start_free和end_free指针指向内存的头和尾。

每当需要申请128字节以内的空间时,让一个指针指向start_free指向的位置,然后start_free向后移动跳过此时申请空间的大小,将前面跳过的空间分配出去使用,最后如果这个内存池空间用完了,再去malloc开辟128字节空间作为新内存池即可。分配出去的某一小块内存不再使用回收后可以再次分配出去使用,如何管理分配出去小块内存的回收呢?

如果将回收的小块内存使用链表进行链接,再次申请空间时,通过链表依次查找回收的小块内存空间大小是否满足申请大小,满足则该小块内存空间可以再分配出去使用。使用链表进行管理依次查找的效率很低,可以使用哈希表来进行优化,对回收的小块内存进行管理。

那是否需要128桶个空间来管理用户已经归还的内存块呢?答案是不需要,因为用户申请
的空间基本都是4的整数倍,其他大小的空间几乎很少用到。因此SGI-STL将用户申请的内存块向上对齐到了8的整数倍,如上图所示哈希表,哈希表0号桶挂的是8字节的内存空间......15号桶挂的是128字节的内存空间。

内存池空间申请的流程:申请小于128字节空间时,首先算出申请空间大小对应哈希表的几号桶(如果申请1-8字节空间则都找哈希表的0号桶,如果申请9-16字节空间则都找哈希表的1号桶......如果申请121-128字节空间则都找哈希表的15号桶),如果对应桶中有回收的小块内存空间则直接获取,如果对应桶中没有回收的小块内存空间,则去start_free和end_free指针管理的内存池获取空间,在内存池获取空间也是在申请空间大小的基础上向上对齐到8的整数倍获取空间。

注:申请小空间内存时往往会申请多个,因此这里编译器会进行一个优化,如果申请小于128字节空间时,首先是算出申请空间大小对应哈希表的几号桶,如果对应桶中没有回收的小块内存空间会去内存池获取空间,如果要申请的空间大小为n,这里往往会向内存池申请多个n空间大小,start_free跳过这多个n空间大小指向后面,第一个n空间地址返回,其余的n空间挂在哈希表中以备下次申请时使用。

__default_alloc_template类中deallocate成员函数和allocate成员函数相同,如果大于__MAX_BYTES,__MAX_BYTES是一个枚举常量为128,如果大于128则调用一级空间配置器来处理,如果小于128则在此处的二级空间配置器处理,如下图所示。

注:这里可以看出STL容器释放空间其实会直接找二级空间配置器,如果申请空间大于128字节才会跳到一级空间配置器中。

二级空间配置器deallocate成员函数中,如果申请的内存空间不再使用要释放,则根据要释放空间大小计算出哈希表对应的桶号,将要释放空间头插在哈希表对应的桶中。

问题:哈希表每个桶中挂的是一个单向链表,这里挂的是内存空间,内存空间如何像链表一样挂起来呢?

答:每一个内存空间如果要挂在哈希表中,首先要将指向该空间的指针强制转换成下图所示的联合体obj*类型,该空间前四个字节就存的是联合体指针free_list_link,free_list_link指向下一个被强转成联合体的空间地址,最后一个被强转成联合体的空间中前四个字节的free_list_link变量存的是空,这样就可以像链表一样链接起来。

当申请空间,哈希表桶中空间被拿走使用时,该空间所有的字节都可以随便使用,该空间释放时,再将该空间指针强转成联合体obj*类型,头插在哈希表对应桶中,并与桶中原本挂着的空间进行链接。

问题:SGI-STL将用户申请的内存块向上对齐到了8字节的整数倍,这里为什么是8字节的整数倍,而不是4字节的整数倍?
原因:哈希表的桶中要将空间的指针强制转换成联合体obj*类型,通过联合体的前四个字节的free_list_link指针进行空间链接。在32位机器下指针是4字节的,在64位机器下指针是8字节的,如果申请内存块向上对齐到了4字节的整数倍,而申请空间刚好为4字节的话这里就无法强转,因为free_list_link指针要占8字节空间,而申请空间本身才4字节。

一个进程里面应该只有一个空间配置器,因此空间配置器的类可以设置成单例模式的类,这样哈希表变量、管理内存池的各指针变量都对应只有一个,每次容器要开辟和释放空间时通过GetInstance函数调用allocate和deallocate成员函数即可。

如下图所示,这里源代码没有使用前面我们所学的单例模式,源代码是把所有的成员变量都设置为静态的,这样虽然可以创建多个对象,但一个进程中空间配置器类的每个成员变量都只有对应的一个,这也近似算是一种近似单例。

如果频繁申请小块内存,空间配置器相比于malloc,效率更高,且可以在一定程度内缓解内存碎片的问题。

内存碎片问题:

内存内碎片问题:申请下来的内存空间没有全部使用就是内碎片问题。空间配置器中将内存挂在哈希表中管理,申请空间时申请的内存块向上对齐到了8字节的整数倍,就会导致内碎片问题。

内存外碎片问题:在有足够的内存的条件下,频繁小块的内存空间申请,小块内存如果间隔的释放,那么可用内存被分隔开,就导致了内存的碎片化,因此无法开辟大块内存空间,这就是内存外碎片问题。内存外碎片问题其实就是频繁向内存申请小块内存空间导致的。

空间配置器缓解外碎片问题:空间配置器一次性开辟大块内存空间,将大块内存空间切割成小块内存空间挂在哈希表中,以满足自己的需求,这里小块内存空间是连续的,因此在一定程度内缓解内存外碎片的问题。


4.STL空间配置器的使用

以list为例如下图所示,list类模板的第二个参数就是空间配置器Alloc,缺省参数给的是alloc,那么alloc是什么呢?

如下图所示,alloc是STL库中二级的空间配置器,作为STL容器的默认空间配置器。如果容器申请空间大小大于128字节就会跳到STL库中的一级空间配置器,如果容器申请空间大小小于128字节就会继续执行二级空间配置器。

如下图一所示,在list类中使用simple_alloc类对二级空间配置器alloc进行封装,生成一个专门针对list_node的空间申请类list_node_allocator。如下图二所示是simple_alloc类中对alloc的封装,分别调用了二级空间配置器alloc的allocate函数和deallocate函数进行二级空间适配器的空间申请和释放。

如下图三所示,在list类中get_node和put_node调用封装的空间申请类list_node_allocator中allocate函数和deallocate函数进行节点空间的申请和释放。

如下图三所示,在create_node函数中,先调用get_node函数申请节点空间,因为get_node只申请了节点空间没有初始化,所以调用construct函数针对新申请的节点的data进行初始化,construct函数中使用了定位new来调用data对应类型的构造函数进行初始化工作(因为data可能是自定义类型的变量),如下图四所示。

如下图三所示,destroy_node函数中显式的调用data对应类型的析构函数,并调用put_node函数将节点释放掉。

注:STL各容器模板的Alloc参数可以传自己实现的空间适配器类,只要空间适配器类中定义实现了allocate函数和deallocate函数即可。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
STL是指标准模板库(Standard Template Library),它是C++语言的一部分,提供了一系列的模板类和函数,用于支持通用的数据结构和算法。STL的目标是提供高效、可重用和可扩展的组件,以便开发人员能够更轻松地编写高质量的代码。STL包含了许多常见的数据结构,如vector、list、set、map等,以及各种算法,比如排序、查找、遍历等。通过使用STL开发人员可以更加高效地处理各种数据结构和算法的问题,提高代码的开发效率和质量。 在STL中,我们可以使用各种容来存储和管理数据。例如,我们可以使用std::map来创建一个键值对的映射,其中每个键都有一个与之相关联的值。下面是一个示例代码,展示了如何创建和使用一个std::map对象: std::map<std::string, int> disMap() { std::map<std::string, int> tempMap{ {"C语言教程",10},{"STL教程",20} }; return tempMap; } std::map<std::string, int> newMap(disMap()); 在这个示例中,disMap()函数创建了一个临时的std::map对象,并初始化了其中的一些键值对。然后,使用移动构造函数将这个临时对象移动到了一个新的std::map对象newMap中。最终,我们可以通过newMap对象来访问和操作这些键值对。 综上所述,STLC++中的标准模板库,提供了一系列的模板类和函数,用于支持通用的数据结构和算法。STL的使用可以提高代码的开发效率和质量,并且通过各种容和算法,可以方便地处理各种数据结构和算法的问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [C++ STL详解超全总结(快速入门STL)](https://blog.csdn.net/qq_50285142/article/details/114026148)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [【C++实验】阅读STL源码并分析](https://blog.csdn.net/qq_35760825/article/details/125311509)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

随风张幔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值