STL 源码剖析重要部分:allocator 源码

SGI 特殊的空间配置器,std::alloc,destroy 与 construct

​ 它将 new 的操作分成了两个阶段:

  • 内存配置操作由 alloc::allocate() 负责,内存释放由 alloc::deallocate() 负责。(注意:这两个函数是类的静态成员)
  • 对象构造由 ::construct() 负责,对象析构由 ::destroy() 负责。(注意:这两个函数是全局作用域的函数)
#include <new.h>        // 欲使用 placement new,需先包含此头文件

template <class T1,class T2>
inline void construct(T1* p,const T2 &value) {
    new (p) T1(value);          // placement new;调用 T1::T1(value)
}

// 以下是 destroy() 第一个版本,接受一个指针
template <class T>
inline void destroy(T *pointer) {
    pointer -> ~T();        // 调用 destructor ~T()
}

// 以下是 destroy() 第二版版本,接受两个迭代器。此函数设法找出元素的数值型别
// 进而利用 __type_traits<> 求取适当措施
template <class ForwardIterator>
inline void destroy(ForwardIterator first, ForwardIterator last) {
    __destroy(first,last,value_type(first));        // 通过 value_type 获得元素型别
}

// 判断元素型别 (value_type) 是否有 trivial destructor
template <class ForwardIterator, class T>
inline void __destroy(ForwardIterator first,ForwardIterator last,T*) {
    typedef typename __type_trait<T>::has_trivial_destructor trivial_destructor;
    __destroy_aux(first,last,trivial_destructor());
}

// 如果元素型别 (value_type) 有 non-trivial destructor ...
template <class ForwardIterator>
inline void __destroy_aux(ForwardIterator first,ForwardIterator last,__false_type) {
    for( ;first < last;++ first) {
        destroy(&*first);       // 迭代器是迭代器,指针是指针,这两个是不同的,所以也就需要解引用,然后取地址
    }
}

// 如果元素型别 (value_type) 有 trivial destructor ...
template <class ForwardIterator>
inline void __destroy_aux(ForwardIterator first,ForwardIterator last,__true_type) { }

// 一下是针对 destroy() 第二版本的 char* 和 wchar_t* 的特化版
inline void destroy(char*, char*) { }
inline void destroy(wchar_t*, wchar_t *) { }

non-trivial destructor 与 trivial destructor:

  1. 如果用户不定义析构函数,而是用系统自带的,则说明,析构函数基本没有什么用(但默认会被调用)我们称之为trivial destructor。反之,如果特定定义了析构函数,则说明需要在释放空间之前做一些事情,则这个析构函数称为non-trivial destructor。如果某个类中只有基本类型的话是没有必要调用析构函数的,delelte p的时候基本不会产生析构代码,
  2. 在C++的类中如果只有基本的数据类型,也就不需要写显式的析构函数,即用默认析构函数就够用了,但是如果类中有个指向其他类的指针,并且在构造时候分配了新的空间,则在析构函数中必须显式释放这块空间,否则会产生内存泄露,
  3. 在STL中空间配置时候destory()函数会判断要释放的迭代器的指向的对象有没有 trivial destructor(STL中有一个 has_trivial_destructor函数,很容易实现检测),如果有trivial destructor则什么都不做,如果没有即需要执行一些操作,则执行真正的destory函数。
空间的释放与配置,std::alloc

​ 转向对象构造前的空间配置和对象析构后的空间释放。SGI 对其的设计如下:

  • 向 system heap 要求空间
  • 考虑多线程状态
  • 考虑内存不足时的应变措施
  • 考虑过多的 “小型区块” 可能造成的内存碎片问题。

!!ps:这里的讨论排出多线程状态的处理。

SGI 设计了双层级配置器

  1. 第一级配置器直接使用 malloc() 和 free()
  2. 第二级配置器则根据情况采取不同的策略:若配置区块超过 128 bytes,便调用第一级配置器;否则便采用复杂度 memory pool 整理方式。

__USE_MALLOC 默认是未定义的

# ifdef __USE_MALLOC
...
typedef __malloc_alloc_template<0> malloc_alloc;
typedef malloc_alloc alloc;     // 令 alloc 为第一级配置器
# else
...
// 令 alloc 为第二级配置器
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc;
#endif /* ! __USE_MALLOC */

