STL之空间配置器allocator

news2024/12/30 2:58:56

STL之空间配置器allocator

  • 空间配置器的标准接口
    • 设计一个简单的空间配置器, JJ::allocator
  • 具备次配置力(sub-allocation)的SGI空间配置器
    • SGI标准的空间配置器, std::allocator
    • SGI特殊的空间配置器, std::alloc
    • 构造和析构基本工具:construct()和destroy()
    • 空间的配置与释放,std::alloc
    • 第一级配置器__malloc_alloc_template剖析
    • 第二级配置器__default_alloc_template剖析
    • 空间配置函数allocate()
    • 空间释放函数deallocate()
    • 重新填充 free lists
    • 内存池(memory pool)

以STL的运用角度而言,空间配置器是最不需要介绍的东西,它总是隐藏在一切组件(更具体的说是指容器,container)的背后,默默的工作,默默付出。但若以STL的实现角度而言,第一个需要介绍的就是空间配置器,因为整个STL的操作对象(所有的数值)都存放在容器之内,而容器一定需要配置空间以放置资料。不先掌握空间配置器的原理,难免在阅读其它STL组件的实现时处处遇到挡路石。
为什么不说allocator是内存配置器而说它是空间配置器呢?因为空间不一定是内存,空间也可以是磁盘或其它辅助存储介质。是的,我们可以写一个allocator,直接指向磁盘空间 1。以下介绍的是SGI STL提供的配置器,配置的对象,呃,是的,是内存。

空间配置器的标准接口

根据STL的规范,以下是allocator的必要接口:

allocator::value_type
allocator::pointer
allocator::const_pointer
allocator::reference
allocator::const_reference
allocator::size_type
allocator::difference_type
allocator::rebind                           // 一个嵌套(nested) class template。class rebind<U>拥有唯一成员other,那是一个typedef,代表allocator<U>
allocator::allocator()                      // default constructor
allocator::allocator(const allocator&)      // copy constructor
template<class U>allocator::allocator(const allocator<U>&)  // 泛化的copy constructor
allocator::~allocator()                     // destructor
pointer allocator::address(reference x) const   // 算式a.address(x)等同于&x
const_pointer allocator::address(reference x) const  // a.address(x)等同于&x
pointer allocator::allocate(size_type n, const void* = 0)
    // 配置空间,足以存储n个T对象。第二个参数是个提示
void allocator::deallocate(pointer p, size_type n)  // 归还先前配置的空间
size_type allocator::max_size() const               // 返还可成功配置的最大量
void allocator::construct(pointer p, const T&x)     // 等同于new((void*)p) T(x)
void allocator::destroy(pointer p)                  // 等同于p->~T()

设计一个简单的空间配置器, JJ::allocator

根据前述的标准接口,我们可以自行完成一个功能简单、接口不怎么齐全的allocaltor如下:
jjallocate.h

#ifndef __JJALLOC__
#define __JJALLOC__
#include <new>
#include <cstddef>
#include <cstdlib>
#include <climits>
#include <iostream>
using namespace std;

namespace JJ {
    
template <class T>
inline T* _allocate(ptrdiff_t size, T*) {
    set_new_handler(0);
    T* tmp = (T*)(::operator new ((size_t)(size * sizeof(T))));
    if (tmp == 0) {
        cerr << "out of memeory" << endl;
        exit(1);
    }
    return tmp;
}

template <class T>
inline void _deallocate(T* buffer) {
    ::operator delete(buffer);
}

template <class T1, class T2> 
inline void _construct(T1*p, const T2& value)
{
    new(p) T1(value);       // 类T1有参数为T2的构造函数,此处在p1出,调用T1的该构造函数
}

template <class T>
inline void _destroy(T* ptr) {
    ptr->~T();
}

template <class T>
class allocator {
public:
    typedef T           value_type;
    typedef T*          pointer;
    typedef const T*    const_pointer;
    typedef T&          reference;
    typedef const T&    const_reference;
    typedef size_t      size_type;
    typedef ptrdiff_t   difference_type;

    // rebind allocator of type U
    template <class U>
    struct rebind {
        typedef allocator<U> other;
    };

    // hint used for locality. ref.[Austern],p189
    pointer allocate(size_type n, const void* hint=0) {
        return _allocate((difference_type)n, (pointer)0);
    }

