C++boost 库---内存池


“allocator分配器”。它是一种用于管理内存分配的机制
内存池是在真正使用内存之前,先申请分配 一定数量的、大小相等(一般情况下)的内存块留作备用。当 有新的内存需求时,就从内存池中 分出一部分内存块若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。
boost.pool 库基于 简单分隔存储的思想实现了一个 快速、紧凑的内存池库,不仅能够管理大量的对象,还可以 用作 STL 的内存分配器。在某种程度上讲,它近似于一个 小型的垃圾回收机制,在需要 大量分配/释放小对象时其效率很高,而且完全不需要考虑 delete。
pool库包含4个部分: 最简单的pool、分配 类实例object_pool单件内存池 singleton_pool和可用于标准库的 pool_alloc

pool

pool是最简单、最容易使用的内存池类,它可以返回一个简单数据类型(POD:一个类或结构体通过二进制拷贝后还能保持其数据不变,那么它就是一个POD类型)的内存指针
需要注意一点:pool只能作为普通数据类型(如 int、double等)的内存池,不能将其应用于复杂的类和对象,因为它只分配内存不调用构造函数。
除非有特殊要求,否则不必对分配的内存调用 free()、purge_memory()释放, pool 会很好地管理内存
因为 pool 在分配内存失败的时候不会抛出异常,所以实际编写代码时应该检查 malloc( )函数返回的指针,以防止空指针发生错误,不过通常这种情况极少出现。
explicit pool(const size_type nrequested_size,const size_type nnext_size = 32,const size_type nmax_size = 0);
pool构造函数的第一个参数nrequested_size指示每次分配内存块的大小(而不是内存池的大小)。第二个参数nnext_size 说明分配nrequested_size大小的块为几个,默认是32个。(就是会分配32个nrequested_size大小的内存作为内存池大小。当不够时会以当前池大小*2的内存继续分配)
void * malloc ();
从内存池中任意分配一个内存块
void * ordered_malloc();
从内存池中任意分配一个内存块同时合并空闲块列表
void * ordered_malloc(size_type n);
从内存池中连续分配 n 块内存
bool is_from(void * const chunk) const;
测试分配后的内存块是否是从这个内存池分配出去的
void free (void * const chunk);
free( )用来手动释放之前分配的内存块,注意是还给内存池并不是还给系统,这些内存块必须是从这个内存池分配出去的,即 is_from()== true。一般情况下,内存池会自动管理内存分配,不应该调用 free( )除非你认为内存池的空间已经不足,必须释放已经分配的内存。
size_type get_requested_size() const;
获得每次分配的内存块大小。
bool release_memory();
慎用让内存池释放所有未被分配的内存,但已分配的内存块不受其影响
bool purge_memory();
慎用。强制释放 pool持有的所有内存(不管内存块是否被使用)。
~pool();
析构函数调用purge_memory ()。

#include "boost/pool/pool.hpp"
void TestPool()
{
    boost::pool<> pool1(sizeof(int));              //1个可分配 int 的内存池
    int* p = static_cast<int*> (pool1.malloc());   //从内存池分配内存块,必须把void*转为需要的类型
    if (p != nullptr) {
        if (pool1.is_from(p))                      //判断p是否从pool1这个内存池分配的
        {
            std::cout << "p是从pool1内存池中分配的" << std::endl;
            pool1.free(p);                         //释放分配的内存块
        }
    }
    std::cout << pool1.get_requested_size() << std::endl; //4
    for (int i = 0; i < 500; i++)
    {
        int* _p = static_cast<int*> (pool1.ordered_malloc(10)); //连续分配10块内存
        std::cout << _p <<" " << (_p+1)<<".."<< (_p + 9) << std::endl;
    }
}                                                  //内存池对象pool1析构,所有分配的内存都被释放

在这里插入图片描述

object_pool

