c++ stl 内存配置器

在STL中,Memory Allocator 处于最底层的位置,为一切的 Container 提供存储服务,是一切其他组件的基石。对于一般使用 STL 的用户而言,Allocator 是不可见的,如果需要对 STL 进行扩展,如编写自定义的容器,就需要调用 Allocator 的内存分配函数进行空间配置。

在C++中,一个对象的内存配置和释放一般都包含两个步骤,对于内存的配置,首先是调用operator new来配置内存,然后调用对象的类的构造函数进行初始化;而对于内存释放,首先是调用析构函数,然后调用 operator delete进行释放。 如以下代码:

  1. class Foo { ... };

  2.  

    Foo* pf = new Foo;

  3.  

    ...

  4.  

    delete pf;

Allocator 的作用相当于operator new 和operator delete的功能,只是它考虑得更加细致周全。SGI STL 中考虑到了内存分配失败的异常处理,内置轻量级内存池(主要用于处理小块内存的分配,应对内存碎片问题)实现, 多线程中的内存分配处理(主要是针对内存池的互斥访问)等,本文就主要分析 SGI STL 中在这三个方面是如何处理的。在介绍着三个方面之前,我们先来看看 Allocator的标准接口。

1. Allocator 的标准接口

在 SGI STL 中,Allocator的实现主要在文件 alloc.h 和 stl_alloc.h 文件中。根据 STL 规范,Allocator 需提供如下的一些接口(见 stl_alloc.h 文件的第588行开始的class template allocator):

  1. // 标识数据类型的成员变量,关于中间的6个变量的涵义见后续文章(关于Traits编程技巧)

  2.  

    typedef alloc _Alloc;

  3.  

    typedef size_t size_type;

  4.  

    typedef ptrdiff_t difference_type;

  5.  

    typedef _Tp* pointer;

  6.  

    typedef const _Tp* const_pointer;

  7.  

    typedef _Tp& reference;

  8.  

    typedef const _Tp& const_reference;

  9.  

    typedef _Tp value_type;

  10.  

    template <class _Tp1> struct rebind {

  11.  

    typedef allocator<_Tp1> other;

  12.  

    }; // 一个嵌套的class template,仅包含一个成员变量 other

  13.  

    // 成员函数

  14.  

    allocator() __STL_NOTHROW {} // 默认构造函数,其中__STL_NOTHROW 在 stl_config.h中定义,要么为空,要么为 throw()

  15.  

    allocator(const allocator&) __STL_NOTHROW {} // 拷贝构造函数

  16.  

    template <class _Tp1> allocator(const allocator<_Tp1>&) __STL_NOTHROW {} // 泛化的拷贝构造函数

  17.  

    ~allocator() __STL_NOTHROW {} // 析构函数

  18.  

    pointer address(reference __x) const { return &__x; } // 返回对象的地址

  19.  

    const_pointer address(const_reference __x) const { return &__x; } // 返回const对象的地址

  20.  

    _Tp* allocate(size_type __n, const void* = 0) {

  21.  

    return __n != 0 ? static_cast<_Tp*>(_Alloc::allocate(__n * sizeof(_Tp))) : 0;

  22.  

    // 配置空间,如果申请的空间块数不为0,那么调用 _Alloc 也即 alloc 的 allocate 函数来分配内存,

  23.  

    } //这里的 alloc 在 SGI STL 中默认使用的是__default_alloc_template<__NODE_ALLOCATOR_THREADS, 0>这个实现(见第402行)

  24.  

    void deallocate(pointer __p, size_type __n) { _Alloc::deallocate(__p, __n * sizeof(_Tp)); } // 释放空间

  25.  

    size_type max_size() const __STL_NOTHROW // max_size() 函数,返回可成功配置的最大值

  26.  

    { return size_t(-1) / sizeof(_Tp); } //这里没看懂,这里的size_t(-1)是什么意思?

  27.  

    void construct(pointer __p, const _Tp& __val) { new(__p) _Tp(__val); } // 调用 new 来给新变量分配空间并赋值

  28.  

    void destroy(pointer __p) { __p->~_Tp(); } // 调用 _Tp 的析构函数来释放空间

