126.STL 之 空间配置器(allocator)

126.STL 之 空间配置器(allocator)

1.SGI 标准的空间配置器,std::allocator

SGI也定义了一个符合部分标准,名为allocator的配置器,但是它自己不使用,也不建议我们使用,主要原因是效率不佳。

它只是把C++的操作符::operator new和::operator delete做了一层简单的封装而已。

2.SGI 特殊的空间配置器,std::alloc

由于SGI 标准的空间配置器只是把C++的操作符::operator new和::operator delete做了一层简单的封装,没有考虑到任何效率上的强化。

所以 SGI 出现了一个特殊的空间配置器,供 STL标准模板库使用。

通常,C++中用new操作符来分配内存都包括两个阶段:

(1)调用::operator new配置内存

(2)调用构造函数来构造对象内容

同理,delete操作也包括两个阶段:

(1)调用析构函数将对象析构

(2)调用::operator delete释放内存

为了精密分工,SGI STL allocator将两个阶段分开

内存配置操作由alloc:allocate负责,内存释放由alloc:deallocate负责;对象构造操作由::contructor()负责,对象析构由::destroy()负责。

配置器定义在头文件<memory>中,它里面又包括两个文件:

#include <stl_alloc.h>        // 负责内存空间的配置和器释放  
#include <stl_construct.h>        // 负责对象的构造和析构  

3.STL 空间配置器:

3.1STL 空间配置器解决的问题

1.内存碎片问题(外碎片)

内碎片:操作系统在分配给申请者未被利用的部分。产生原因:内存对齐(效率高);

外碎片:内存充足时但分配不出大块内存。产生原因:由于多次分配小块内存,将大块内存分割成小块;

2.频繁系统分配,释放小块内存,调用 malloc,delete 系统调用产生性能问题;

STL 空间配置器的实现机制:

3.2双层级的配置器

注:

当需要开辟的空间大于 128 bytes 时,视为“足够大”,调用一级配置器,直接调用 malloc,free;

当需要开辟的空间小于 128 bytes 时,视为“过小”,为了降低额外负担,调用二级空间配置器

A.一级空间配置器

//以下是第第一级配置器
template <int inst>
class __malloc_alloc_template 
{

private:
    //以下函数用来处理内存不足的情况
    static void* oom_malloc(size_t);
    static void* oom_realloc(void*, size_t);
    static void (*__malloc_alloc_oom_handler)();

public:

    static void* allocate(size_t n)
    {
        void* result = malloc(n);                    //第一级配置器,直接使用malloc()
        //如果内存不足,则调用内存不足处理函数oom_alloc()来申请内存
        if (0 == result) result = oom_malloc(n);
        return result;
    }

    static void deallocate(void* p, size_t /* n */)
    {
        free(p);            //第一级配置器直接使用 free()
    }

    static void* reallocate(void* p, size_t /* old_sz */, size_t new_sz)
    {
        void* result = realloc(p, new_sz);            //第一级配置器直接使用realloc()
        //当内存不足时,则调用内存不足处理函数oom_realloc()来申请内存
        if (0 == result) result = oom_realloc(p, new_sz);
        return result;
    }

    //设置自定义的out-of-memory handle就像set_new_handle()函数
    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>::__malloc_alloc_oom_handler)() = 0;  //内存处理函数指针为空,等待客户端赋值

template <int inst>
void* __malloc_alloc_template<inst>::oom_malloc(size_t n)
{
    void (*my_malloc_handler)();
    void* result;

    for (;;) 
    {                                                     //死循环
        my_malloc_handler = __malloc_alloc_oom_handler;            //设定自己的oom(out of memory)处理函数
        if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; }         //如果没有设定自己的oom处理函数,毫不客气的抛出异常
        (*my_malloc_handler)();                                    //设定了就调用oom处理函数
        result = malloc(n);                                        //再次尝试申请
        if (result) return(result);
    }
}

template <int inst>
void* __malloc_alloc_template<inst>::oom_realloc(void* p, 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; }    //如果自己没有定义oom处理函数,则编译器毫不客气的抛出异常
        (*my_malloc_handler)();                                //执行自定义的oom处理函数
        result = realloc(p, n);                                //重新分配空间
        if (result) return(result);                            //如果分配到了,返回指向内存的指针
    }
}