无论 alloc 被定义为第一级配置器还是第二级配置器,SGI 还为它再包装一个接口如下。这样可以很方便的按元素型别 T 进行配置:

template <class T,class Alloc>
class simple_alloc {
public:
    static T *allocate(size_t n)
                { return 0 == n ? 0 : (T*)Alloc::allocate(n * sizeof(T)); }
    static T *allocate(void)
                { return (T*)Alloc::allocate(sizeof(T)); }
    static void deallocate(T *p,size_t n)
                { if(0 != n) Alloc::deallocate(p,n * sizeof(T)); }
    static void deallocate(T *p)
                { Alloc::deallocate(p,sizeof(T)); }
};

如 vector,其部分代码如下

template <class T,class Alloc = alloc>
class vector {
protected:
    // 专属之空间配置器,每次配置一个元素大小
    typedef simple_alloc<value_type, Alloc> data_allocator;
    void deallocate() {
        if(...)
            data_allocator::deallocate(start, end_of_storage - start);
    }
};
第一级配置器 __malloc_alloc_template 剖析

值得注意:在 __malloc_alloc_template 中,所有的成员都是 static,静态的。所以我们只需要在类外定义好必要的静态成员变量。就可以直接通过类名去调用其中的静态成员成员函数,不需要生成特定对象。

#if 0
#   include <new>
#   define __THROW_BAD_ALLOC throw bad_alloc
#elif !defined(__THROW_BAD_ALLOC)
#   include <iostream.h>
#   define __THROW_BAD_ALLOC cerr << "out of memory" << endl; exit(1)
#endif

// malloc-based allocator. 通常比稍后介绍的 default alloc 速度慢
// 一般而言是 thread-safe,并且对于空间的运用比较高效 (efficient)
// 以下是第一级配置器
// 注意,无 “template 型别参数”。至于 “非型别参数” inst,则完全没派上用场
template <int inst>
class __malloc_alloc_template {
private:
    // 一下都是函数指针,所代表的函数将用来处理内存不足的情况
    // oom: out of memory
    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_malloc()
        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;
    }

    // 一下仿真 C++ 的 set_new_handler()。换句话说,你可以通过它
    // 指定自己的 out-of-memory handler
    static void (* set_malloc_handler(void (*f)())) () {
        void (* old)() = __malloc_alloc_oom_handler;
        __malloc_alloc_oom_handler = f;
        return (old);
    }
};

// malloc_alloc out-of-memory handling
// 初值为 0.有待客端设定  (!!!这是赋初值。同时注意,这个成员是static)
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;
        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>::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; }
        (*my_malloc_handler)();     // 调用处理历程,企图释放内存
        result = realloc(p, n);         // 再次尝试配置内存
        if(result) return result;
    }
}

// 注意,一下直接将参数 inst 指定为 0。。见 __USE_MALLOC 那一部分的代码
typedef __malloc_alloc_template<0> malloc_alloc;
第二级配置器 __default_alloc_template 剖析(从 2.2.6 ~ 2.2.10)

​ 这一节同样是在源码的基础上进行详细的讲解。

对于源码,需要注意的点:所有的成员都是 static 的。对于静态成员变量,我们在头文件中进行定义 (引入头文件就会定义)。而对于静态成员函数,我们只需要通过类名就可以直接访问到,无须定义具体对象。

其源码主要包括:

  • 当区块足够大时 (超过 128 bytes),直接交由第一级配置器处理。

  • 当区块小于 128 bytes 时,则以内存池 (memory pool) 管理。

    ​ 介绍了如何用内存池进行管理

    • 维护 16 个自由链表 (free_list),每一种链表代表一种内存大小(从 8 到 128,均是 8 的倍数)
    • 空间配置与空间释放。配置:对于区块大小,如果 free_list[i] 之内有可用区块,就直接分配。否则将区块大小调至 8 倍数的边界 (如 30 调至 32),然后调用 refill() 为需要的 free_list[i] 重新填充空间。当然,取的时候直接取 free_list[i] 的第一块。释放:直接回收到对应 free_list[i] (连上去)。
    • 重新填充 free_list[i]。在需要 free_list[i] 没有可用区块时,就调用 refill 重新填充,新的空间取自内存池。默认直接获得 20 个新节点给 free_list[i]。但万一内存池空间不足,获得的实际节点数量会小于 20。
    • 具体的内存池的操作。包括内存池空间足,不足,为 0 的情况。