在SGI STL中设计了如下几个空间分配的 class template:

  1. template <int __inst> class __malloc_alloc_template // Malloc-based allocator. Typically slower than default alloc

  2.  

    typedef __malloc_alloc_template<0> malloc_alloc

  3.  

    template<class _Tp, class _Alloc> class simple_alloc

  4.  

    template <class _Alloc> class debug_alloc

  5.  

    template <bool threads, int inst> class __default_alloc_template // Default node allocator.

  6.  

    typedef __default_alloc_template<__NODE_ALLOCATOR_THREADS, 0> alloc

  7.  

    typedef __default_alloc_template<false, 0> single_client_alloc

  8.  

    template <class _Tp>class allocator

  9.  

    template<>class allocator<void>

  10.  

    template <class _Tp, class _Alloc>struct __allocator

  11.  

    template <class _Alloc>class __allocator<void, _Alloc>

其中 simple_alloc , debug_alloc , allocator 和 __allocator 的实现都比较简单,都是对其他适配器的一个简单封装(因为实际上还是调用其他配置器的方法,如 _Alloc::allocate )。而真正内容比较充实的是 __malloc_alloc_template 和 __default_alloc_template 这两个配置器,这两个配置器就是 SGI STL 配置器的精华所在。其中 __malloc_alloc_template 是SGI STL 的第一层配置器,只是对系统的 malloc , realloc 函数的一个简单封装,并考虑到了分配失败后的异常处理。而 __default_alloc_template 是SGI STL 的第二层配置器,在第一层配置器的基础上还考虑了内存碎片的问题,通过内置一个轻量级的内存池。下文将先介绍第一级配置器的异常处理机制,然后介绍第二级配置器的内存池实现,及在多线程环境下内存池互斥访问的机制。

2. SGI STL 内存分配失败的异常处理

内存分配失败一般是由于out-of-memory(oom),SGI STL 本身并不会去处理oom问题,而只是提供一个 private 的函数指针成员和一个 public 的设置该函数指针的方法,让用户来自定义异常处理逻辑:

  1. private:

  2.  

    #ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG

  3.  

    static void (* __malloc_alloc_oom_handler)(); // 函数指针

  4.  

    #endif

  5.  

    public:

  6.  

    static void (* __set_malloc_handler(void (*__f)()))() // 设置函数指针的public方法

  7.  

    {

  8.  

    void (* __old)() = __malloc_alloc_oom_handler;

  9.  

    __malloc_alloc_oom_handler = __f;

  10.  

    return(__old);

  11.  

    }

如果用户没有调用该方法来设置异常处理函数,那么就不做任何异常处理,仅仅是想标准错误流输出一句out of memory并退出程序(对于使用new和C++特性的情况而言,则是抛出一个 std::bad_alloc() 异常), 因为该函数指针的缺省值为0,此时对应的异常处理是 __THROW_BAD_ALLOC :

  1. // line 152 ~ 155

  2.  

    #ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG

  3.  

    template <int __inst>

  4.  

    void (* __malloc_alloc_template<__inst>::__malloc_alloc_oom_handler)() = 0;

  5.  

    #endif

  6.  

    // in _S_oom_malloc and _S_oom_realloc

  7.  

    __my_malloc_handler = __malloc_alloc_oom_handler;

  8.  

    if (0 == __my_malloc_handler) { __THROW_BAD_ALLOC; }

  9.  

    // in preprocess, line 41 ~ 50

  10.  

    #ifndef __THROW_BAD_ALLOC

  11.  

    # if defined(__STL_NO_BAD_ALLOC) || !defined(__STL_USE_EXCEPTIONS)

  12.  

    # include <stdio.h>

  13.  

    # include <stdlib.h>

  14.  

    # define __THROW_BAD_ALLOC fprintf(stderr, "out of memory\n"); exit(1)

  15.  

    # else /* Standard conforming out-of-memory handling */

  16.  

    # include <new>

  17.  

    # define __THROW_BAD_ALLOC throw std::bad_alloc()

  18.  

    # endif

  19.  

    #endif

SGI STL 内存配置失败的异常处理机制就是这样子了,提供一个默认的处理方法,也留有一个用户自定义处理异常的接口。

3. SGI STL 内置轻量级内存池的实现

