C++的空间分配器allocator

一、背景

STL的 操作对象都存储在容器(vector,deque,list)内,而容器对象是一定要配置内存空间的,每一种容器的内存都是通过空间分配器(allocator)实现的。

空间分配器allocator,这是一种用来分配和管理内存的模板类。allocator是STL的一部分,用于管理动态分配的内存,它为对象提供内存,而不像常规 new 和 delete 运算符那样分别为每个对象调用构造函数和析构函数。allocator的使用可以提高内存分配和管理的效率,减少内存碎片的产生,提高程序的性能。

注:为什么不说 allocator 是内存分配器而说它是空间分配器呢?因为空间不一定是内存,空间也可以是磁盘或者其它辅助存储介质。

举个例子,vector 的默认空间分配器allocator,它的调用过程如下:

#include <vector>
#include <iostream>

using namespace std;

int main() {
    vector<int, allocator<int>> v = {1, 2, 3, 4, 5}; // 默认空间分配器allocator<int>
    for (auto i : v) {
        cout << i << " ";
    }
    cout << endl;
    return 0;
}

其中,vector的第二个参数是allocator<int>,表示要使用的空间分配器类型是allocator<int>

可以看到,vecor等容器的空间配置包括了内存分配、对象构造、析构对象、内存回收/释放。那么, 空间分配器的原理到底是什么样的呢?

二、allocator的常用方法

allocator的常用方法包括allocatedeallocate,用于分配和释放内存。用法如下:

#include <iostream>
#include <memory>

using namespace std;

int main() {
  allocator<int> alloc;
  
  int* ptr = alloc.allocate(5);
  for (int i = 0; i < 5; i++) {
    ptr[i] = i;
  }
  
  for (int i = 0; i < 5; i++) {
    cout << ptr[i] << " ";
  }
  cout << endl;
  
  alloc.deallocate(ptr, 5);
  
  return 0;
}

其中,allocate方法分配5个整型空间,并返回指向第一个整型的指针。deallocate方法释放内存,参数是指向首元素的指针和要释放的元素个数。

三、空间分配器的两级实现

很容易想象,为了实现空间配置器,完全可以利用new和delete函数并对其进行封装实现STL的空间配置器。但是,为了最大化提升效率,SGI STL版本并没有简单的这样做,而是采取了一定的措施,实现了更加高效复杂的空间分配策略。

在SGI STL中,将对象的构造分成内存空间配置和对象构造两部分:

内存配置操作: 通过alloc::allocate()实现
​内存释放操作: 通过alloc::deallocate()实现
​对象构造操作: 通过::construct()实现
​对象释放操作: 通过::destroy()实现

考虑到小型内存块所可能造成的内存碎片的问题,SGI STL采用了两级配置器:

第一级:大型内存分配直接使用malloc()和free()函数进行处理。大型内存的分配不仅仅是内存大小的问题,更重要的是内存的使用情况。在程序运行时,很难预先预测到程序需要的内存大小,因此当内存需求较大、难以确定时,就需要使用malloc()和free()进行内存分配和释放。

第二级:小型内存分配采用固定大小的内存池进行分配。一般使用链表来保存已经分配过的、但是现在处于空闲状态的内存块。每次请求内存时,遍历这个链表,找到第一个合适的内存块返回。如果这个链表里没有可用的内存块,则向操作系统请求更多的内存,并继续维护链表。

为了最大化解决内存碎片问题,提升效率,当配置的区块超过 128 bytes 时,视之为 ”足够大“,便调用第一级配置器;当配置区块小于 128 bytes 时,则视之为 ”过小“,便采用内存池方式,而不再求助于第一级配置器。默认使用第二级配置器。

一级空间配置器 allocate

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;
        exit(1):
    }
    return tmp;
}
template <class T>
inline void deallocate(T*buffer) {
    ::operator delete(buffer);
}

二级空间配置器 allocate