上面代码看似繁杂,其实流程是这样的:

1.我们通过allocate()申请内存,通过deallocate()来释放内存,通过reallocate()重新分配内存。

2.当allocate()或reallocate()分配内存不足时会调用oom_malloc()或oom_remalloc()来处理。

3.当oom_malloc() 或 oom_remalloc()还是没能分配到申请的内存时,会转如下两步中的一步:

(a)调用用户自定义的内存分配不足处理函数(这个函数通过set_malloc_handler() 来设定),然后继续申请内存!

(b)如果用户未定义内存分配不足处理函数,程序就会抛出bad_alloc异常或利用exit(1)终止程序。

看完这个流程,再看看上面的代码就会容易理解多了!

B.二级配置器 _ _default_alloc_template

第二级配置器的代码很多,这里我们只贴出其中的 allocate() 和 dellocate()函数的实现和工作流程(参考侯捷先生的《STL源码剖析》),而在看函数实现代码之前,我大致的描述一下第二层配置器配置内存的机制。

我们之前说过,当申请的内存大于 128 bytes时就调用第一层配置器。当申请的内存小于 128bytes时才会调用第二层配置器。第二层配置器如何维护128bytes一下内存的配置呢? SGI 第二层配置器定义了一个 free-lists,这个free-list是一个数组,如下图:

这数组的元素都是指针,用来指向16个链表的表头。这16个链表上面挂的都是可以用的内存块。只是不同链表中元素的内存块大小不一样,16个链表上分别挂着大小为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128 bytes的小额区块,图如下:

就是这样,现在我们来看allocate()代码:

static void* allocate(size_t n)
{
    obj* __VOLATILE* my_free_list;
    obj* __RESTRICT result;

    //要申请的空间大于128bytes就调用第一级配置
    if (n > (size_t)__MAX_BYTES) 
    {
        return(malloc_alloc::allocate(n));
    }
    //寻找 16 个free lists中恰当的一个
    my_free_list = free_list + FREELIST_INDEX(n);
    result = *my_free_list;
    if (result == 0)
    {
        //没找到可用的free list,准备新填充free list
        void* r = refill(ROUND_UP(n));
        return r;
    }
    *my_free_list = result->free_list_link;
    return (result);
};

其中有两个函数我来提一下,一个是ROUND_UP(),这个是将要申请的内存字节数上调为8的倍数。因为我们free-lists中挂的内存块大小都是8的倍数嘛,这样才知道应该去找哪一个链表。另一个就是refill()。这个是在没找到可用的free list的时候调用,准备填充free lists。意思是:参考上图,假设我现在要申请大小为 56bytes 的内存空间,那么就会到free lists 的第 7 个元素所指的链表上去找。如果此时 #6元素所指的链表为空怎么办?这个时候就要调用refill()函数向内存池申请N(一般为20个)个大小为56bytes的内存区块,然后挂到 #6 所指的链表上。这样,申请者就可以得到内存块了。当然,这里为了避免复杂,误导读者我就不讨论refill()函数了。allocate()过程图如下:

学过链表的操作的人不难理解上图,我就不再讲解。下面看deallocate(),代码如下:

static void deallocate(void* p, size_t n)
{
    obj* q = (obj*)p;
    obj* __VOLATILE* my_free_list;

    //如果要释放的字节数大于128,则调第一级配置器
    if (n > (size_t)__MAX_BYTES) 
    {
        malloc_alloc::deallocate(p, n);
        return;
    }
    //寻找对应的位置
    my_free_list = free_list + FREELIST_INDEX(n);
    //以下两步将待释放的块加到链表上
    q->free_list_link = *my_free_list;
    *my_free_list = q;
}
static _Obj* __STL_VOLATILE _S_free_list[_NFREELISTS]; 

使用VOLATILE是为了防止线程缓存,导致多个线程看到的数据版本不一样

deallocate()函数释放内存的步骤如下图:

其实这就是一个链表的插入操作,也很简单。不再赘述!上面忘了给链表结点的结构体定义了,如下:

union obj
{
    union obj * free_list_link;
    char client_date[1]; 
};

总结:

空间配置器的相关定义

template <class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR(_Tp) >
class vector : protected _Vector_base<_Tp, _Alloc>

可以看到,容器的默认空间配置器是__STL_DEFAULT_ALLOCATOR( _Tp),它是一个宏定义,如下:

# ifndef __STL_DEFAULT_ALLOCATOR
# ifdef __STL_USE_STD_ALLOCATORS
# define __STL_DEFAULT_ALLOCATOR(T) allocator< T >
# else
# define __STL_DEFAULT_ALLOCATOR(T) alloc
# endif
# endif

从上面可以看到__STL_DEFAULT_ALLOCATOR通过宏控制有两种实现,一种是allocator< T >,另一种是alloc,这两种分别就是SGI STL的一级空间配置器和二级空间配置器的实现。

template <int __inst>
class __malloc_alloc_template // 一级空间配置器内存管理类 -- 通过malloc和free管理内存
template <bool threads, int inst>
class __default_alloc_template { // 二级空间配置器内存管理类 -- 通过自定义内存池实现内存管理

重要类型和变量定义

// 内存池的粒度信息
enum {_ALIGN = 8};
enum {_MAX_BYTES = 128};
enum {_NFREELISTS = 16};
// 每一个内存chunk块的头信息
union _Obj {
union _Obj* _M_free_list_link;
char _M_client_data[1]; /* The client sees this. */
};
// 组织所有自由链表的数组,数组的每一个元素的类型是_Obj*,全部初始化为0
static _Obj* __STL_VOLATILE _S_free_list[_NFREELISTS];
// Chunk allocation state. 记录内存chunk块的分配情况
static char* _S_start_free;
static char* _S_end_free;
static size_t _S_heap_size;
template <bool __threads, int __inst>
char* __default_alloc_template<__threads, __inst>::_S_start_free = 0;
template <bool __threads, int __inst>
char* __default_alloc_template<__threads, __inst>::_S_end_free = 0;
template <bool __threads, int __inst>
size_t __default_alloc_template<__threads, __inst>::_S_heap_size = 0;

重要的辅助接口函数

/*将 __bytes 上调至最邻近的 8 的倍数*/
static size_t _S_round_up(size_t __bytes)
{ return (((__bytes) + (size_t) _ALIGN-1) & ~((size_t) _ALIGN - 1)); }
enum { _ALIGN = 8 };
/*将__bytes上调至最临近的8的倍数*/
static size_t _S_round_up(size_t __bytes)
{
	return (((__bytes)+(size_t)__ALIGN - 1) & ~((size_t)__ALIGN - 1));
}

/*
数字1-8 => 8
9-16 => 16

 x = 00000000 00000000 00000000 00000111    => (size_t)__ALIGN - 1
~x = 11111111 11111111 11111111 11111000

__bytes = 0;的话容易得出结果为0

__bytes = 1;
 x = 00000000 00000000 00000000 00001000
~x = 11111111 11111111 11111111 11111000       与后结果 x = 00000000 00000000 00000000 00001000      8

__bytes = 8;
  x = 00000000 00000000 00000000 00001111
 ~x = 11111111 11111111 11111111 11111000      与后结果 x = 00000000 00000000 00000000 00001000      8

 __bytes = 9;
  x = 00000000 00000000 00000000 00010000
 ~x = 11111111 11111111 11111111 11101111      与后结果 x = 00000000 00000000 00000000 00010000      16
*/
/*返回 __bytes 大小的chunk块位于 free-list 中的编号*/
static size_t _S_freelist_index(size_t __bytes) 
{
	return (((__bytes)+(size_t)_ALIGN - 1) / (size_t)_ALIGN - 1);
}
#     ifndef _NOTHREADS
/*REFERENCED*/
_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;
}

面向对象加锁解锁方式,栈上对象出作用域析构,构造函数加锁、析构函数解锁,以上称为临界区代码对,SGI二级空间配置器内存池的管理完全支持线程安全。

内存池管理函数

