STL源码——空间配置器

1.概述

整个STL的操作对象(所有的数值)都存放在容器之内,而容器一定需要配置空间以置放资料。

一般C++内存配置和释放操作是这样的:

class A {};
A* pa = new A;
//...执行其他操作
delete pa;

这其中的new算式内含两阶段操作:(1)调用:: operator new配置内存(2)调用Fo::F0o()构造对象内容。 delete算式也内含两阶段操作:(1)调用Foo::~foo()将对象析构;(2)调用:: operator delete释放内存

为了精密分工, STL allocator决定将这两阶段操作区分开来。内存配置操作由 alloc::allocate()负责,内存释放操作由 alloc:: deallocate()负责对象构造操作由:: construct()负责,对象析构操作由:: destroy()负责

2.构造和析构工具:全局函数construct()和 destroy()

<stl_construct.h>部分代码

#include <new.h>  // 使用 placement new,需要先包含此文件。
// 将初值 __value 设定到指针所指的空间上。
template <class _T1, class _T2>
inline void _Construct(_T1* __p, const _T2& __value) {
  new ((void*) __p) _T1(__value);   // placement new,调用 _T1::_T1(__value);
}

template <class _T1>
inline void _Construct(_T1* __p) {
  new ((void*) __p) _T1();
}

// 第一个版本,接受一个指针,准备将该指针所指之物析构掉。
template <class _Tp>
inline void _Destroy(_Tp* __pointer) {
  __pointer->~_Tp();
}

// 第二版本,destroy 泛型特化
inline void _Destroy(char*, char*) {}
inline void _Destroy(int*, int*) {}
inline void _Destroy(long*, long*) {}
inline void _Destroy(float*, float*) {}
inline void _Destroy(double*, double*) {}
#ifdef __STL_HAS_WCHAR_T
inline void _Destroy(wchar_t*, wchar_t*) {}
#endif /* __STL_HAS_WCHAR_T */

上述 construct()接受一个指针p和一个初值 value,该函数的用途就是将初值设定到指针所指的空间上。C++的placement new运算子可用来完成这一任务。destroy()有两个版本,第一版本接受一个指针,准备将该指针所指之物析构掉。直接调用该对象的析构函数即可。第二版本接受 first和last两个迭代器,准备将[first,last)范围内的所有对象析构掉。


3.空间的配置与释放

<stl_alloc.h>

在SGI版本的STL中,空间的配置释放都由< stl_alloc.h > 负责。它的设计思想如下:向system heap要求空间,考虑多线程,考虑内存不足的应变措施,考虑内存碎片的问题.

考虑到小型区块所可能造成的内存破碎问题,SGI设计了双层级配置器,第一级配置器直接使用 malloc()和free),第二级配置器则视情况采用不同的策略:当配置区块超过128 bytes时,视之为“足够大”,便调用第一级配置器;当配置区块小于128 bytes时,视之为“过小”,为了降低额外负担,采用复杂的 memory pool整理方式,而不再求助于第一级配置器。

3.1第一级配置器

直接调用malloc和free来配置释放内存

// SGI STL 第一级配置器
// 无 “template 类型参数”,“非类型参数 __inst”,完全没有用
template <int __inst>
class __malloc_alloc_template {
private:
  
  // 以下函数将用来处理内存不足的情况
  static void* _S_oom_malloc(size_t);
  static void* _S_oom_realloc(void*, size_t);
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
  static void (* __malloc_alloc_oom_handler)();
#endif
public:
  // 第一级配置器直接调用 malloc()
  static void* allocate(size_t __n)
  {
    void* __result = malloc(__n);
    // 以下无法满足需求时,改用 _S_oom_malloc()
    if (0 == __result) __result = _S_oom_malloc(__n);
    return __result;
  }
  // 第一级配置器直接调用 free()
  static void deallocate(void* __p, size_t /* __n */)
  {
    free(__p);
  }
  
  // 第一级配置器直接调用 realloc()
  static void* reallocate(void* __p, size_t /* old_sz */, size_t __new_sz)
  {
    void* __result = realloc(__p, __new_sz);
    // 以下无法满足需求时,改用 _S_oom_realloc()
    if (0 == __result) __result = _S_oom_realloc(__p, __new_sz);
    return __result;
  }
  // 以下仿真 C++ 的 set_new_handler(),可以通过它指定自己的 out-of-memory handler
  // 为什么不使用 C++ new-handler 机制,因为第一级配置器并没有 ::operator new 来配置内存
  static void (* __set_malloc_handler(void (*__f)()))()
  {
    void (* __old)() = __malloc_alloc_oom_handler;
    __malloc_alloc_oom_handler = __f;
    return(__old);
  }
};