public:

  /* __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         // —— _Obj** 二级指针 + volatile         
          = _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; // 编号链的对应编号位置下指向其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; // 返回当前free list的第一个空闲位置
  };

 四、空间分配器 allocators的对外接口

通过实现重载这些成员函数可以实现内存池。SGI STL 中考虑到了内存分配失败的异常处理,内置轻量级内存池(主要用于处理小块内存的分配,应对内存碎片问题)实现, 多线程中的内存分配处理(主要是针对内存池的互斥访问)等。

typedef unsigned int size_t;

allocator::value_type  // 数值类型 typedef T

allocator::pointer // 指针  typedef T*

allocator::const_pointer // 常指针 typedef const T*

allocator::reference // 引用 typedef T&

allocator::const_reference // 常引用 typedef const T&

allocator::size_type // 长度类型 typedef size_t

allocator::difference_type // typedef ptrdiff_t
/*ptrdiff_t是C/C++标准库中定义的一个与机器相关的数据类型。ptrdiff_t类型变量通常用来保存两个指针减法操作的结果。*/

allocator::rebind // 重新绑定
/*  
rebind 
一个嵌套的 nested(嵌套) class template(类模板) 。class rebind<U> 拥有唯一成员 other,那么是一个typedef(定义类型),代表allocator<U> 配置U类型的空间
给定了类型T的分配器Allocator=allocator<T>,现在想根据相同的策略得到另外一个类型U的分配器allocator<U>,那么allocator<U>=allocator<T>::Rebind<U>::other.
*/

allocator::allocator() // default constructor(默认构造函数)

allocator::allocator(const allocator&) // copy constructor(拷贝构造函数)

template<class U> allocator::allocator(const allocator<U>&) // 泛化的拷贝构造函数

allocator::~allocator() // destructor 析构函数

pointer allocator::address(reference x) const//返回对象的地址

const_pointer allocator::address(const_reference x) const // 返回const 对象的地址

pointer allocator::allocate(size_type n,const void * = 0)
/*
配置空间,足以存储n个T对象
参数提示,可以用他增进区域性
*/

void allocator::deallocator(pointer p,size_type n)// 归还先前配置的空间

size_type allocator::max_size() const //返回可成功配置的最大空间

void allocator::construct(ponter p,const T& x) // 等价于new((void *)p) T(x)

void allocator::destroy(pointer p)// 等同于p->~T()

 五、自定义空间分配器

以下是一个简单的空间配置器的实现:

#include <cstdio>
#include <cstdlib>

#define __THROW_BAD_ALLOC printf("out of memory"); exit(1)

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) {
        __THROW_BAD_ALLOC;
    }
    return tmp;
}

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

// 配置一个元素空间,space为元素的大小
template<class T>
class alloc {
public:
    static T* allocate() {
        return _allocate(1, (T*)0);
    }
    static T* allocate(size_t n) {
        return n == 0 ? 0 : _allocate(n, (T*)0);
    }
    static void deallocate(T* buffer) {
        _deallocate(buffer);
    }
    static void construct(T* ptr) {
        new (ptr) T(); // 在已分配空间的地址上构造一个 T 类型的对象
    }
    static void destroy(T* ptr) {
        ptr->~T(); // 手动调用 T 类型对象的析构函数
    }
};

其中,_allocate是分配函数,_deallocate是释放函数。alloc是模板类,它的静态方法allocate用于分配内存,deallocate用于释放内存,construct用于在已分配的内存中构造一个对象,destroy用于销毁一个对象。

以下是一个简单的两级空间分配器的实现,代码如下:

#include <cstdlib>
#include <cstddef>
#include <new>

class Allocator {
public:
    void *allocate(size_t size) {
        // 对于小型内存的分配,以8个字节的字节为块大小进行管理
        if (size <= kMaxBytes) {
            int index = freeListIndex(size);
            obj *list = freeList[index];
            if (list) {
                freeList[index] = list->next;
                return list;
            }
            // 如果没有空闲的内存块,则重新申请以kAlign字节对齐的内存,并把超过需要的部分添加到内存池之中
            else {
                return refill(roundUp(size));
            }
        }
        // 对于大型内存的分配,直接使用malloc进行分配
        else {
            return malloc(size);
        }
    }