object_pool 是 pool 的子类,但它使用的是保护继承,因此不能使用 pool 的接口,但它们的基本操作还是很相似的。
object_pool 的模板类型参数 T 指定了 object_pool 要分配的元素类型,要求其析构函数不能抛出异常。一旦在模板中指定了类型, object_pool 实例就不能再用于分配其他类型的对象
malloc()和 free()函数分别分配和释放一块类型为 T * 的内存块,但它们被调用时并不调用类的构造函数和析构函数,也就是说,操作的是一块原始内存块,里面的值是未定义的,因此我们应当尽量少使用 malloc()和 free()
is_from()用来测试内存块的归属,只有本内存池分配的内存才能被free()释放
construct() 是一组函数,有多个参数的重载形式 ( 目前最多支持 3 个参数 , 但可以扩展 ),它先调用 malloc()分配内存 , 再在内存块上使用传入的参数调用类的构造函数 , 返回的是一个已经初始化的对象指针。
destory()先调用对象的析构函数,再用 free()释放内存块
以上函数都不会抛出异常,如果内存分配失败,将返回 0 ( 空指针 )。

class MyClass {
public:
	int a, b, c;
	MyClass(int x = 1, int y = 2, int z = 3) :a(x), b(y), c(z) {
		std::cout << "MyClass 构造函数" << std::endl;
		std::cout << "x:" << x << "y:" << y << "z:" << z << std::endl;
	}
	~MyClass()
	{
		std::cout << "MyClass 析构函数" << std::endl;
	}
};
#include "boost/pool/object_pool.hpp"
void TestObjectPool()
{
	//定义一个分配MyClass对象的内存池
	boost::object_pool<MyClass> obj_pool1;
	MyClass* myClass = obj_pool1.malloc();//分配一个原始内存块,没有初始化
	if (myClass != nullptr)
	{
		if (obj_pool1.is_from(myClass))
		{
			std::cout << "myClass是从obj_pool1内存池中分配的" << std::endl;
		}
	}
	std::cout << myClass->a << std::endl;//一个随机数
	myClass = obj_pool1.construct(4, 5, 6);//调用构造函数,构造一个对象,可以传递参数,这里内存已经分配过了
	std::cout << myClass->a << std::endl;//4
	boost::object_pool<std::string> str_pool;
	std::string* str = str_pool.construct("hello world"); //分配内存 构造对象 传递参数 一气呵成 malloc可以尽量少用
	std::cout << *str << std::endl;
}

在这里插入图片描述

singleton_pool

singleton_pool与 pool 的接口完全一致,可以用它分配简单数据类型( POD )的内存指针,但它是一个“单件”。
singleton_pool成员函数均是静态的,所以不需要声明 singleton_pool的实例,直接用域操作符**“::”来调用静态成员函数即可。因为 singleton_pool 是单件,所以它的生命周期与整个程序同样长,除非手动调用release_memory () 或 purge_memory (),否则singleton_pool 不会自动释放所占用的内存。除了述两点, singleton _ pool 的用法与 pool 完全相同。
singleton_pool默认使用 boost.thread 库提供线程安全保证,所以需要将它
链接 boost_thread 库**,如果不使用多线程,那么可以在头文件前定义宏BOOST_POOL_NO_MT
singleton_pool为单例类,是对pool的加锁封装,适用于多线程环境,其中所有函数都是静态类型。它的模版参数有6个:
Tag:标记而已,无意义;
RequestedSize:内存块的大小;
UserAllocator分配子,默认是default_user_allocator_new_delete;
Mutex锁机制,默认值最终依赖于系统环境,linux下是pthread_mutex,它是对pthread_mutex_t的封装;
NextSize:内存不足的时候,申请的内存块数量,默认是32;
MaxSize:指定在任何单个分配请求中要分配的最大块数,默认为0。

#include "boost/pool/singleton_pool.hpp"
typedef boost::singleton_pool<struct pool_tag, sizeof(int) * 1024, boost::default_user_allocator_new_delete, boost::details::pool::default_mutex, 1024> spl; //将单例池改名为sql
void TestSingleton_pool()
{
    int* p = (int*)spl::malloc();
    if (spl::is_from(p))//用作用域访问是因为内存池用的是静态成员函数
    {
        std::cout << "" << std::endl;
    }
    *p = 10;
    //Sleep(500);
    //spl::purge_memory();
}