// 分配内存的入口函数
static void* allocate(size_t __n)
// 负责把分配好的chunk块进行连接,添加到自由链表当中
static void* _S_refill(size_t __n);
// 分配相应内存字节大小的chunk块,并且给下面三个成员变量初始化
static char* _S_chunk_alloc(size_t __size, int& __nobjs);
// 把chunk块归还到内存池
static void deallocate(void* __p, size_t __n);
// 内存池扩容函数
template <bool threads, int inst>
void*
__default_alloc_template<threads, inst>::reallocate(void* __p,size_t __old_sz,size_t __new_sz);

allocate
/* __n must be > 0      */
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);
        // 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;
        if (__result == 0)
            __ret = _S_refill(_S_round_up(__n));
        else
        {
            *__my_free_list = __result->_M_free_list_link;
            __ret = __result;
        }
    }

    return __ret;
};
 __my_free_list是二级指针,__n是要分配的字节数,_S_free_list作用是把分配的内存的大小映射到数组具体位置(1-8都在#0,9-16都在#1...)
 假如想分配15个字节大小,_S_freelist_index(__n)返回的是1,
class _Lock
{
public:
    _Lock() { __NODE_ALLOCATOR_LOCK; }
    ~_Lock() { __NODE_ALLOCATOR_UNLOCK; }
};

构造函数加锁,析构函数解锁,利用栈上对象出作用域析构的特点,出了LOCK所在大括号,LOCK对象进行析构,下面代码段是临界区代码段,通过锁控制,保证其线程安全问题

        _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;
        }
 _Obj* __RESTRICT __result = *__my_free_list;//开始为0
if (__result == 0)
            __ret = _S_refill(_S_round_up(__n));

为空的话,开始在数组下面分配16字节大小的chunk块,_M_free_list_link存储了下一个chunk块地址

流程:

1.首先定义一个二级指针用来遍历指针数组,根据所需分配的内存大小定位到相应的chunk块

2.通过指针解引用赋值给result( _Obj* __RESTRICT __result = *__my_free_list;//开始为0),如果为空,说明相应大小的chunk块从来没有被分配过,也就是该chunk块没有任何内存池

如果没有就分配内存池,让ret指向新分配内存池的首节点(第一个chunk块,__ret = _S_refill(_S_round_up(__n));),返回ret

如果说相应位置内存池不空,那么

__result = *__my_free_list;
*__my_free_list = __result->_M_free_list_link;//__result->_M_free_list_link相当于next
ret = __result;
return(__result);
_S_refill
/* Returns an object of size __n, and optionally adds to size __n free list.*/
/* We assume that __n is properly aligned.                                */
/* We hold the allocation lock.                                         */
template <bool __threads, int __inst>
void*
__default_alloc_template<__threads, __inst>::_S_refill(size_t __n)
{
    int __nobjs = 20;
    char* __chunk = _S_chunk_alloc(__n, __nobjs);
    _Obj* __STL_VOLATILE* __my_free_list;
    _Obj* __result;         
    _Obj* __current_obj;
    _Obj* __next_obj;
    int __i;

    if (1 == __nobjs) return(__chunk);
    __my_free_list = _S_free_list + _S_freelist_index(__n);

    /* Build free list in chunk */
    __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);
}

指针数组中_Obj 类型数据(指针)还是空的时候,下面没有没有下挂任何内存池,进入下面_S_refill

if (__result == 0)
__ret = _S_refill(_S_round_up(__n));//_S_round_up(__n)将__bytes上调至最临近的8的倍数
else
{
    *__my_free_list = __result->_M_free_list_link;//相当于指针数组中保存了指针,该指针指向下一个chunk块,因为当前节点要分配出去
    __ret = __result;
}

int __nobjs = 20;
char* __chunk = _S_chunk_alloc(__n, __nobjs);// _S_chunk_alloc内存开辟,返回开辟空间的地址      按照自由链表的相应元素位的字节数开辟相应的chunk块

每一块内存8个字节(chunk块),第一维数组是指针类型数组,需要用二级指针遍历
    
1 == __nobjs//__nobjs会变是因为_S_chunk_alloc(size_t __size, int& __nobjs)接受到的是__nobjs的引用
1 == __nobjs表示8个字节的chunk块只剩一个节点
    
