当你将元素加入容器中,容器必须分配更多内存以保存这些元素,于是它们向其模板参数 Allocator 发出申请,该模板参数往往被另名为(aliased to)allocator_type。甚至你将 chars 添加到 string class 也是如此,因为 string 也算是一个正规 STL 容器。

template <class T, class Allocator=allocator<T>>
class vector;

template <class T, class Allocator=allocator<T>>
class list;

template <class T, class Allocator=allocator<T>>
class deque;

每个元素类型为 T 的容器(container-of-T) 的 Allocator 模板参数默认为 allocator<T>。其接口只有大约 20 个 public 声明,包括嵌套的(nested) typedefs 和成员函数。最终要的两个函数是:

T *allocate(size_type n, const void *hint = 0);
void deallocate(T *p, size_type n);

n 指的是客户申请的元素个数,不是指空间数量(字节数)

这些空间都是通过调用 ::operator new 获得,但何时调用以及多么频繁调用,并无具体指定

最容易满足需求的做法就是每当容器需要内存就调用 operator new, 每当容器释放内存就调用 operator delete。这种做法比起分配大块内存并缓存(caching)然后徐徐小块使用当然慢,优势则是可以在极大范围的硬件和操作系统上有效运作

  • __gnu_cxx::new_allocator

实现出简朴的 operator new 和 operator delete 语义

template<typename _Tp>
class new_allocator
{
    ....
    pointer allocate(size_type __n, const void*=0)
    {
        ....
        return static_cast<_Tp*>(operator new(__n * sizeof(_Tp)));
    }
    
    void deallocate(pointer __p, size_type)
    {
        ::operator delete(__p);
    }
};
  • __gnu_cxx::malloc_allocator
template <typename _Tp>
class malloc_allocator
{
    pointer allocate(size_type __n, const void*=0)
    {
        ...
        pointer __ret = ...(std::malloc(__n * sizeof(_Tp)));
    }
    
    void deallocate(pointer __p, size_type)
    {
        std::free(...(__p));
    }
};

另一种做法就是使用智能型 allocator, 将分配所得的内存加以缓存(cache、内存池)。这种额外机制可以数种形式呈现:

可以是个 bitmap index, 用以索引至一个以 2 的指数倍成长的篮子(exponentially increasing power-of-two-sized buckets)

也可以是个相较之下比较简易的 fixed-size pooling cache

这里所说的 cache 被程序内的所有容器共享,而 operator new 和 operator delete 不经常被使用,这可带来速度上的优势(底层malloc也是一个复杂的内存池,速度优势并不明显,主要是减少了cookie的使用)。使用这类技巧的 allocators 包括:

  • __gnu_cxx::bitmap_allocator

一个高效能 allocator,使用 bit-map 追踪被使用和未被使用(used and unused)的内存块

  • __gnu_cxx::pool_allocator
  • __gnu_cxx::__mt_alloc

Class allocator 只拥有 typedef, constructor 和 rebind 等成员。它继承子一个 high-speed extension allocators。也因此,所有分配和归还(allocation and deallocation) 都取决于该 base class, 而这个 base class 也许是终端用户无法触碰和操作的(user-configurable)

image.png

很难挑选出某个分配策略说它能提供最大利益而不至于令某些行为过度劣势。事实上,就算要挑选何种典型动作以测量速度,都是一种困难

GNU C++ 提供三项综合测试(three synthetic benchmarks)用以完成 C++ allocators 之间的速度对比:

Insertion, 经过多次 insertions 后各种 STL 容器将拥有某些及大量。分别测试循序式(sequence)和关联式(assiciative)容器
多线程环境中的 insertion and erasure, 这个测试展示 allocator 归还内存的能力,以及测量线程之间对内存的竞争
A threaded producer/consumer model, 分分别测试循序式(sequence)和关联式(assiciative)容器

令两个智能型 allocator:

  • __gnu_cxx::debug_allocator(用处极少)

这是一个外覆器(wrapper), 可包覆于任何 allocator 之上,它把客户的申请量添加一些,然后由 allocator 回应,并以那一小块额外内存放置 size 信息。一旦 deallocate() 收到一个 pointer, 就会检查 size 并以 assert() 保证吻合

  • __gnu_cxx::array::allocator

允许分配一已知固定大小(known and fixed size)的内存块,内存来自 std::array objects(原生的静态数组)。用上这个 allocator, 大小固定的容器(包括 std::string) 就无需再调用 ::operator new 和 operator delete。 这就允许我们使用 STL abstractions 而无需再运行期 "添乱"、增加开销。甚至再 program startup 情况下也可使用(动态分配还没准备好的情况下)


TianSong
737 声望139 粉丝

阿里山神木的种子在3000年前已经埋下,今天不过是看到当年注定的结果,为了未来的自己,今天就埋下一颗好种子吧