    void deallocate(pointer p, size_type n) { _deallocate(p); }

    void construct(pointer p, const T& value) {
        _construct(p, value);
    }

    void destroy(pointer p) {
        _destroy(p);
    }

    pointer address(reference x) { return (pointer)&x; }

    const_pointer const_address(const_reference x) {
        return (const_pointer)&x;
    }

    size_type max_size() const {
        return size_type(UINT_MAX/sizeof(T));
    }
};
} // end of namespace JJ
#endif

将JJ::allocator应用于程序之中,我们发现,它只能有限度地搭配PJ STL和RW STL,示例如下:jjallocator.cpp

#include "jjalloc.h"
#include <vector>
#include <iostream>
using namespace std;

int main() {
    int ia[] = {0,1,2,3,4};
    unsigned int i;

    vector<int, JJ::allocator<int> > iv(ia, ia+5);
    for (i = 0; i < iv.size(); ++i) {
        cout << iv[i] << ' ';
    }
    cout << endl;
}

“只能有限搭配PJ STL”是因为,PJ STL未完全遵循STL规格,其所供应的许多容器都需要一个非标准的空间配置器接口allocator::__Charalloc()。“只能有限度地搭配RW STL”则因为,RW STL在很多容器身上运用了缓冲区,情况复杂得多,JJ::allocator无法与之兼容。至于完全无法应用于SGI STL,是因为SGI STL在这个项目上根本就逸脱了STL标准规格,使用一个专属的、拥有层次配置(sub-allocation)能力的、效率优越的特殊配置器,稍后有详细介绍。
提前做一下说明,事实上SGI STL仍然提供了一个标准的配置器接口,只是把它做了一层隐藏。这个标准接口的配置器名为simple_alloc,稍后便会看到。

具备次配置力(sub-allocation)的SGI空间配置器

SGI STL的配置器与众不同,也与标准规范不同,其名称是alloc而非allocator,而且不接受任何参数。换句话说,如果在程序中明白采用SGI配置器,则不能采用标准写法:

	vector<int> std::allocator<int> > iv;

必须这么写

vector<int, std::alloc> iv;

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

虽然SGI也定义有一个符合部分标准,名为allocator的配置器,但SGI自己从未用过它,也不建议使用。主要原因是效率不佳,只把C++的::operator new和::operator delete做一层薄薄的包装而已。下面是SGI的std::allocator全貌:

#ifndef DEFALLOC_H
#define DEFALLOC_H

#include <new.h>
#include <stddef.h>
#include <stdlib.h>
#include <limits.h>
#include <iostream.h>
#include <algobase.h>

template <class T>
inline T* allocate(ptrdiff_t size, T*) {
    set_new_handler(0);
    T* tmp = (T*) (::operator new((size_t)(size * sizeof(T))));
    if (tmp == 0) {
        cerr << "out of memory" << endl;
    }
    return tmp;
}

template <class T>
inline void deallocate(T* buffer) {
    ::operator delete(buffer);
}

template <class T>
class allocator {
public:
    typedef T           value_type;
    typedef T*          pointer;
    typedef const T*    const_pointer;
    typedef T&          reference;
    typedef const T&    const_reference;
    typedef size_t      size_type;
    typedef ptrdiff_t   difference_type;

    pointer allocate(size_type n) {
        return ::allocate((difference_type)n, (pointer)0);
    }
    void deallocate(pointer p) { ::deallocate(p); }
    pointer address(reference x) { return (pointer)&x; }
    const_pointer const_address(const_reference x) {
        return (const_reference)&x;
    }

    size_type init_page_size() {
        return max(size_type(1), size_type(4096/sizeof(T)));
    }
    size_type max_size() const {
        return max(size_type(1), size_type(UINT_MAX/sizeof(T)));
    }
};

// 特化版本(specialization)
class allocator<void> {
public:
    typedef void * pointer;
};
#endif

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