不调用spl::purge_memory()与调用spl::purge_memory()的内存曲线对比:
在这里插入图片描述

pool_alloc

本人看的是这个博主的,感觉写的不错
https://zhuanlan.zhihu.com/p/349786437
标准库提供了多种allocator类模板。比如:
new_allocator,该allocator只是简单的封装了::operator new和::operator delete,也是目前默认的allocator。
malloc_allocator,该allocator也只是简单的封装了malloc和free
debug_allocator,该allocator封装了任意一种其它类型的allocator(称之为A)。它会不断地向A申请相比于之前稍微更大一些的内存,并用额外的内存存储size信息。在deallocate中,会用一个assert()来检查被存储的size信息和将要被释放的内存的size是否一致。
__pool_alloc,一个高性能的内存池allocator。
以及**__mt_alloc,bitmap_allocator,throw_allocator**等适用于不同场景的allocator。本文将重点介绍__pool_alloc。

__poll_alloc 原理简介
__pool_alloc使用的是一个单池,该单池会被__pool_alloc的不同实例所共享。__pool_aloc适用于为size小于128 byte的类型进行的内存申请,如果某个类型的size不是8的整数倍,那么为其申请的内存会被向上取整到8的倍数。为大于128 bytes的类型执行的内存申请将被直接转发给::operator new。所有的已分配的内存最终都会被回收,并用一个数组进行管理。
__pool_alloc的父类__pool_alloc_base是一个**“非”模板类,上述数组就是其所包含的一个static的、长度为16的数组。被回收的内存会被以单链表的形式挂载到这个数组中,根据被回收内存的大小,会为其在数组中选择合适的位置,并在该位置存储一个指向链表起点的指针**。(整体而言是hash桶的组织方式)。
在这里插入图片描述

#include <iostream>
#include <memory>
#include<ext/pool_allocator.h>


struct S_8
{
	double a[1];
};

struct S_16
{
	double a[2];
};

struct S_40
{
	double a[5];
};

struct S_120
{
	double a[15];
};


int main() {
	//这里用的是gcc的 实际上标准库也是有的 后续讲一下区别
	__gnu_cxx::__pool_alloc<S_8> S_8_allo;
	__gnu_cxx::__pool_alloc<S_16> S_16_allo;
	__gnu_cxx::__pool_alloc<S_120> S_120_allo;
	__gnu_cxx::__pool_alloc<S_40> S_40_allo;

	auto p_S_16 = S_16_allo.allocate(1);
	auto p_S_16_1 = S_16_allo.allocate(1);

	std::cout << (char*)p_S_16_1 - (char *)p_S_16 << std::endl;  // should output 16 

	auto p_S_8 = S_8_allo.allocate(1);
	std::cout << (char *)p_S_8 - (char*)p_S_16 << std::endl;     //should output 320

	auto p_S_120 = S_120_allo.allocate(1);
	std::cout << (char *)p_S_120 - (char*)p_S_16 << std::endl;  // should output 480 

	auto p_S_120_1 = S_120_allo.allocate(1);
	std::cout << (char *)p_S_120_1 - (char*)p_S_16 << std::endl;   // a unpredictale value

	auto p_S_40 = S_40_allo.allocate(1);
	std::cout << (char *)p_S_40 - (char*)p_S_16 << std::endl;   // shoud output 600

	S_16_allo.deallocate(p_S_16, 1);
	S_16_allo.deallocate(p_S_16_1, 1);
	S_8_allo.deallocate(p_S_8, 1);
	S_120_allo.deallocate(p_S_120, 1);
	S_120_allo.deallocate(p_S_120_1, 1);
	S_40_allo.deallocate(p_S_40, 1);

	return 0;
}

代码中引入struct S_8,struct S_16,struct S_40和struct S_120,只是单纯的为了引入大小为8,16,40和120 byte的类型。后续图例中的_S_heap_size记录的是当前为内存池所分配的总的内存的大小,_S_start_free记录的是由内存池所分配的内存块中未被利用、也未被归入到某个链表中的内存的起点,_S_end_free所记录的则是这块内存的终点,它们都是父类__pool_alloc_base的static成员。