----------------------------------
template <int __inst>
void*
__malloc_alloc_template<__inst>::_S_oom_malloc(size_t __n)
{
    void (* __my_malloc_handler)();
    void* __result;
    // 不断尝试释放、配置
    for (;;) {
        __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);
    }
}
template <int __inst>
void* __malloc_alloc_template<__inst>::_S_oom_realloc(void* __p, size_t __n)
{
    void (* __my_malloc_handler)();
    void* __result;
    //  给一个已经分配了地址的指针重新分配空间,参数 __p 为原有的空间地址,__n 是重新申请的地址长度
    for (;;) {
        // 当 "内存不足处理例程" 并未被客户设定,便调用 __THROW_BAD_ALLOC,丢出 bad_alloc  异常信息
        __my_malloc_handler = __malloc_alloc_oom_handler;
        if (0 == __my_malloc_handler) { __THROW_BAD_ALLOC; }
        (*__my_malloc_handler)();   // 调用处理例程,企图释放内存
        __result = realloc(__p, __n);  // 再次尝试配置内存,扩大内存大小
        if (__result) return(__result);
    }
}
// 直接将参数 __inst 指定为0
typedef __malloc_alloc_template<0> malloc_alloc;

请注意,SGI第一级配置器的 allocate()和 realloc()都是在调用malloc()和 realloc()不成功后,改调用oom_ malloc()和oom_realloc()后两者都有内循环,不断调用“内存不足处理例程”,期望在某次调用之后,获得足够的内存而圆满完成任务。但如果“内存不足处理例程”并未被客端设定,oom_ malloc()和oom_ realloc()便老实不客气地调用 THROWBADALLOC,丢出 bad alloc异常信息,或利用exit(1)硬生生中止程序。

3.2第二级配置器

视情况采用不同的策略:当配置区块超过128 bytes时,视之为“足够大”,便调用第一级配置器;当配置区块小于128 bytes时,视之为“过小”,为了降低额外负担,采用复杂的 memory pool整理方式,而不再求助于第一级配置器

allocate函数

  /* __n must be > 0      */
  // 申请大小为n的数据块,返回该数据块的起始地址
  static void* allocate(size_t __n)
  {
    void* __ret = 0;
    // 如果需求区块大于 128 bytes,就转调用第一级配置
    if (__n > (size_t) _MAX_BYTES) {
      __ret = malloc_alloc::allocate(__n);
    }
    else {
      // 根据申请空间的大小寻找相应的空闲链表(16个空闲链表中的一个)
      _Obj* __STL_VOLATILE* __my_free_list
          = _S_free_list + _S_freelist_index(__n);
      // Acquire the lock here with a constructor call.
      // This ensures that it is released in exit or during stack
      // unwinding.
#     ifndef _NOTHREADS
      /*REFERENCED*/
      _Lock __lock_instance;
#     endif
      _Obj* __RESTRICT __result = *__my_free_list;
      // 空闲链表没有可用数据块,就将区块大小先调整至 8 倍数边界,然后调用 _S_refill() 重新填充
      if (__result == 0)
        __ret = _S_refill(_S_round_up(__n));
      else {
        // 如果空闲链表中有空闲数据块,则取出一个,并把空闲链表的指针指向下一个数据块  
        *__my_free_list = __result -> _M_free_list_link;
        __ret = __result;
      }
    }
    return __ret;
  };

if用户需要的区块大于128bytes,则直接调用第一级空间配置器

else如果用户需要的区块小于等于128128bytes,则到自由链表free_list中去找{
if自由链表有,则直接去拿来用
else空闲链表没有可用数据块,就将区块大小先调整至 8 倍数边界,然后调用 _S_refill() 重新填充
}

deallocate函数

/* __p may not be 0 */
  // 空间释放函数 deallocate()
  static void deallocate(void* __p, size_t __n)
  {
    if (__n > (size_t) _MAX_BYTES)   
      malloc_alloc::deallocate(__p, __n);   // 大于 128 bytes,就调用第一级配置器的释放
    else {
      _Obj* __STL_VOLATILE*  __my_free_list
          = _S_free_list + _S_freelist_index(__n);   // 否则将空间回收到相应空闲链表(由释放块的大小决定)中  
      _Obj* __q = (_Obj*)__p;
      // acquire lock
#       ifndef _NOTHREADS
      /*REFERENCED*/
      _Lock __lock_instance;
#       endif /* _NOTHREADS */
      __q -> _M_free_list_link = *__my_free_list;   // 调整空闲链表,回收数据块
      *__my_free_list = __q;
      // lock is released here
    }
  }
  static void* reallocate(void* __p, size_t __old_sz, size_t __new_sz);
} ;

逻辑:   

if区块大于128, 则直接由第一级空间配置器收回   

else区块小于等于128, 则找到对应的自由链表,将区块收回
二级配置器和一级配置器一样都提供Allocate和Deallocate的接口

4.自由链表

自由链表是一个指针数组,有点类似hash桶,它的数组大小为16,每个数组元素代表所挂的区块大小,比如free _ list[0]代表下面挂的是8bytes的区块,free _ list[1]代表下面挂的是16bytes的区块…….依次类推,直到free _ list[15]代表下面挂的是128bytes的区块
内存池,以start _ free和 end _ free记录其大小,用于保存未被挂在自由链表的区块,它和自由链表构成了伙伴系统。自由链表对应的位置没有所需的内存块该怎么办,也就是Refill函数的实现。