上一节所说的allocator只是基层内存配置/释放行为(也就是::operator new和::operator delete)的一层薄薄的包装,并没有考虑到任何效率上的强化。SGI另有法宝供其内部使用。
一般而言,我们所习惯的C++内存配置操作和释放操作是这样的:
class Foo { … };
Foo* pf = new Foo; // 配置内存,然后构造对象
delete pf // 将对象析构,然后释放内存
这其中的new算式内含两阶段操作(1)调用::operator new配置内存;(2)调用Foo::Foo()构造对象内容。delete算式也内含两阶段操作:(1)调用Foo::~Foo将对象析构;(2)调用::operator delete释放内存。
为了精密分工,STL allocator决定将两阶段操作区分开来。内存配置操作由alloc::allocate()负责,内存释放操作由alloc::deallocate()负责;对象构造由::construct()负责,对象析构由::destroy()负责。
STL标准规格告诉我们,配置器定义于<memory>之中,SGI <memory>内含一下两个文件:
#include <stl_alloc.h> // 负责内存空间的配置和释放
#include <stl_construct.h> // 负责对象内容的构造和析构
内存空间的配置/释放于对象内容的构造/析构,分别着落再这两个文件身上。其中<stl_construct.h> 定义有两个基本函数:构造用的construct()和析构用的destroy()。

构造和析构基本工具:construct()和destroy()

下面是<stl_construct.h>的部分内容

#include <new.h>

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

template <class T>
inline void destroy(T* pointer) {
    pointer->~T();
}

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

// 判断元素的数值型别(value_type)是否有trivial destructor
template<class ForwardIterator, class T>
inline void __destroy(ForwardIterator first, ForwardIterator last, T*) {
    typedef typename __type_traits<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*) {}

这两个作为构造、析构之用的函数被设计为全局函数,符合STL的规范。此外,STL还规定配置器必须拥有名为construct()和destroy()的两个成员函数,然后真正在SGI STL中大显身手的那个名为std::alloc的配置器并未遵守这一规则(稍后可见)。
上述construct()接受一个指针p和一个初值value,该函数的用途就是将初值设定到指针所指的空间上。C++的placement new运算子可用来完成这一任务。
destroy有两个版本,第一个版本接受一个指针,准备将该指针所指之物析构掉。这很简单,直接调用该对象的析构函数即可。第二版本接受first和last两个迭代器,准备将[first, last)范围内的所有对象析构掉。我们不知道这个范围有多大,万一很大,而每个对象的析构函数都无关痛痒,那么一次次调用这些无关痛痒的析构函数,对效率是一种伤害。一次,这里首先利用value_type()获得迭代器所指对象的型别,再利用__type_traits判断该型别的析构函数是否无关痛痒。若是__true_type,则什么也不做就结束;若否__false_type, 这才以循环方式巡访整个范围,并在循环中每经历一个对象就调用第一个版本的destroy()。
这样的观念很好,但C++本身并不直接支持对“指针所指之物”的型别判断,也不支持对“对象析构函数是否为trivial”的判断,因此,上述的value_type()和__type_traits<>该如何实现呢?迭代器与traits编程技法中有详细介绍。

空间的配置与释放,std::alloc

看完了内存配置后的对象构造行为和内存释放前的对象析构行为,现在我们来看看内存的配置和释放。
对象构造前的空间配置和对象析构后的空间释放,由<stl_alloc.h>负责,SGI对此的设计哲学如下:

  • 向system heap要求空间。
  • 考虑多线程(multi-threads)状态。
  • 考虑内存不足时的应变措施。
  • 考虑过多“小型区块”可能造成的内存碎片(fragment)问题。
    为了将问题控制在一定的复杂度内,我们暂且不考虑多线程的情况。
    C++的内存配置基本操作是::operator new(),内存释放基本操作是::operator delete(),这两个全局函数相当于C的malloc()和free()函数。是的,正是如此,SGI正是以malloc()和free()完成内存的配置和释放。
    考虑到小型区块所可能造成的内存破碎问题,SGI设计了双层配置器,第一级配置器直接使用malloc()和free(),第二级配置器则视情况采用不同的策略:当配置区块超过128 bytes时,视之为“足够大”,便调用第一级配置器;当配置区块小于128bytes时,视之为“过小”,为了降低额外负担(overhead),便采用复杂的memory pool整理方式,而不再求助于第一级配置器。整个设计究竟只开放第一级配置器,或是同时开放第二级配置器,取决于__USE_MALLOC是否被定义(SGI STL并未定义__USE_MALLOC):
#ifdef __USE_MALLOC
...
typedef __malloc_alloc_template<> malloc_alloc;
typedef malloc_alloc;
#else 
...
// 令alloc为第二级配置器
typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc;
#endif /* __USE_MALLOC */