__my_free_list = _S_free_list + _S_freelist_index(__n);//把所申请的字节数映射到自由链表的下标出
_S_free_list数组起始地址,相当于起始地址加0,8字节在零号位元素下挂的自由链表

__result = (_Obj*)__chunk;//__chunk刚是char*类型,__result是_Obj*,__result和__chunk都指向chunk块的起始地址

*__my_free_list = __next_obj = (_Obj*)(__chunk + __n);//__chunk刚是char*类型,__chunk + __n是加8个字节,现在__chunk + __n指向下一个字节,
//也就是__next_obj指向下一个字节,*__my_free_list就是第0号位置的存储的值,现在把chunk块下一个位置地址存在*__my_free_list

for (__i = 1; ; __i++) //i初始值是1表示剩最后一个节点     每处理一个节点i++,
{
    __current_obj = __next_obj;
    __next_obj = (_Obj*)((char*)__next_obj + __n);//__next_obj转成char*后指针加1加一个字节,(char*)__next_obj + __n指针遍历8个字节,__next_obj指向第4个节点(#3)
    if (__nobjs - 1 == __i)//__nobjs表示到底创建了多少个chunk块   相当于obj已经指向最后一个节点,因为创建的chunk块都要回收,
    //每一个节点的头都安装了一个类型union
    {
        __current_obj->_M_free_list_link = 0;
        break;
    }
    else
    {
        __current_obj->_M_free_list_link = __next_obj;
    }
}
union _Obj
{
    union _Obj* _M_free_list_link;//指针域:记录下一个节点地址__next_obj
    char _M_client_data[1];    /* The client sees this.        */
};
_S_chunk_alloc
/* We allocate memory in large chunks in order to avoid fragmenting     */
/* the malloc heap too much.                                            */
/* We assume that size is properly aligned.                             */
/* We hold the allocation lock.                                         */
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);
        if (0 == _S_start_free) 
        {
            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));
    }
}

1.__bytes_left = _S_end_free - _S_start_free;因为_S_end_free_S_start_free为0,所以__bytes_left为0

2.执行else

__bytes_to_get = 2 * __total_bytes + _S_round_up(_S_heap_size >> 4);其中_S_heap_size = 0所以_S_round_up(_S_heap_size >> 4) = 0

3.__size是8字节的话,__nobjs本来是20,__bytes_to_get就是320字节

4._S_start_free = (char*)malloc(__bytes_to_get);这里分配了40个chunk块

5. _S_heap_size += __bytes_to_get;这里_S_heap_size = 320(40个chunk块)

_S_end_free = _S_start_free + __bytes_to_get;这之后_S_end_free到末尾了

第二轮

1.__total_bytes = __size * __nobjs__total_bytes = 160__bytes_left = _S_end_free - _S_start_free;这里 __bytes_left = 320,执行下面的代码

if (__bytes_left >= __total_bytes)
{
    __result = _S_start_free;
    _S_start_free += __total_bytes;
    return(__result);
}

2.__result = _S_start_free;__result指向第一个即将被分配的chunk块,__total_bytes = 160,是20个chunk块大小,_S_start_free = 160,返回__result



第一轮

size_t __total_bytes = __size * __nobjs;
size_t __bytes_left = _S_end_free - _S_start_free;
  • __size = 8__nobjs = 20; => __total_bytes = 160

  • _S_end_free_S_start_free都为0,__bytes_left为0;

  • __bytes_left >= __total_bytes不成立,__bytes_left >= __size不成立,执行else

  • size_t __bytes_to_get = 2 * __total_bytes + _S_round_up(_S_heap_size >> 4); __bytes_to_get = 320,40个chunk块

  • __my_free_list = _S_free_list + _S_freelist_index(__bytes_left);这段代码的作用是计算当前内存块大小所对应的自由链表的头部地址。具体来说,它会根据内存块大小计算出对应的自由链表索引 _S_freelist_index(__bytes_left),然后将 _S_free_list 数组中对应索引的元素地址加上偏移量,得到当前内存块大小所对应的自由链表的头部地址。最终,将该地址赋值给指针变量 __my_free_list,以便后续使用。

  • ((_Obj*)_S_start_free) -> _M_free_list_link = *__my_free_list;

