4.SGI STL 二级空间配置器 allocate 与_S_refill 源码解析

本节我们继续深耕 SGI STL 二级空间配置器 核心源码,重点学习两大核心函数:allocate 内存分配函数_S_refill 内存池填充函数


一、allocate 主函数源码整体解析

1. 完整源码

cpp 复制代码
static void* allocate(size_t __n)
{
  void* __ret = 0;

  if (__n > (size_t) _MAX_BYTES) {
    __ret = malloc_alloc::allocate(__n);
  }
  else {
    _Obj* __STL_VOLATILE* __my_free_list
        = _S_free_list + _S_freelist_index(__n);
    // 构造函数加锁,栈销毁自动解锁,保证线程安全
#   ifndef _NOTHREADS
    _Lock __lock_instance;
#   endif
    _Obj* __RESTRICT __result = *__my_free_list;
    if (__result == 0)
      __ret = _S_refill(_S_round_up(__n));
    else {
      *__my_free_list = __result -> _M_free_list_link;
      __ret = __result;
    }
  }

  return __ret;
};

2. 核心分配逻辑

函数入参 __n 代表用户申请的内存字节数,整体分为两套分配策略,分界值为 _MAX_BYTES (128 字节)

  • 大于 128 字节 :不走内存池,直接调用一级空间配置器,底层通过 malloc 向系统申请内存;
  • 小于等于 128 字节 :走二级空间配置器内存池,通过自由链表复用内存块,规避频繁调用 malloc 造成的性能损耗和内存碎片。

3. 小内存块(≤128 字节)详细分配流程

① 定位对应规格自由链表

通过二级指针 __my_free_list,结合 _S_freelist_index 计算出的下标,精准定位 _S_free_list 自由链表数组中对应的内存块规格。

举例:用户申请 15 字节内存,经过下标计算会定位到1 号自由链表,对应 16 字节规格的内存块。

② 多线程加锁保证安全

自由链表的增、删、改操作是非线程安全的,因此通过 RAII 机制创建锁对象,在核心逻辑执行前加锁,函数栈销毁时自动解锁,避免多线程竞争问题。

③ 空闲内存块分配逻辑
cpp 复制代码
_Obj* __RESTRICT __result = *__my_free_list;
// 取出当前自由链表的首个节点
if (__result == 0)
  // 链表为空,调用_S_refill填充内存池
  __ret = _S_refill(_S_round_up(__n));
else {
  // 链表不为空,执行头删操作,取出空闲块分配给用户
  *__my_free_list = __result -> _M_free_list_link;
  __ret = __result;
}

__result = *__my_free_list 用于获取当前规格自由链表的首个空闲内存块。

每个 chunk 内存块通过联合体实现内存复用:空闲时存储下一个节点地址,被使用时存储用户业务数据

  • 链表为空(__result == 0) :当前规格无空闲内存块,先通过 _S_round_up 完成 8 字节对齐,再调用 _S_refill 填充内存池;
  • 链表不为空(__result != 0) :执行链表头删操作,将链表头节点取出分配给用户,链表头指针后移。

二、_S_refill 内存池填充函数(自由链表补给站)

当对应规格自由链表无空闲块时,会触发 _S_refill 函数,核心作用:向内存池批量申请连续内存、切分固定大小 chunk 块、构建自由链表,为后续内存申请提前储备空闲块,大幅提升分配效率。

1. 完整源码

cpp 复制代码
template <bool __threads, int __inst>
void* __default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
    int __nobjs = 20;// 默认一次性申请20个对齐后的内存块
    // 向内存池申请大块内存,__nobjs为引用传递,会更新实际分配块数
    char* __chunk = _S_chunk_alloc(__n, __nobjs);
    _Obj* __STL_VOLATILE* __my_free_list;// 对应规格自由链表头指针
    _Obj* __result;                      // 返回给用户的内存块
    _Obj* __current_obj;                 // 链表构建当前节点
    _Obj* __next_obj;                     // 链表构建下一个节点
    int __i;                              // 循环计数器

    // 特殊情况:仅分配到1个块,直接返回无需建链表
    if (1 == __nobjs) return(__chunk);

    // 定位当前内存规格对应的自由链表
    __my_free_list = _S_free_list + _S_freelist_index(__n);
 
    /* 切分大块内存,构建自由链表 */
    // 第一个块分配给用户
    __result = (_Obj*)__chunk;
    // 剩余块挂载到自由链表,从第二个块开始
    *__my_free_list = __next_obj = (_Obj*)(__chunk + __n);

    // 循环串联所有剩余内存块
    for (__i = 1; ; __i++) {
        __current_obj = __next_obj;
        // 按字节精准偏移,获取下一个内存块
        __next_obj = (_Obj*)((char*)__next_obj + __n);

        // 最后一个节点,尾指针置空
        if (__nobjs - 1 == __i) {
            __current_obj -> _M_free_list_link = 0;
            break;
        }
        // 普通节点,串联下一个块
        else {
            __current_obj -> _M_free_list_link = __next_obj;
        }
    }
    // 返回首个内存块给用户
    return(__result);
}