其中__malloc_alloc_template就是第一级配置器,__default_alloc_template就是第二级配置器。稍后分别有详细介绍;注意:alloc并不接受任何template型别参数。
无论alloc被定义为第一级或第二级配置器,SGI还为它再包装一个接口如下,使配置器的接口能够符合STL规格:

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); }
};

其内部四个成员函数其实都是单纯的转调用,调用传递给配置器(可能是第一级也可能是第二级)的成员函数。这个接口使配置器的配置单位从bytes转为个别元素的大小(sizeof(T)).SGI STL容器全部使用这个simple_alloc接口,

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);
}
...
};

一、二级配置器的关系,接口包装,及实际运用方式,见下图所示:
图1

第一级配置器__malloc_alloc_template剖析

首先我们观察第一级配置器:

#if 0
#include <new>
#icnlude __THROW_BAD_ALLOC throw bad_alloc
#elif !defined(__THROW_BAD_ALLOC)
#include <iostream>
#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);
    if (result == 0) result = oom_malloc(n);
    return result;
}

static void deallocate(void *p, size_t /* n */) {
    free(p);
}

static void * reallocate(void*p, size_t /* od_sz */, size_t new_sz) 
{
    void *result = realloc(p, new_sz);
    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。有待客户端设定
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 = realloc(p, n);         // 再次尝试配置内存
        if (result) return(result);
    }
}

};

// 注意一下直接将参数inst指定为0
typedef __malloc_alloc_template<0> malloc_alloc;

第一级配置器以malloc(),free(),realloc()等C函数执行实际的内存配置、释放、从配置操作,并实现出类似C++ new-handler的机制。是的,它不能直接运用C++ new-handler机制,因为它并非使用::operator new来配置内存。
所谓C++ new handler机制是,你可以要求系统在内存配置需求无法被满足时,调用一个指定的函数。换句话说,一旦::operator new无法完成任务,在丢出std::bad_alloc异常状态之前,会先调用由客户端指定的处理例程该处理例程被称为new-handler。
注意,SGI以malloc而非::operator new来配置内存,因此,SGI不能直接使用C++的set_new_handler(),必须仿真一个类似的set_malloc_handler().
请注意,SGI第一级配置器的allocate()和rellocate()都是在调用malloc()和realloc()不成功后,改调用oom_malloc()和oom_realloc().后两者都有内循环,不断调用“内存不足处理例程”,期望在某次调用之后,获得足够的内存而圆满完成任务。但如果“内存不足处理例程”并未设定,oom_malloc和oom_realloc()会调用————THROW_BAD_ALLOC,丢出bad_alloc异常信息,或利用exit(1)中止程序。
记住,设计“内存不足处理例程”是客户端的责任,设定“内存不足处理例程”也是客户端的责任。关于“内存不足处理例程”的做法和模式,另写博文进行阐述。

第二级配置器__default_alloc_template剖析

第二级配置器多了一些机制,避免太多小额区块造成内存的碎片。小额区块带来的起始不仅是内存碎片,配置时的额外负担(overhead)也是一个大问题。额外负担永远无法避免,毕竟系统要考这多出来的空间来管理内存,如下图所示,但是区块愈小,额外负担所占的比例就愈大,愈显得浪费。
图2

SGI第二级配置器的做法是,如果区块够大,超过128bytes时,就移交第一级配置器。当区块小于128bytes时,则以内存池(memory pool)管理,此法又称为次层配置(sub-allocations):每次配置一大块内存,并维护对应之自由链表(free-list)。下次若再有相同大小的内存需求,就直接从free-lists中拔出。如果客户端释放小额区块,就由配置器回收到free-lists中----是的,别忘了,配置器除了负责配置,也负责回收。为了方便管理,SGI第二级配置器会主动将任何小额区块的内存需求上调至8的倍数(例如调用申请30bytes,就自动调整为32byts),并维护16个free-lists,各自管理大小分贝为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128bytes的小额区块。free-lists的节点结构如下:

union obj {
    union obj * free_list_link;
    char client_data[1];            /* The client sees this */
};