//freelist没有可用区块,将要填充,此时新的空间取自内存池
static void* Refill(size_t n)
{
    size_t nobjs = 20;
    char* chunk = (char*)ChunkAlloc(n, nobjs); //默认获得20的新节点,但是也可能小于20,可能会改变nobjs
    if (nobjs == 1) // 如果只获得一个数据块,那么这个数据块就直接分给调用者,空闲链表中不会增加新节点
        return chunk;
    //有多块,返回一块给调用者,其他挂在自由链表中
    Obj* ret = (Obj*)chunk;
    Obj* cur = (Obj*)(chunk + n);
    Obj* next = cur;
    Obj* volatile *myFreeList = FreeList + FreeListIndex(n);
    *myFreeList = cur;
    for (size_t i = 1; i < nobjs; ++i)
    {       
        next = (Obj*)((char*)cur + n);
        cur->freeListLink = next;
        cur = next;
    }
    cur->freeListLink = NULL;
    return ret;


}

从内存池中取空间给freelist使用,是_S_chunk_alloc()的工作

// 从内存池中取空间
template <bool __threads, int __inst>
char*
__default_alloc_template<__threads, __inst>::_S_chunk_alloc(size_t __size,
                                                            int& __nobjs)
{
    char* __result;
    size_t __total_bytes = __size * __nobjs;  // 需要申请空间的大小
    size_t __bytes_left = _S_end_free - _S_start_free;  // 计算内存池剩余空间
    if (__bytes_left >= __total_bytes) {  // 内存池剩余空间完全满足申请
        __result = _S_start_free;
        _S_start_free += __total_bytes;
        return(__result);
    } else if (__bytes_left >= __size) {  // 内存池剩余空间不能满足申请,提供一个以上的区块
        __nobjs = (int)(__bytes_left/__size);
        __total_bytes = __size * __nobjs;
        __result = _S_start_free;
        _S_start_free += __total_bytes;
        return(__result);
    } else {                             // 内存池剩余空间连一个区块的大小都无法提供                      
        size_t __bytes_to_get =
          2 * __total_bytes + _S_round_up(_S_heap_size >> 4);
        // Try to make use of the left-over piece.
        // 内存池的剩余空间分给合适的空闲链表
        if (__bytes_left > 0) {
            _Obj* __STL_VOLATILE* __my_free_list =
                        _S_free_list + _S_freelist_index(__bytes_left);
            ((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;
            *__my_free_list = (_Obj*)_S_start_free;
        }
        _S_start_free = (char*)malloc(__bytes_to_get);  // 配置 heap 空间,用来补充内存池
        if (0 == _S_start_free) {  // heap 空间不足,malloc() 失败
            size_t __i;
            _Obj* __STL_VOLATILE* __my_free_list;
            _Obj* __p;
            // Try to make do with what we have.  That can't
            // hurt.  We do not try smaller requests, since that tends
            // to result in disaster on multi-process machines.
            for (__i = __size;
                 __i <= (size_t) _MAX_BYTES;
                 __i += (size_t) _ALIGN) {
                __my_free_list = _S_free_list + _S_freelist_index(__i);
                __p = *__my_free_list;
                if (0 != __p) {
                    *__my_free_list = __p -> _M_free_list_link;
                    _S_start_free = (char*)__p;
                    _S_end_free = _S_start_free + __i;
                    return(_S_chunk_alloc(__size, __nobjs));
                    // Any leftover piece will eventually make it to the
                    // right free list.
                }
            }
            _S_end_free = 0;   // In case of exception.
            _S_start_free = (char*)malloc_alloc::allocate(__bytes_to_get);  // 调用第一级配置器
            // This should either throw an
            // exception or remedy the situation.  Thus we assume it
            // succeeded.
        }
        _S_heap_size += __bytes_to_get;
        _S_end_free = _S_start_free + __bytes_to_get;
        return(_S_chunk_alloc(__size, __nobjs));  // 递归调用自己
    }
}

上述的 chunk_alloc()函数以end_free- start free来判断内存池的水量。如果水量充足,就直接调出20个区块返回给 free list如果水量不足以提供20个区块,但还足够供应一个以上的区块,就拨出这不足20个区块的空间出去。这时候其 pass by reference的 nobjs参数将被修改为实际能够供应的区块数。如果内存池连一个区块空间都无法供应,对客端显然无法交待,此时便需利用malloc()从heap中配置内存,为内存池注入活水源头以应付需求。新水量的大小为需求量的两倍,再加上一个随着配置次数增加而愈来愈大的附加量.以上便是整个第二级空间配置器的设计。

5.总结

我们知道,引入相对复杂的空间配置器,主要源自两点:
1、频繁使用malloc、free开辟释放小块内存带来的性能效率的低下
2、内存碎片问题,导致不连续内存不可用的浪费


引入两层配置器帮我们解决了以上的问题,但是也带来一些问题:
1、内存碎片的问题,自由链表所挂区块都是8的整数倍,因此当我们需要非8倍数的区块,往往会导致浪费。
2、我们并没有释放内存池中的区块。释放需要到程序结束之后。这样子会导致自由链表一直占用内存,其它进程使用不了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值