C++内存池技术分析

杀马特。学长 韩版系。学妹 提交于 2019-12-25 04:45:42

C++内存池技术分析

为了防止应用程序频繁的使用分配和释放内存,很多程序都会使用内存池技术来预先分配内存,使用内存的时候再从内存池中取出内存。

内存池的好处是优化内存分配,使得内存碎片减少,不过使用起来肯定会增加复杂程度。在C++中有两种办法来提供内存池管理方案:

  1. newdelete
  2. std::allocator

通过这两种方式,我们可以手动管理内存。

1. new 和 delete

1.1 内存分配的原理

在C++中new和delete不是一个函数,而是一个操作表达式,我们使用的时候如下:

class CData
{
public:
	CData(int d) : Data(d) {}

private:
	int Data;
};
void Mem()
{
	CData* data = new CData(100);
}

反汇编的结果如下:

	CData* data = new CData(100);
00E619A7  push        4  
00E619A9  call        operator new (0E61361h)  
00E619AE  add         esp,4  
00E619B1  mov         dword ptr [ebp-0ECh],eax  
00E619B7  mov         dword ptr [ebp-4],0  
00E619BE  cmp         dword ptr [ebp-0ECh],0  
00E619C5  je          Mem+89h (0E619E9h)  
00E619C7  push        4  
00E619C9  mov         ecx,dword ptr [ebp-0ECh]  
00E619CF  call        CData::__autoclassinit2 (0E61005h)  
00E619D4  push        64h  
00E619D6  mov         ecx,dword ptr [ebp-0ECh]  
00E619DC  call        CData::CData (0E6129Eh)  

这里有两个操作:

  1. 调用call operator new (0E61361h),分配内存函数。
  2. 调用call CData::CData (0E6129Eh)构造函数。

对于operator new (0E61361h)这个函数,解析接口为operator new(unsigned int),这个也就是C++的内存分配函数,这个函数只负责申请函数。

对于申请内存的函数家族,其实存在这一些:

// 这4个版本可能抛出异常
void *operator new(size_t);         // 分配一个对象
void *operator new[](size_t);           // 分配一个数组
void *operator delete(void*) noexcept;      // 释放一个对象
void *operator delete[](void*) noexcept;    // 释放一个数组

// 这些版本承若不会抛出异常
void *operator new(size_t, nothorw_t &);        // 分配一个对象
void *operator new[](size_t, nothorw_t &);      // 分配一个数组
void *operator delete(void*, nothorw_t &) noexcept; // 释放一个对象
void *operator delete[](void*, nothorw_t &) noexcept;   // 释放一个数组

1.2 new 和 delete重载

在C++中,我们知道,只要是函数就可以重载,那么new和delete的函数是否也可以重载呢?其实是可以的。我们先看看new和delete函数怎么调用。

void Mem()
{
	CData* data = NULL;
	data = (CData*)::operator new(sizeof(CData));
	::operator delete(data);
}

反汇编的结果如下:

	CData* data = NULL;
002318D8  mov         dword ptr [data],0  
	data = (CData*)::operator new(sizeof(CData));
002318DF  push        4  
002318E1  call        operator new (0231361h)  
002318E6  add         esp,4  
002318E9  mov         dword ptr [data],eax  
	::operator delete(data);
002318EC  mov         eax,dword ptr [data]  
002318EF  push        eax  
002318F0  call        operator delete (0231159h)  
002318F5  add         esp,4  

可以看到,这里只是分配了内存,但是并没有构造对象。

我们尝试重载new和delete,得到如下:

void* operator new(size_t size)
{
	std::cout << "my new" << std::endl;
	return malloc(size);
}

void operator delete(void* p)
{
	std::cout << "my delete" << std::endl;
	::free(p);
}
void Mem()
{
	CData* data = NULL;
	data = new CData(10);
	delete data;
}

此时反汇编结构如下:

	CData* data = NULL;
00DE2627  mov         dword ptr [data],0  
	data = new CData(10);