这段代码的功能是将自由链表中的空闲内存块分配给当前对象。首先,根据之前计算得到的自由链表头部地址 __my_free_list,通过 *__my_free_list 取得该自由链表的第一个空闲内存块的地址。然后,将该地址赋值给 _Obj 类型的指针 _M_free_list_link,使当前对象的 _M_free_list_link 成员指向该空闲内存块。这样,当前对象就可以使用该空闲内存块了。

  • _S_start_free = (char*)malloc(__bytes_to_get);

这段代码的作用是通过调用 malloc 函数来申请一段内存空间,并将其起始地址赋值给 _S_start_free 变量。具体来说,它会根据参数 __bytes_to_get 指定的字节数来申请相应大小的内存空间,并将其转换成 char* 类型的指针,最后将该指针赋值给 _S_start_free 变量,以便后续使用。

  • 下面是空间分配失败执行的操作
if (0 == _S_start_free) 
{
    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_start_free 为 0 时,尝试从自由链表中寻找可用的内存块,如果找到了则分配给内存申请者,否则通过 malloc_alloc::allocate 函数申请一段内存空间,并将其起始地址赋值给 _S_start_free 变量。具体来说,它会首先检查 _S_start_free 是否为 0,如果是则表示没有可用的内存空间,需要从自由链表中寻找可用的空闲块。它会从当前申请的内存大小 __size 开始,逐步增加大小,直到找到一个大小合适的自由链表。然后,它会检查该自由链表中是否有可用的空闲块,如果有,则将其分配给内存申请者,并返回分配的空闲块地址。如果自由链表中没有可用的空闲块,则会通过 malloc_alloc::allocate 函数申请一段大小为 __bytes_to_get 的内存空间,并将其起始地址赋值给 _S_start_free 变量。如果申请失败,则会抛出异常。

  • _S_heap_size += __bytes_to_get;//现在40个chunk块

_S_heap_size 是一个变量,用于跟踪内存池的总大小。它记录了已经分配的内存空间的累计大小。在这段代码中,通过将当前申请的内存空间大小 __bytes_to_get 累加到 _S_heap_size 变量上,可以更新内存池的总大小。

  • _S_end_free = _S_start_free + __bytes_to_get;//第40个chunk块(最后一个chunk块位置)

这段代码的作用是将 _S_end_free 变量设置为 _S_start_free 加上 __bytes_to_get,即将 _S_end_free 设置为当前申请的内存空间的末尾地址。具体来说,它通过将 _S_start_free 变量加上 __bytes_to_get 的大小,计算出当前申请的内存空间的末尾地址,然后将其赋值给 _S_end_free 变量,以便后续使用。

  • 下面代码作用:return(_S_chunk_alloc(__size, __nobjs));

这段代码的作用是调用 _S_chunk_alloc 函数来分配一段大小为 __size 的内存空间,并将其分成 __nobjs 块。然后将这些内存块中的第一块返回给内存申请者,其余的内存块添加到自由链表中。具体来说,它会调用 _S_chunk_alloc 函数来申请一段大小为 __size * __nobjs 的内存空间,并将其分成 __nobjs 块。然后,它会将其中的第一块返回给内存申请者,其余的内存块添加到自由链表中。这样,下一次申请同样大小的内存时,就可以直接从自由链表中获取可用的内存块,而不需要再次调用 _S_chunk_alloc 函数来申请内存空间。

第二轮

size_t __total_bytes = __size * __nobjs;//160,20个chunk块
size_t __bytes_left = _S_end_free - _S_start_free;//有备用的20个chunk块

满足

if (__bytes_left >= __total_bytes)
{
    __result = _S_start_free;//指向备用的20个chunk块第一个
    _S_start_free += __total_bytes;//_S_start_free指向最后一个chunk块,此时_S_start_free = _S_end_free
    return(__result);
}

第三轮

size_t __total_bytes = __size * __nobjs;//160,20个chunk块
size_t __bytes_left = _S_end_free - _S_start_free;//0

执行else

size_t __bytes_to_get =  2 * __total_bytes + _S_round_up(_S_heap_size >> 4);

_S_heap_size = 320, __bytes_to_get = 480(40 + 20 = 60)

_S_start_free = (char*)malloc(__bytes_to_get);//_S_start_free=480(60)
_S_heap_size += __bytes_to_get;
_S_end_free = _S_start_free + __bytes_to_get;

本来申请了20个8字节的chunk块,现在又要申请16字节的

deallocate
/* __p may not be 0 */
static void deallocate(void* __p, size_t __n)
{
    if (__n > (size_t)_MAX_BYTES)
        malloc_alloc::deallocate(__p, __n);
    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
    }
}