第一级配置器 __malloc_alloc_template 仅仅只是对 malloc 的一层封装,没有考虑可能出现的内存碎片化问题。内存碎片化问题在大量申请小块内存是可能非常严重,最终导致碎片化的空闲内存无法充分利用。SGI 于是在第二级配置器 __default_alloc_template 中 内置了一个轻量级的内存池。 对于小内存块的申请,从内置的内存池中分配。然后维护一些空闲内存块的链表(简记为空闲链表,free list),小块内存使用完后都回收到空闲链表中,这样如果新来一个小内存块申请,如果对应的空闲链表不为空,就可以从空闲链表中分配空间给用户。具体而言SGI默认最大的小块内存大小为128bytes,并设置了128/8=16 个free list,每个list 分别维护大小为 8, 16, 24, …, 128bytes 的空间内存块(均为8的整数倍),如果用户申请的空间大小不足8的倍数,则向上取整。

SGI STL内置内存池的实现请看 __default_alloc_template 中被定义为 private 的这些成员变量和方法(去掉了部分预处理代码和互斥处理的代码):

  1. private:

  2.  

    #if ! (defined(__SUNPRO_CC) || defined(__GNUC__))

  3.  

    enum {_ALIGN = 8}; // 对齐大小

  4.  

    enum {_MAX_BYTES = 128}; // 最大有内置内存池来分配的内存大小

  5.  

    enum {_NFREELISTS = 16}; // _MAX_BYTES/_ALIGN // 空闲链表个数

  6.  

    # endif

  7.  

    static size_t _S_round_up(size_t __bytes) // 不是8的倍数,向上取整

  8.  

    { return (((__bytes) + (size_t) _ALIGN-1) & ~((size_t) _ALIGN - 1)); }

  9.  

    __PRIVATE:

  10.  

    union _Obj { // 空闲链表的每个node的定义

  11.  

    union _Obj* _M_free_list_link;

  12.  

    char _M_client_data[1]; };

  13.  

    static _Obj* __STL_VOLATILE _S_free_list[]; // 空闲链表数组

  14.  

    static size_t _S_freelist_index(size_t __bytes) { // __bytes 对应的free list的index

  15.  

    return (((__bytes) + (size_t)_ALIGN-1)/(size_t)_ALIGN - 1);

  16.  

    }

  17.  

    static void* _S_refill(size_t __n); // 从内存池中申请空间并构建free list,然后从free list中分配空间给用户

  18.  

    static char* _S_chunk_alloc(size_t __size, int& __nobjs); // 从内存池中分配空间

  19.  

    static char* _S_start_free; // 内存池空闲部分的起始地址

  20.  

    static char* _S_end_free; // 内存池结束地址

  21.  

    static size_t _S_heap_size; // 内存池堆大小,主要用于配置内存池的大小

函数 _S_refill 的逻辑是,先调用 _S_chunk_alloc 从内存池中分配20块小内存(而不是用户申请的1块),将这20块中的第一块返回给用户,而将剩下的19块依次链接,构建一个free list。这样下次再申请同样大小的内存就不用再从内存池中取了。有了 _S_refill ,用户申请空间时,就不是直接从内存池中取了,而是从 free list 中取。因此 allocate 和 reallocate 在相应的free list为空时都只需直接调用 _S_refill 就行了。其中 _S_refill 和 _S_chunk_alloc 这两个函数是该内存池机制的核心。 __default_alloc_template 对外提供的 public 的接口有 allocate , deallocate 和 reallocate 这三个,其中涉及内存分配的 allocate 和 reallocate 的逻辑思路是,首先看申请的size(已round up)对应的free list是否为空,如果为空,则调用 _S_refill 来分配,否则直接从对应的free list中分配。而 deallocate 的逻辑是直接将空间插入到相应free list的最前面。

这里默认是依次申请20块,但如果内存池空间不足以分配20块时,会尽量分配足够多的块,这些处理都在 _S_chunk_alloc 函数中。该函数的处理逻辑如下(源代码这里就不贴了):

1) 能够分配20块

从内存池分配20块出来,改变 _S_start_free 的值,返回分配出来的内存的起始地址

2) 不足以分配20块,但至少能分配一块

分配经量多的块数,改变 _S_start_free 的值,返回分配出来的内存的起始地址

3) 一块也分配不了

首先计算新内存池大小 size_t __bytes_to_get = 2 * __total_bytes + _S_round_up(_S_heap_size >> 4) 
将现在内存池中剩余空间插入到适当的free list中 
调用 malloc 来获取一大片空间作为新的内存池: 
– 如果分配成功,则调整 _S_end_free 和 _S_heap_size 的值,并重新调用自身,从新的内存池中给用户分配空间; – 否则,分配失败,考虑从比当前申请的空间大的free list中分配空间,如果无法找不到这样的非空free list,则调用第一级配置器的allocate,看oom机制能否解决问题