读者可能会想,为了维护链表(lists),每个节点需要额外的指针(指向下一个节点),这不又造成了另一种额外负担吗?顾虑是对的,但是STL早已有好的解决办法。注意,上述obj所用的是union,由于union之故,从其第一字段观之,obj可被视为一个指针,指向实际区块,如下图所示。一物二用的结果是,不会为了维护链表所必须得指针而造成内存的另一种浪费(我们正在努力节省内存的开销)。这种技巧在强类型语言如Java中行不通,但非强类型语言如C++中十分普遍。
图三

下面是第二级配置器的部分实现内容:

enum {__ALIGN = 8};
enum {__MAX_BYTES = 128};
enum {__NFREELISTS = __MAXBYTES / __ALIGN};     // free-lists 个数

template <bool threads, int inst>
class __default_alloc_template {
private:
    // 将bytes上调至8的倍数
    static size_t ROUND_UP(size_t bytes) {
        return (((bytes) + __ALIGN - 1) & ~(__ALIGIN - 1));
    }
    union obj {
        union obj * free_list_link;
        char client_data[1];            
    };
    // 16个free_lists
    static obj * volatile free_list[__NFREELISTS];
    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 alllocation 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* rellocate(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>
char* __default_alloc_template<threads, inst>::heap_size = 0;

template <bool threads, int inst>
__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,};

空间配置函数allocate()

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

// n must be > 0
static void * allocate(size_t n) {
    obj * valatitle * my_free_list;
    obj * resutl;
    // 大于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(ROUNDUP(n));
        return r;
    }
    // 调整free list
    *my_free_list = result->free_list_link;
    return (result);
}

区块子free list调出操作,如下图所示:
图4

空间释放函数deallocate()

身为一个配置器,__default_alloc_template拥有配置器标准接口函数deallocate()。该函数首先判断区块大小,大于128bytes调用第一级配置器,小于128bytes就找出对应的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);
    q->free_list = *my_free_list;
    *my_free_list = q;
}

基本就是将释放的指针插入到了链表的头部;

重新填充 free lists

回头讨论先前说过的allocate()。当它发现free lists中没哟可用区块了时,就调用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;
    *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-> free_list_link = 0;
            break;
        } else {
            current_obj-> free_list_link = nex_obj;
        }
    }
    return (result);
}

内存池(memory pool)

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

// 假设size已经适当调整为8的倍数
// 注意参数nobjs是pass by reference
template <bool threads, int inst>
char * __default_alloc_template<threads, int>::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;
        strat_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;
            // 试着检验我们手上拥有的东西。这不会造成伤害。我们不打算尝试配置较小的区块,因为那在对进程机器上容器导致灾难
            // 以下搜寻适当的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内尚有未用区块
                {
                    *my_free_list = p->free_list_link;
                    start_free = (char*)p;
                    end_free = start_free + i;
                    // 递归调用,为了修正nobjs
                    return (chunk_alloc(size, nobjs));
                }
            }
            // 运行到此处,说明free list中也没有多余的空间
            end_free = 0;
            // 调用第一级配置器,看看out-of-memory机制能否尽点力
            start_free = (char*) malloc::allocate(bytes_to_get); // 这会导致抛出异常(exceptions),或内存不足的情况得到改善
        }
        heap_size += bytes_to_get;
        end_free = start_free + bytes_to_get;
        // 递归调用自己,修正nbojs
        return (chunk_alloc(size, nobjs));
    }
}

上述的chunk_alloc()函数以end_free-start_free来判断内存池的水量。如果水量充足,就直接调出20个区块返回给free list。如果水量不足以提供20个区块,但还足够供应一个以上的区块,就拔出这股组20个区块的空间出去。这时候pass by reference的nobjs参数将被修改为实际能够供应的区块数。如果内存池连一个区块空间都无法供应,,此时便需要利用malloc()从heap中配置内存,为内存池诸如源头活水以应付需求。新水量的大小为需求量的两倍,再加上一个随机配置次数增加来愈来愈大的附加量。
万一山穷水尽,整个system heap空间都不够了,malloc()行动失败,chunk_alloc()就四处寻找有无“尚有未用区块,且区块够大”之free lists。找到了就挖一块交出,找不到就调用第一级配置器。第一级配置器起始也是调用malloc()来配置内存,但它有out-of-memory处理机制,或许有机会释放其它的内存拿来此处使用。如果可以就成功,否则发出bad_alloc异常。
以上便是真个第二级空间配置器的设计。