enum { __ALIGN = 8 };       // 小型区块的上调边界
enum { __MAX_BYTES = 128};  // 小型区块的上限
enum { __NFREELISTS = __MAX_BYTES / __ALIGN};   // free-lists 个数

// 以下是第二级配置器
// 注意,无 “template 型别参数”,且第二参数完全没派上用场
// 第一参数用于多线程环境下。本书不讨论多线程环境
template <bool threads, int inst>
class __default_alloc_template {
private:
    // ROUND_UP() 将 bytes 上调至 8 的倍数
    static size_t ROUND_UP(size_t bytes) {
        return (((bytes) + __ALIGN - 1) & ~(__ALIGN - 1));
    }

private:
    union obj {
        union obj* free_list_link;	/* 即 next */
        char client_data[1];        /*The client sees this. */
    };

private:
    // 16 个 free-lists
    static obj * volatile free_list[__NFREELISTS];
    // 以下函数根据区块大小,决定使用第 n 号 free-list,n 从 1 起算
    static size_t FREELIST_INDEX(size_t bytes) {
        return (((bytes) + __ALIGN - 1) / __ALIGN - 1);
    }

    // 返回一个大小为 n 的对象,并可能加入大小为 n 的其它区块到 free list
    static void *refill(size_t n);
    // 配置一大块空间,可容纳 nobjs 个大小为 "size" 的区块
    // 如果配置 nobjs 个区块有所不便,nobjs 可能会降低
    static char *chunk_alloc(size_t size, int &nobjs);

    // Chunk allocation state
    static char *start_free;        // 内存池起始位置。只在 chunk_alloc() 中变化
    static char * end_free;        // 内存池结束位置。只在 chunk_alloc() 中变化
    static size_t heap_size;

public:
    static void *allocate(size_t n) { /* 详述于后 */}
    static void deallocate(void *p, size_t n) { /* 详述于后 */}
    static void *reallocate(void *p, size_t old_sz, size_t new_sz);
};

// 以下是 static data member 的定义与初始值设定
template <bool threads, int inst>
char *__default_alloc_template<threads, inst>::start_free = 0;

template <bool threads, int inst>
char *__default_alloc_template<threads, inst>::end_free = 0;

template <bool threads, int inst>
size_t __default_alloc_template<threads, inst>::heap_size = 0;

template <bool threads, int inst>
typename __default_alloc_template<threads, inst>::obj * volatile
__default_alloc_template<threads, inst>::free_list[__NFREELISTS] =
        {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
空间配置函数 allocate()

此函数首先判断区块大小,大于 128 bytes 就调用第一级配置器,小于 128 bytes 就检查对应的 free list。如果 free list 之内有可用区块,就直接拿来用,如果没有可用区块,就将区块大小上调至 8 倍数的边界,然后调用 refill(),准备为 free list 重新填充空间。refill() 将于稍后介绍

// n must be > 0
static void *allocate(size_t n) {
    obj * volatile * my_free_list;
    obj * result;
    // 大于 128 就调用第一级配置器
    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;
    }
    // 调整 free list
    *my_free_list = result -> free_list_link;
    return (result);
}
空间释放函数 deallocate()

该函数首先判断区块大小,大于 128 bytes 就调用第一级配置器,小于 128 bytes 就找出对应的 free list,将区块回收

// p 不可以是 0
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 ;
    }
    // 寻找对应的 free list
    my_free_list = free_list + FREELIST_INDEX(n);
    // 调整 free list,回收区块
    q -> free_list_link = *my_free_list;
    *my_free_list = q;
}
重新填充 free lists,refill()

当 allocate() 发现 free list 中没有可用区块时,就调用 refill(),准备为 free list 重新填充空间。新的空间将取自内存池 ( 由 chunk_alloc() 完成)。缺省取得 20 个新区块,但万一内存池空间不足,获得的区块数可能小于 20:

// 返回一个大小为 n 的对象,并且有时候会为适当的 free list 增加节点
// 假设 n 以及适当上调至 8 的倍数
template <bool threads, int inst>
void *__default_alloc_template<threads, inst>::refill(size_t n) {
    int nobjs = 20;
    // 调用 chunk_alloc,尝试取得 nobjs 个区块作为 free list 的新节点
    // 注意 nobjs 是 pass by reference
    char *chunk = chunk_alloc(n, nobjs);    // 下节详述
    obj * volatile * my_free_list;
    obj * result;
    obj * current_obj, *next_obj;
    int i;

    // 如果只获得一个区块,这个区块就分配给调用者,free list 无新节点
    if(1 == nobjs) return (chunk);
    // 否则准备调整 free list,纳入新节点
    my_free_list = free_list + FREELIST_INDEX(n);

    // 以下在 chunk 空间内建立 free list
    result = (obj *) chunk;     // 这一块准备返回给客端
    // 以下导引 free list 指向新配置的空间 (取自内存池)
    *my_free_list = next_obj = (obj *) (chunk + n);     // 第一块已分配,+ n 获得第二块起始地址
    // 以下将 free list 的各节点串接起来
    for(i = 1;; ++ i) {
        current_obj = next_obj;
        next_obj = (obj *)((char *)next_obj + n);
        if(nobjs - 1 == i) {
            current_obj -> free_list_link = 0;
            break;
        } else {
            current_obj -> free_list_link = next_obj;
        }
    }
    return result;
}
内存池 (memory pool),chunk_alloc()

从内存池中取空间给 free list 使用,是 chunk_alloc() 的工作:

// 假设 size 已经适当上调至 8 的倍数
// 注意参数 nobjs 是 pass by reference
template <bool threads, int inst>
char *
__default_alloc_template<threads, inst>::
chunk_alloc(size_t size, int &nobjs) {
    char *result;
    size_t total_bytes = size * nobjs;
    size_t bytes_left = end_free - start_free;  // 内存池剩余空间

    if(bytes_left >= total_bytes) {
        // 内存池剩余空间完全满足需求量
        result = start_free;
        start_free += total_bytes;
        return (result);
    } else if(bytes_left >= size) {
        // 内存池剩余空间不能完全满足需求量,但能够供应一个(含)以上的区块
        // 就全部分配出去
        nobjs = bytes_left / size;
        total_bytes = size * nobjs;
        result = start_free;
        start_free += total_bytes;
        return (result);
    } else {
        // 内存池剩余空间连一块区大小都无法提供
        size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
        // 以下试着让内存池中的残余零头还有利用价值
        if(bytes_left > 0) {
            // 内存池内还有一些零头,先分配给适当的 free list
            // 首先寻找适当的 free list
            obj * volatile * my_free_list = free_list + FREELIST_INDEX(bytes_left);
            // 调整 free list,将内存池中的残余空间编入
            ((obj *)start_free) -> free_list_link = *my_free_list;
            *my_free_list = (obj *) start_free;
        }

        // 配置 heap 空间,用来补充内存池
        start_free = (char *) malloc(bytes_to_get);
        if(0 == start_free) {
            // heap 空间不足,malloc() 失败
            int i;
            obj * volatile * my_free_list, *p;
            // 试着检视我们手上拥有的东西,这不会造成伤害,我们不打算尝试配置
            // 较小的区块,因为那在多进程(mutli-process)机器上容易导致灾难
            // 以下搜索适当的 free list
            // 所谓适当是指 “尚有未用区块,且区块够大” 之 free list
            for(i = size; i <= __MAX_BYTES; i += __ALIGN) {
                my_free_list = free_list + FREELIST_INDEX(i);
                p = *my_free_list;
                if(0 != p) {    // free list 尚有未用区块
                    // 调至 free list 以释出未用区块
                    *my_free_list = p -> free_list_link;
                    start_free = (char *)p;
                    end_free = start_free + i;
                    // 递归调用,为了修正 nobjs
                    return (chunk_alloc(size, nobjs));
                }
            }
            end_free = 0;   // 如果出现意外,到处没有内存可用
            // 就调用第一级配置器,看看 oom 机制能否尽点力
            start_free = (char *) malloc_alloc::allocate(bytes_to_get);
            // 这会导致抛出异常,或内存不足的情况获得改善
        }
        heap_size += bytes_to_get;
        end_free = start_free + bytes_to_get;
        // 递归自己,修正 nobjs
        return (chunk_alloc(size, nobjs));
    }
}

可以看到,当内存池空间也不足时,会将内存的碎片部分选择适当的 free list,然后连接上去。接着用 heap 空间来补充内存池;如果 heap 空间不足,chunk_alloc() 则会在大于 size 的 free list 上取下一块,还给内存池,然后递归调用;如果还不行,就试试第一级配置器的 oom 机制,到了这里,就只有两种可能了,要么抛出异常,要么内存得到改善,继续执行函数。