SGI STL的轻量级内存池的实现就是酱紫了,其实并不复杂。

4. SGI STL 内存池在多线程下的互斥访问

最后,我们来看看SGI STL中如何处理多线程下对内存池互斥访问的(实际上是对相应的free list进行互斥访问,这里访问是只需要对free list进行修改的访问操作)。在SGI的第二级配置器中与内存池互斥访问相关的就是 _Lock 这个类了,它仅仅只包含一个构造函数和一个析构函数,但这两个函数足够了。在构造函数中对内存池加锁,在析构函数中对内存池解锁:

  1. //// in __default_alloc_template

  2.  

    # ifdef __STL_THREADS

  3.  

    static _STL_mutex_lock _S_node_allocator_lock; // 互斥锁变量

  4.  

    # endif

  5.  

    class _Lock {

  6.  

    public:

  7.  

    _Lock() { __NODE_ALLOCATOR_LOCK; }

  8.  

    ~_Lock() { __NODE_ALLOCATOR_UNLOCK; }

  9.  

    };

  10.  

    //// in preprocess

  11.  

    #ifdef __STL_THREADS

  12.  

    # include <stl_threads.h> // stl 的线程,只是对linux或windows线程的一个封装

  13.  

    # define __NODE_ALLOCATOR_THREADS true

  14.  

    # ifdef __STL_SGI_THREADS

  15.  

    # define __NODE_ALLOCATOR_LOCK if (threads && __us_rsthread_malloc) \

  16.  

    { _S_node_allocator_lock._M_acquire_lock(); } // 获取锁

  17.  

    # define __NODE_ALLOCATOR_UNLOCK if (threads && __us_rsthread_malloc) \

  18.  

    { _S_node_allocator_lock._M_release_lock(); } // 释放锁

  19.  

    # else /* !__STL_SGI_THREADS */

  20.  

    # define __NODE_ALLOCATOR_LOCK \

  21.  

    { if (threads) _S_node_allocator_lock._M_acquire_lock(); }

  22.  

    # define __NODE_ALLOCATOR_UNLOCK \

  23.  

    { if (threads) _S_node_allocator_lock._M_release_lock(); }

  24.  

    # endif

  25.  

    #else /* !__STL_THREADS */

  26.  

    # define __NODE_ALLOCATOR_LOCK

  27.  

    # define __NODE_ALLOCATOR_UNLOCK

  28.  

    # define __NODE_ALLOCATOR_THREADS false

  29.  

    #endif

由于在 __default_alloc_template 的对外接口中,只有 allocate 和 deallocate 中直接涉及到对free list进行修改的操作,所以在这两个函数中,在对free list进行修改之前,都要实例化一个_Lock 的对象 __lock_instance ,此时调用构造函数进行加锁,当函数结束时,的对象 __lock_instance 自动析构,释放锁。这样,在多线程下,可以保证free list的一致性。

原文地址:https://www.cnblogs.com/xietianjiao/p/12344098.html

时间: 2024-11-05 23:27:55

c++ stl 内存配置器的相关文章

SGI STL内存配置器存在内存泄漏吗?

阅读了SGI的源码后对STL很是膜拜,很高质量的源码,从中学到了很多.温故而知新!下文中所有STL如无特殊说明均指SGI版本实现. STL 内存配置器 STL对内存管理最核心部分我觉得是其将C++对象创建过程分解为构造.析构和内存分配.释放两类操作分离开来!摆脱了对频繁调用new或malloc函数想操作系统申请空间而造成的低效.其中析构操作时对具有non-trival.trival 析构函数的class区别对待也提高了效率.SGI 的两级配置器结构属于锦上添花. STL内存配置器有没有内存泄漏?

SGI STL内存配置器(一):内存泄漏?

阅读了Alexander大神的SGI STL源码,膜拜,很高质量的源码,获益匪浅.温故而知新!下文中所有STL如无特殊说明均指SGI版本实现. STL 内存配置器 STL对内存管理最核心部分我觉得是它将C++对象创建过程分解为构造.析构和内存分配.释放!摆脱了由于频繁调用new或malloc函数向操作系统申请空间而造成的低效.其中析构操作时对具有non-trival.trival 析构函数的class区别对待也提高了效率.至于SGI的两级配置器结构则属于锦上添花的类型. STL两级结构的内存配置