不过此处有个疑问:在线程池每次重新调用了malloc后,此时已经没人维护内存池之前malloc申请的内存了;在程序退出的时候,谁来负责销毁之前已经申请的内存呢?或许通过free lists的连续特性可以重新获得可free的指针?欢迎大家参与评论及讨论


  1. 请参考disk-Based Container Objects, By Tom Nelson, C/C++ Users Journal, 1998/04 ↩︎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2257446.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

人大金仓(KingBaseEs)数据库操作手册

人大金仓数据库&#xff08;KingbaseES&#xff09;是由北京人大金仓信息技术股份有限公司&#xff08;简称人大金仓&#xff09;自主研发的、具有自主知识产权的通用关系型数据库管理系统。 官方下载地址&#xff1a;KingbaseES 人大金仓数据库 KES技术文档在线手册&#xf…

容器镜像仓库

文章目录 1、docker hub1_注册2_登录3_创建容器镜像仓库4_在本地登录Docker Hub5_上传容器镜像6_下载容器镜像 2、harbor1_获取 docker compose 二进制文件2_获取harbor安装文件3_获取TLS文件4_修改配置文件5_执行预备脚本6_执行安装脚本7_验证运行情况8_访问harborUI界面9_har…

概率论相关知识随记

作为基础知识的补充&#xff0c;随学随记&#xff0c;方便以后查阅。 概率论相关知识随记 期望&#xff08;Expectation&#xff09;期望的定义离散型随机变量的期望示例&#xff1a;掷骰子的期望 连续型随机变量的期望示例&#xff1a;均匀分布的期望 期望的性质线性性质期望的…

如何解决压测过程中JMeter堆内存溢出问题

如何解决压测过程中JMeter堆内存溢出问题 背景一、为什么会堆内存溢出&#xff1f;二、解决堆内存溢出措施三、堆内存参数应该怎么调整&#xff1f;四、堆内存大小配置建议 背景 Windows环境下使用JMeter压测运行一段时间后&#xff0c;JMeter日志窗口报错“java.lang.OutOfMe…

快速了解 Aurora DSQL

上周在 AWS re:Invent大会&#xff08;类似于阿里云的云栖大会&#xff09;上推出了新的产品 Aurora DSQL[1] &#xff0c;在数据库层面提供了多区域、多点一致性写入的能力&#xff0c;兼容 PostgreSQL。并声称&#xff0c;在多语句跨区域的场景下&#xff0c;延迟只有Google …

java垃圾回收机制中的引用计数算法

垃圾回收机制 java 语言中一个显著的特点就是引入了java回收机制&#xff0c;是c程序Q员最头疼的内存管理的问题迎刃而解&#xff0c;它使得java程序员在编写程序的时候不在考虑内存管理。由于有个垃圾回收机制&#xff0c;iava中的额对象不在有“作用域”的概念&#xff0c;只…

android studio 读写文件操作(应用场景二)

android studio版本&#xff1a;2023.3.1 patch2 例程&#xff1a;readtextviewIDsaveandread 本例程是个过渡例程&#xff0c;如果单是实现下图的目的有更简单的方法&#xff0c;但这个方法是下一步工作的基础&#xff0c;所以一定要做。 例程功能&#xff1a;将两个textvi…

单片机C51--笔记8-STC89C51RC/RD-IIC协议

一、概述 IIC全称Inter-Integrated Circuit (集成电路总线) 是由PHILIPS公司在80年代开发的两线式串行总线&#xff0c;用于连接微控制器及其外围设备。IIC属于半双 工同步通信方式。 特点 简单性和有效性。 由于接口直接在组件之上&#xff0c;因此IIC总线占用的空间非常小…

小程序-基于java+SpringBoot+Vue的智慧校园管理系统设计与实现

项目运行 1.运行环境&#xff1a;最好是java jdk 1.8&#xff0c;我们在这个平台上运行的。其他版本理论上也可以。 2.IDE环境&#xff1a;IDEA&#xff0c;Eclipse,Myeclipse都可以。推荐IDEA; 3.tomcat环境&#xff1a;Tomcat 7.x,8.x,9.x版本均可 4.硬件环境&#xff1a…

Linux下的编程

实验7 Linux下的编程 一、实验目的 熟练掌握Linux下Python编程的方法、函数调用方法以及shell编程中的控制结构。 二、实验环境 硬件&#xff1a;PC电脑一台&#xff0c;网络正常。 配置&#xff1a;win10系统&#xff0c;内存大于8G &#xff0c;硬盘500G及以上。 软件&a…