内存分配
编译并运行上述代码,当代码运行到第34行后,已经分别为这4种类型创建了各自的allocator,但是请记住它们将共享同一个内存池(单池),此时内存池所管理的内存为零(_S_heap_size = 0, _S_start_free = 0, S_end_free = 0)。

在第34行代码运行期间,为了响应内存分配的请求,__pool_alloc会先分配一块大小为40 * sizeof(S_16) = 640 bytes的内存,并将前16个byte返回给p_S_16,然后将前20 * sizeof(S_16) = 320 byte中扣除前16 byte的内存(304 bytes)挂载到长度为16的static数组中的对应位置,其间会将这304(19*16) bytes的内存分成19个链表中前后相连的node。图示如下:
在这里插入图片描述
当程序运行第35行代码时,由于它发现在内存池中对应于类型大小为16 byte的链表不为空,因此它只需要从这一链表中直接取用16 byte内存(链表中第一个node),并将链表起点后移一位即可,
在这里插入图片描述
由于p_S_16和p_S_16所指的内存是连续的,因此程序第36行将会输出16。

在程序运行第37行时,由于它发现对应于8 byte的链表为空,因此会试着从_S_start_free和_S_end_free中找出20 * 8 byte的内存给相应链表,并将链表中第一个node的内存分配给p_S_8,然后后移链表起点,此时状态如下:
在这里插入图片描述
如图所示,第40行代码的输出结果将是320。

在程序运行第42行代码时,虽然介于_S_start_free和_S_end_free之间的内存小于120 * 20 byte,但是依然大于120 byte并小于2 * 120 byte,因此__pool_alloc会将其中的第一个120 byte直接赋值给p_S_120,此时的状态如下:
在这里插入图片描述
显然第43行代码将输出480。

第45行代码试图再次申请一块大小为120 byte的内存,但是此时介于_S_start_free和_S_end_free之间的内存并不足以满足这一要求,因此需要开辟新的内存块。不过,在分配新的内存块之前,我们应该尽可能的利用剩余的、介于_S_start_free和_S_end_free之间的这一块内存。方法就是将其挂载到适合的链表中(把40挂载到链表当中),然后再去分配新的内存块(新内存块的尺寸将是40*120加上(_S_heap_size >> 4)之后向上取整到8的倍数的值,也就是:4840 bytes)。随后采入相同的方式,将新内存块中前20120 byte的内存挂载到120对应的链表下,然后将第一个node赋值给p_S_120_1,并后移链表的起点。此时的状态如下:
在这里插入图片描述
由于p_S_120_1的起点在新的内存块中,因此第46行代码的结果将是一个不可预知的值。

当程序执行第48行代码时,由于40byte对应的链表的不是空的,因此会直接从链表中得到所需的内存,并将链表起点后移一位(结果是使该链表变为空),此时状态如下:
在这里插入图片描述
由于此时p_S_40和p_S_16所指向的内存处在同一个内存块中,因此第49行代码将会输出600。

以上过程大致涵盖了__pool_alloc中各种内存分配的情况,后续新的内存申请将遵照相同的形式进行。下面来看内存回收再利用的情况。

内存回收再利用
作为使用了内存池的allocator,__pool_alloc并不会将其分配的内存释放掉,而是会将回收内内存放到与其对应的链表中。由于过程相对简单,此处只将其最终结果展示如下(120 byte对应的链表有两条箭头):
在这里插入图片描述
__pool_alloc提供了一种简单、普适且高效的内存池实现机制,能够很容易地被用于std::vector一类的容器。但是该实现并没有提供主动释放内存池中内存的接口,因此这部分内存一旦被分配,就会一致持续存在,直到程序结束。这会导致对于某些只有中期某个时间点会占用大量内存的程序,其内存占用会一直保持较高的占用率,这或许是我们所希望能够尽量避免的,但是增加能存池大小的动态调整的功能,无疑会大大增加该实现的复杂度,又会降低该实现的普适性。

gn4.9中__gnu_cxx::__pool_alloc详解和std::allocator的比较
//todo

  • 7
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值