    void deallocate(void *p, size_t size) {
        // 对于小型内存的释放,回收到内存池之中
        if (size <= kMaxBytes) {
            int index = freeListIndex(size);
            obj *list = (obj *) p;
            list->next = freeList[index];
            freeList[index] = list;
        }
        // 对于大型内存的释放,直接使用free()函数释放内存
        else {
            free(p);
        }
    }

private:
    static const int kMaxBytes = 128;
    static const int kAlign = 8;
    static const int kNumFreeLists = kMaxBytes / kAlign;

    union obj {
        union obj *next;
        char clientData[1];
    };

    obj *volatile freeList[kNumFreeLists] = {nullptr};

    // 返回大小为nBytes的内存块应该在连接的链表里的位置
    int freeListIndex(size_t nBytes) const {
        return (nBytes + kAlign - 1) / kAlign - 1;
    }

    // 将内存块添加到连接的链表里
    void *refill(size_t nBytes) {
        int nObjs = 20;
        char *chunks = chunkAlloc(nBytes, nObjs);
        if (nObjs == 1) {
            return chunks;
        }
        obj *list = freeList[freeListIndex(nBytes)];
        obj *curObj = (obj *) chunks;
        obj *nextObj = (obj *) (chunks + nBytes);
        for (int i = 0;;) {
            curObj[i].next = &curObj[i+1];
            if (++i == nObjs - 1) {
                curObj[i].next = list;
                break;
            }
        }
        freeList[freeListIndex(nBytes)] = &curObj[0];
        return chunks;
    }

    // 从系统中申请内存,并且将超出部分添加到内存池之中
    char *chunkAlloc(size_t size, int &nObjs) {
        size_t totalBytes = nObjs * size;
        size_t bytesLeft = endFree - startFree;
        char *result;

        if (bytesLeft >= totalBytes) {
            result = startFree;
            startFree += totalBytes;
            return result;
        }
        else if (bytesLeft >= size) {
            nObjs = bytesLeft / size;
            totalBytes = size * nObjs;
            result = startFree;
            startFree += totalBytes;
            return result;
        }
        else {
            size_t bytesToGet = 2 * totalBytes + roundUp(heapSize >> 4);
            if (bytesLeft > 0) {
                obj *list = freeList[freeListIndex(bytesLeft)];
                ((obj *) startFree)->next = list;
                freeList[freeListIndex(bytesLeft)] = (obj *) startFree;
            }
            startFree = (char *) malloc(bytesToGet);
            if (!startFree) {
                obj *list;
                for (int i = size; i <= kMaxBytes; i += kAlign) {
                    list = freeList[freeListIndex(i)];
                    if (list) {
                        startFree = (char *) list;
                        endFree = startFree + i;
                        freeList[freeListIndex(i)] = list->next;
                        return chunkAlloc(size, nObjs);
                    }
                }
                endFree = nullptr;
                startFree = (char *) allocate(bytesToGet);
            }
            heapSize += bytesToGet;
            endFree = startFree + bytesToGet;
            return chunkAlloc(size, nObjs);
        }
    }

    // 对齐,即将n按8对齐
    size_t roundUp(size_t n) const {
        return (n + kAlign - 1) & ~(kAlign - 1);
    }

    // 内存池的起始地址
    static char *startFree;
    // 内存池的结束地址
    static char *endFree;
    // 内存池总共的大小
    static size_t heapSize;
};

char *Allocator::startFree = nullptr;
char *Allocator::endFree = nullptr;
size_t Allocator::heapSize = 0;

两级空间分配器可以根据不同大小的内存要求采用不同的原理,从而更加灵活地分配和管理内存。这种实现方式在许多高效的C++库中使用,例如STL和Boost等。

参考:

c++实现一个简单的空间配置器allocator_swffsdgasdg的博客-CSDN博客
《STL源码剖析》提炼总结:空间配置器(allocator) - 知乎

STL——空间配置器_两片空白的博客-CSDN博客

  • 7
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值