flink的安装配置(详细版本)

Standalone集群模式安装部署 conda deactivate 退出 base环境 Flink支持多种安装模式。 local&#xff08;本地&#xff09;——本地模式 standalone——独立模式&#xff0c;Flink自带集群&#xff0c;开发测试环境使用 standaloneHA—独立集群高可用模式&#xff0c;Fli…

【大语言模型】LangChain ModelsIO与Models I/O Promopts详解

【大语言模型】LangChain ModelsIO与Prompts详解 一、LangChain ModelsIO1、简介2、Models I/O 的应用场景3、Models I/O 主要模块3.1、Prompts3.2、Modelsa、MESSAGES 类型 3.3、Output Parsers 二、LangChain ModelsIO Prompts1、简介2、Prompts 的优点3、实战示例3.1、Promp…

Prometheus加入BasicAuth认证,通过配置 Prometheus 的 Web 身份验证来限制访问/debug/pprof/

Prometheus 作为监控工具&#xff0c;暴露了大量的系统监控数据和配置信息&#xff0c;这些数据可能包含敏感信息。Prometheus 默认没有身份验证&#xff0c;任何能够访问 Prometheus Web 界面的人都可以查看和查询这些数据。 此外Prometheus Web 界面的/debug/pprof/接口存在…

再谈多重签名与 MPC

目录 什么是 MPC 钱包以及它们是如何出现的 多重签名和智能合约钱包已经成熟 超越 MPC 钱包 关于小队 多重签名已经成为加密货币领域的一部分&#xff0c;但近年来&#xff0c;随着 MPC&#xff08;多方计算&#xff09;钱包的出现&#xff0c;多重签名似乎被掩盖了。MPC 钱包之…

QT 中基于 TCP 的网络通信

基础 基于 TCP 的套接字通信需要用到两个类&#xff1a; 1&#xff09;QTcpServer&#xff1a;服务器类&#xff0c;用于监听客户端连接以及和客户端建立连接。 2&#xff09;QTcpSocket&#xff1a;通信的套接字类&#xff0c;客户端、服务器端都需要使用。 这两个套接字通信类…

数值分析—数值积分

研究背景 积分的数学解法为牛顿莱布尼兹公式&#xff0c;数学表示为 ∫ a b f ( x ) d x F ( b ) − F ( a ) \int_{a}^{b} f(x)dxF(b)-F(a) ∫ab​f(x)dxF(b)−F(a)&#xff0c;但应用该方法有如下困难&#xff1a; 1&#xff0c; f ( x ) f(x) f(x)的原函数有时不能用初等函…

如何配置Github并在本地提交代码

前提: 可以流畅访问github, 需要一些上网技巧, 这就自行处理了 申请一个github账号 Github官网地址 首先就是邮箱注册啦, github没有对邮箱的限制, 只要是能收邮件的就ok, qq邮箱, 163等都可以使用. 然后和普通注册账号一样, 一路填写需要的信息, 验证邮箱即可. 如何新增代…

Unity控制物体材质球的改变

Unity控制物体材质球的改变 1.前言2.示例单个材质球的获取和更改多个材质球的获取和更改 1.前言 材质球其实就是一个数组&#xff0c;有的只有一个&#xff0c;有的却有多个 2.示例 单个材质球的获取和更改 private Material m_material;m_material GetComponent<Render…

一些硬件知识【2024/12/6】

MP6924A: 正点原子加热台拆解&#xff1a; PMOS 相比 NMOS 的缺点&#xff1a; 缺点描述迁移率低PMOS 中的空穴迁移率约为电子迁移率的 1/3 到 1/2&#xff0c;导致导通电流较低。开关速度慢由于迁移率较低&#xff0c;PMOS 的开关速度比 NMOS 慢&#xff0c;不适合高速数字电…

数据结构排序算法详解

数据结构排序算法详解 1、冒泡排序&#xff08;Bubble Sort&#xff09;2、选择排序&#xff08;Selection Sort&#xff09;2、插入排序&#xff08;Insertion Sort&#xff09;4、快速排序&#xff08;Quick Sort&#xff09; 1、冒泡排序&#xff08;Bubble Sort&#xff09…