STL内存配置器

一.STL内存配置器的总体设计结构 1.两级内存配置器:SGI-STL中设计了两级的内存配置器,主要用于不同大小的内存分配需求,当需要分配的内存大小大于128bytes时, 使用第一级配置器,否则使用第二级配置器:对于小块的内存的分配使用第二级配置器使用分配与释放内存块的效率更高,时间复杂度为O(1): 2.两级配置器的优点: (1)使用两级配置器主要是为了避免太多的小块的内存申请导致内存空间中的内存碎片问题: (2)使用第二级配置器也可以避免太多小块内存分配导致的内存空间浪费问题,因为在申请内

自己动手实现STL 01:内存配置器的实现(stl_alloc.h)

一.前言 在STL中,容器是其中的重中之重,基本的STL中的算法,仿函数等都是围绕着容器实现的功能.而,内存配置器,是容器的实现的基础.所以,我第一次要去编写便是内存配置器的实现.在STL中,内存配置器的实现是在stl_alloc.h中. 二.配置器原理简要介绍 在SGI STL中配置分为两级,第一级配置器和第二级配置器.两者关系如下: 图1:第一级配置器和第二级配置器 在SGI STL中内存的配置器分为两级,第一级配置器和第二级配置器.第一级配置器就是,直接调用系统的malloc分配内存.对于

STL容器内存配置器

本系列文章更多是笔记形式,希望能在总结过程中将一些东西理顺.难免出错,欢迎指正. STL六大功能组件: 1.容器(containers):2.算法(algorithm):3.迭代器(iterator):4.仿函数(functors):5.配接器(adapters):6.配置器(allcators). 各个功能组件间存在交互关系,这里不涉及这些内容,本篇文章讨论容器的内存配置. 首先,容器用来存放数据,那么存放数据之前必须向系统申请内存资源.我们知道c++中通常用(::operator new/:

STL空间配置器那点事

STL简介 STL(Standard Template Library,标准模板库),从根本上说,STL是一些“容器”的集合,这些“容器”有list,vector,set,map等,STL也是算法和其他一些组件的集合. 谈及组件,那么我们就首先来简单谈下STL六大组件,其相关的设计模式使用,以及各组件之间的协作关系. 设计模式一览 六大组件简单介绍 1. 空间配置器:内存池实现小块内存分配,对应到设计模式--单例模式(工具类,提供服务,一个程序只需要一个空间配置器即可),享元模式(小块内存统一由

内存配置器

stl中内存配置器分为两级:第一级配置对象超过128B的内存,第二级配置对象小于128B的内存,stl默认采用第二级内存配置器,因为如果对象大于128B,则第二级内存配置器会自动调用第一级内存配置器. 第一级内存配置器简单的对malloc,realloc,free的封装 第二级内存配置器采用内存池管理内存,并用16个链表管理回收到的空闲内存块,当分配内存时,首先找到合适位置的空闲链表,若链表上有空闲内存块,则直接摘下空闲内存块供使用;否则就需要向内存池申请新的空闲内存块,若内存池都没有剩余内存了

stl空间配置器线程安全问题补充

摘要 在上一篇博客<STL空间配置器那点事>简单介绍了空间配置器的基本实现 两级空间配置器处理,一级相关细节问题,同时简单描述了STL各组件之间的关系以及设计到的设计模式等. 在最后,又关于STL空间配置的效率以及空间释放时机做了简单的探讨. 线程安全问题概述 为什么会有线程安全问题? 认真学过操作系统的同学应该都知道一个问题. first--进程是系统资源分配和调度的基本单位,是操作系统结构的基础,是一个程序的运行实体,同时也是一个程序执行中线程的容器 seconed--进程中作为资源分配基

STL空间配置器

1.什么是空间配置器? 空间配置器负责空间配置与管理.配置器是一个实现了动态空间配置.空间管理.空间释放的class template.以内存池方式实现小块内存管理分配.关于内存池概念可以点击:内存池. 2.STL空间配置器产生的缘由 在软件开发,程序设计中,我们不免因为程序需求,使用很多的小块内存(基本类型以及小内存的自定义类型).在程序中动态申请,释放.这个过程过程并不是一定能够控制好的,于是乎出现以下问题: 问题1:就出现了内存碎片问题.(ps外碎片问题) 问题2:一直在因为小块内存而进行