2. 核心执行流程

① 批量申请内存块
cpp 复制代码
int __nobjs = 20;
char* __chunk = _S_chunk_alloc(__n, __nobjs);

默认一次性向内存池申请 20 个 对齐后的固定大小内存块;__nobjs 是引用传递,若内存池资源不足,会自动更新为实际分配到的块数。__chunk 指向申请到的连续大块内存首地址。

② 特殊场景处理(仅分配 1 个块)
cpp 复制代码
if (1 == __nobjs) return(__chunk);

若内存池资源极度紧张,仅能分配 1 个内存块,直接将该块返回给用户,无需构建自由链表(无剩余块可挂载)。

③ 定位对应自由链表

通过下标计算,找到当前内存规格对应的自由链表数组槽位,为后续挂载空闲块做准备。

④ 内存切分 + 构建单向链表

8 字节内存块 为例,申请 20 个块会得到一段连续内存:[块0][块1][块2]...[块19]

  • 块 0:直接分配给用户使用,作为函数返回值;
  • 块 1~ 块 19:全部串联成单向链表,挂载到对应规格的自由链表上,供下次复用。

3. 重点难点:指针偏移必须强转 char*

源码中最核心的细节,也是高频面试考点:

cpp 复制代码
__next_obj = (_Obj*)((char*)__next_obj + __n);

*为什么不能直接使用 _Obj 指针偏移?**

  1. _Obj 是联合体,固定占用 4 字节(指针大小);
  2. 若直接通过 _Obj* 偏移,+__n 会触发指针类型偏移规则(偏移大小 = 类型大小 × 偏移数),造成内存越界;
  3. char* 是 1 字节粒度指针,强转后 +__n 可以实现精准按字节偏移,完美切分固定大小的内存块。

4. 链表构建最终效果

批量申请 20 个内存块后:

  • 用户获得 块 0,正常使用;
  • 自由链表挂载:块1 → 块2 → 块3 ... → 块19 → nullptr
  • 后续同规格内存申请,直接从链表头部取块,无需调用系统内存申请函数,性能极高。

三、整体流程总结

  1. 用户调用 allocate 申请小内存块,先通过辅助函数完成内存对齐、定位对应自由链表;
  2. 判断链表是否有空闲块,有则直接头删分配,无则调用 _S_refill 批量补给内存;
  3. _S_refill 一次性申请 20 个内存块,一个交付用户,剩余批量挂载到自由链表;
  4. 用户使用完毕后,通过 deallocate 释放内存,内存块重新挂回自由链表,实现内存循环复用

四、核心设计优势

  • 规避频繁调用 malloc,减少系统调用开销;
  • 批量申请、按需分配,极大减少小内存块碎片;
  • O (1) 链表头尾操作,内存分配释放效率拉满;
  • 多线程加锁,保证并发场景下内存池操作安全。

(原笔记:GameServer-Learning/00-Notes/C++/SGI_MemoryPool at main · maomianbaobumoyu/GameServer-Learning

相关推荐
码界筑梦坊2 小时前
120-基于Python的食品营养特征数据可视化分析系统
开发语言·python·信息可视化·数据分析·毕业设计·echarts·fastapi
lsx2024062 小时前
《Foundation 模态框》
开发语言
fufu03112 小时前
vscode配置C/C++环境,用GDB调试简单程序分享
开发语言·c++
快乐江湖2 小时前
「层层包装」—— 装饰器模式
开发语言·python·装饰器模式
java1234_小锋2 小时前
String、StringBuilder、StringBuffer的区别?
java·开发语言
星原望野2 小时前
JAVA集合:List、Set和Map
java·开发语言·list·set·map·集合
摘星小杨3 小时前
如何在前端循环调取接口,实时查询数据
开发语言·前端·javascript
yujunl3 小时前
U9的UI插件客开的总结1
开发语言
多敲代码防脱发3 小时前
Spring进阶(容器实现)
java·开发语言·后端·spring