00DE262E  push        4  
00DE2630  call        operator new (0DE135Ch)  
00DE2635  add         esp,4  
00DE2638  mov         dword ptr [ebp-0ECh],eax  
00DE263E  mov         dword ptr [ebp-4],0  
00DE2645  cmp         dword ptr [ebp-0ECh],0  
00DE264C  je          Mem+83h (0DE2663h)  
00DE264E  push        0Ah  
00DE2650  mov         ecx,dword ptr [ebp-0ECh]  
00DE2656  call        CData::CData (0DE12A3h)  
00DE265B  mov         dword ptr [ebp-100h],eax  
00DE2661  jmp         Mem+8Dh (0DE266Dh)  
00DE2663  mov         dword ptr [ebp-100h],0  
00DE266D  mov         eax,dword ptr [ebp-100h]  
00DE2673  mov         dword ptr [ebp-0E0h],eax  
00DE2679  mov         dword ptr [ebp-4],0FFFFFFFFh  
00DE2680  mov         ecx,dword ptr [ebp-0E0h]  
00DE2686  mov         dword ptr [data],ecx  

这里依旧调用了构造函数,因为我们这里使用的是new CData(10)表达式,表达式默认都会调用构造函数。

1.3 定位new

我们一定知道C++的一个规则,那就是:

void* operator new(size_t, void*);

这个函数不能重载。但是为什么呢?这个函数是干什么用的呢?

在C++中,专门提供可一个初始化内存的操作,叫做定位new表达式,语法如下:

new (place_address) type 
new (place_address) type(initializer-list)  
new (place_address) type[]
new (place_address) type[]{initializer-list}

我们看下定位new表达式的用法:

void Mem()
{
	CData* data = NULL;
	data = (CData*)operator new(sizeof(CData));
	new(data) CData(100);

	delete data;
}

这个new(data) CData(100);操作就是在调用定位new表达式。看下反汇编的代码:

	new(data) CData(100);
0041266C  mov         eax,dword ptr [data]  
0041266F  push        eax  
00412670  push        4  
00412672  call        operator new (0411442h)  
00412677  add         esp,8  
0041267A  mov         dword ptr [ebp-0D4h],eax  
00412680  push        64h  
00412682  mov         ecx,dword ptr [ebp-0D4h]  
00412688  call        CData::CData (04112A3h)  

定位new多了两个操作:

  1. call operator new (0411442h)调用
  2. call CData::CData (04112A3h)构造函数调用。

对于operator new (0411442h)我们可以看下调用的结果为operator new(unsigned int,void *),这个就是C++无法充值的函数

void* operator new(size_t, void*);

也就是说无法重载的函数void* operator new(size_t, void*);其实就是定位new表达式需要调用的函数。

1.4 应用

如果我们在C++程序中存在内存泄露,那么怎么办呢?想法是接管new和delete,跟踪一下那些new没有被释放,并且记录new的信息,可以如下实现:

class CData
{
public:
	CData(int d) : Data(d) {}

private:
	int Data;
};


void* operator new(size_t size, const char *file, int line)
{
	void* p = nullptr;
	p = malloc(size);
	std::cout << "my new(" << file << "," << p << "," << line << ")" << std::endl;
	return p;
}

void operator delete(void* p)
{
	std::cout << "my delete(" << p << ")" << std::endl;
	::free(p);
}

#define DEBUG_NEW new(__FILE__, __LINE__)
#define new DEBUG_NEW
void Mem()
{
	CData* data = NULL;
	data = new CData(100);

	delete data;
}

执行结果:

my new(c:\users\xzh\source\repos\cplusplus\cplusplus\cplusplus.cpp,015CE340,45)
my delete(015CE340)

1.5 内存池

根据上面描述的知识点我们很容易就实现一个内存池模型,这里不再描述内存池怎么实现,下面看一下一个C++标准库的一个内存池allocator。

2. allocator

这是一个模板类,可以将 内存的分配和释放与对象的构造和析构分离开来,例如如下:

std::allocator<std::string> Allocator;  
auto p = Allocator.allocate(n); 

2.1 allocator 基本操作

操作 解释
allocator<T> a 定义了一个名为a的allocator对象,它可以为类型T的对象分配内存
a.allocator(n) 分配一段原始的、未构造的内存,这段内存能保存n个类型为T的对象
a.deallocate(p,n) 释放T*指针p地址开始的内存,这块内存保存了n个类型为T的对象,p必须是一个先前由allocate返回的指针,且n必须是p创建时所要求的大小,且在调用该函数之前必须销毁在这片内存上创建的对象。
a.construct(p,args) p必须是一个类型为T* 的指针,指向一片原始内存,arg将被传递给类型为T的构造函数,用来在p指向的原始内存上构建对象。
a.destory(p) p为T*类型的指针,用于对p指向的对象执行析构函数.

2.2 实例

void Mem()
{
	std::allocator<int> alloc;
	std::allocator<int>::value_type* p = alloc.allocate(5);
	alloc.construct(p, 10);
	alloc.construct(p + 1, 20);
	alloc.construct(p + 2, 30);
	alloc.construct(p + 3, 40);
	alloc.construct(p + 4, 50);
	for (int i = 0; i < 5; ++i)
	{
		std::cout << p[i] << std::endl;
	}
	alloc.destroy(p);
	alloc.destroy(p);
	alloc.destroy(p + 2);
	alloc.destroy(p + 3);
	alloc.destroy(p + 4);
}

2.3 allocator的实现

知道上面的技术,我们就很容易掌握allocator的实现了,主要原理是分配内存和构造对象分离。

2.3.1 allocate内存分配

这个用来分配内存,实现如下:

_NODISCARD _DECLSPEC_ALLOCATOR _Ty * allocate(_CRT_GUARDOVERFLOW const size_t _Count)
{	// allocate array of _Count elements
	return (static_cast<_Ty *>(_Allocate<_New_alignof<_Ty>>(_Get_size_of_n<sizeof(_Ty)>(_Count))));
}

template<size_t _Align,
	class _Traits = _Default_allocate_traits,
	enable_if_t<(_Align > __STDCPP_DEFAULT_NEW_ALIGNMENT__), int> = 0> inline
	_DECLSPEC_ALLOCATOR void *_Allocate(const size_t _Bytes)
{	// allocate _Bytes when _HAS_ALIGNED_NEW && _Align > __STDCPP_DEFAULT_NEW_ALIGNMENT__
	if (_Bytes == 0)
	{
		return (nullptr);
	}

	size_t _Passed_align = _Align;

	return (_Traits::_Allocate_aligned(_Bytes, _Passed_align));
}

_DECLSPEC_ALLOCATOR static void * _Allocate_aligned(const size_t _Bytes, const size_t _Align)
{
	return (::operator new(_Bytes, align_val_t{_Align}));
}

这里直接调用::operator new分配内存,所以这个内存并没有初始化。

2.3.2 construct内存构造

template<class _Objty,
		class... _Types>
_CXX17_DEPRECATE_OLD_ALLOCATOR_MEMBERS void construct(_Objty * const _Ptr, _Types&&... _Args)
{	// construct _Objty(_Types...) at _Ptr
	::new (const_cast<void *>(static_cast<const volatile void *>(_Ptr)))
			_Objty(_STD forward<_Types>(_Args)...);
}

这里使用定位new表达式来构造对象(调用构造函数来构造),参数使用完美转发实现。

2.3.3 destroy内存析构

template<class _Uty>
_CXX17_DEPRECATE_OLD_ALLOCATOR_MEMBERS void destroy(_Uty * const _Ptr)
{	// destroy object at _Ptr
	_Ptr->~_Uty();
}

这里直接调用析构函数来析构内存块。

3. 总结

内存池实现的主要技术是:

  1. 预先分配内存。
  2. 内存分配和构造与析构分离。
  3. 单独提供构造的接口。
  4. 单独提供析构内存块的接口。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!