1.__n是要归还的字节数,__p是要归还的起始地址,要归还的字节数大于128字节,是malloc分配的,通过free释放。要归还的字节数小于128字节,是从内存池分配出去的,如果以前分配的是8字节的chunk块,要归还到8字节的chunk块中

2.要先定位到静态链表里

_Obj* __q = (_Obj*)__p;__q指向要归还的chunk块

__my_free_list = _S_free_list + _S_freelist_index(__n);

如果说前两个chunk块已经分配出去了,__my_free_list,Obj指针指向数组的第3个chunk块的地址

3.*__my_free_list保存第3个chunk块起始地址

__q->_M_free_list_link = *__my_free_list;是将第一个节点保存第3个chunk块的地址

*__my_free_list = __q;Obj指针指向指向第一个节点

4.标准库allocator类及其算法

allocator<T> a 定义了一个名为a的allocator对象,它可以为类型为T的对象分配内存
a.allocate(n) 分配一段原始的、未构造的内存,保存n个类型为T的对象
a.deallocate(p, n) 释放从T*指针p中地址开始的内存,这块内存保存了n个类型为T的对象,调用deallocate前,用户必须对每个在这块内存中创建的对象调用destroy
a.construct(p, args) args被传递给类型为T的构造函数,用来在p指向的内存中构造一个对象
a.destory(p) p为T*类型的指针,此算法对p指向的对象执行析构函数
#include <iostream>
#include <memory>

using namespace std;

class Example
{
public:
    Example() 
    {
        cout << "Example default constructor..." << endl;
    }
    Example(int x) : a(x) 
    {
        cout << "Example constructor..." << endl;
    }
    ~Example() 
    {
        cout << "Example destructor..." << endl;
    }
    int a = 0;
};

int main() 
{
    allocator<Example> alloc;

    // 使用allocate函数分配一块能够存放5个Example对象的内存空间
    Example* p = alloc.allocate(5);

    // 使用construct函数在第一个位置构造一个Example对象
    alloc.construct(p, 1);

    // 使用construct函数在第二个位置构造一个Example对象
    alloc.construct(p + 1, 2);

    // 使用destroy函数销毁第一个位置的Example对象
    alloc.destroy(p);

    // 使用deallocate函数释放分配的内存空间
    alloc.deallocate(p, 5);

    return 0;
}

输出:

Example constructor...
Example constructor...
Example destructor...

在这个例子中,我们首先创建了一个名为alloc的allocator对象,用于分配Example对象所需的内存。

然后,我们使用alloc.allocate(5)函数分配了一块能够存放5个Example对象的内存空间,并将返回的指针赋值给指针变量p。

接下来,我们使用alloc.construct()函数在刚刚分配的内存空间中构造了两个Example对象。第一个对象使用带参数的构造函数进行构造,并将参数值设置为1;第二个对象也使用带参数的构造函数进行构造,并将参数值设置为2。

然后,我们使用alloc.destroy()函数销毁了第一个位置的Example对象。最后,我们使用alloc.deallocate()函数释放了分配的内存空间。

需要注意的是,在使用allocator类进行内存分配和对象构造时,必须使用alloc.construct()函数来显式地调用构造函数进行对象的构造,并使用alloc.destroy()函数来显式地调用析构函数进行对象的销毁。

参考:STL 之 空间配置器(allocator)

浅析STL allocator

posted @ 2023-09-07 22:10  CodeMagicianT  阅读(37)  评论(0编辑  收藏  举报