同时,可以发现,每次向 heap 申请空间时,申请的空间大小为 2 * total_bytes + (heap_size() >> 4),即将其中的一份 total_bytes 交由 free_list,剩下的交由内存池管理。

内存处理基本工具

​ STL 定义了 5 个全局函数,作用于未初始化空间上。分别是 construct()、destroy()、uninitialized_copy()、uninitialized_fill()、uninitialized_fill_n()。

​ 这一节详细介绍了后三个函数。

uninitialized_copy
template <class InputIterator, class ForwardIterator>
ForwardIterator
uninitialized_copy(InputIterator first,InputIterator last,
                  	ForwardIterator result);

输出范围 [result, result + (last - first)) 范围内的迭代器都指向未初始化区域。uninitialized_copy 会将 [first, last) 内的每个对象产生一份复制品,放进输出范围。该函数会调用 construct(&*(result + (i - first)), *i),产生 *i 的复制品,放到输出范围的相对位置上。(!注意,迭代器并不是指针,所以 &*(result + (i - first)) 与 result + (i - first) 是完全不同的)

uninitialized_fill
template <class ForwardIterator, class T>
void uninitialized_fill(ForwardIterator first,ForwardIterator last,
                       const T &x);

该函数作用就不说了。它同样是使用 construct(&*i, *i) 来产生复制品,并放到对应位置。

uninitialized_fill_n
template <class ForwardIterator, class Size, class T>
ForwardIterator
uninitialized_fill_n(ForwardIterator first,Size n, const T &x);

此函数同样使用 construct(&*i, *i) 产生 *i 的复制品,放在对应位置。

uninitialized_copy、uninitialized_fill、uninitialized_fill_n 的实现

​ 这三个函数都有 “commit or rollback” 的语意:要么产生所有必要的元素,否则不产生任何元素。

​ 同时,三个函数的实现都会判断输入范围迭代器的 value_type 是否是 POD 型别。如果是,那么就分别调用库函数 copy、fill、fill_n 进行实现 (因为采用赋值运算就相当于构造);否则必须一个一个进行构造,也就是用 for + construct。

​ 对于 uninitialized_copy 还编写了针对 char* 和 wchar_t* 的特化版本。因为这两个类型可以采用最具效率的做法 memmove (直接移动内存内容) 来执行复制行为。

​ POD 意指 Plain Old Data,也就是标量型别 (scalar types) 或传统的 C struct 型别。POD 型别必然拥有 trivial ctor/dtor/copy/assignment 函数,因此,我们可以对 POD 型别采用最具效率的初值填写手法,而对 non-POD 型别采用最保险安全的做法。

这里列出 uninitialized_copy 源码(可以发现,想比前面的 destroy 函数,实现方式真的很类似):

template <class InputIterator, class ForwardIterator>
inline ForwardIterator
    uninitialized_copy(InputIterator first,InputIterator last,
                      ForwardIterator result) {
    return __uninitialized_copy(first,last,result,value_type(result));
    // 利用 value_type 取出 first 的 value_type,(用于下面函数的 class T)
}

template <class InputIterator, class ForwardIterator,class T>
inline ForwardIterator
    __uninitialized_copy(InputIterator first,InputIterator last,
                      ForwardIterator result) {
    typedef typename __type_traits<T>::is_POD_type is_POD;
    return __uninitialized_copy_aux(first,last,result,is_POD());
}

// 如果是 POD 类型,就会转到此函数
template <class InputIterator, class ForwardIterator,class T>
inline ForwardIterator
    __uninitialized_copy_aux(InputIterator first,InputIterator last,
                      ForwardIterator result, __true_type) {
    return copy(first,last,result);		// 调用 STL 的 copy 算法
}

// 如果是 non-POD,转到这里。这两个函数其实是通过最后一个形参形成重载的。最后一个形参是一个
// struct,没有任何成员,主要是用于形成重载。
template <class InputIterator, class ForwardIterator,class T>
inline ForwardIterator
    __uninitialized_copy_aux(InputIterator first,InputIterator last,
                      ForwardIterator result, __false_type) {
    ForwardIterator cur = result;
    	// 原本有异常处理,这里省略
    	for( ;first != last;++ first, ++ cur)
            construct(&*cur, *first);	// 必须一个一个元素构造
    return cur;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值