自由列表
当一个系统中动态内存管理发生频繁,特别是针对某一类型反复进行内存分配和释放时,此时从性能角度考虑,我们可以想办法减小内存分配中的开销,一个切入点就是自己管理内存的分配和释放。
自己管理内存分配和释放的一个通用策略是预先分配一块原始内存来保存未构造的对象,创建新元素时,可以在一个预先分配的对象中构造;释放元素时,将它们放回预先分配对象的块中,而不是将内存实际返还给系统。这种策略常被称为维持一个自由列表(freelist)。可以将自由列表实现为已分配但未构造的对象的链表。
我们在这实现一个通用的模板类来实现这种内存管理需求,以便任何应用这种内存管理方案的类可以通过继承该模板类而获得该功能。我们定义一个名为CacheObj的模板类来处理自由列表,它只有简单的接口:它的工作只是分配和管理已分配但未构造对象的自由列表。这个类将定义一个成员operator new,返回自由列表中的下一个元素,并将该元素从自由列表中删除。当自由列表为空的时候,operator new将分配新的原始内存。这个类还定义operator delete,在撤销对象时将元素放回到自由列表中。
另外,CachedObj只能用于不包含在继承层次中的类型,因为CachedObj类没有办法根据对象的实际类型分配不同大小的对象:它的自由列表保存单一大小的对象。因此,它只能用于不作基类使用的类。
至此,我们可以得到CachedObj类的基本模型,代码如下:
当一个系统中动态内存管理发生频繁,特别是针对某一类型反复进行内存分配和释放时,此时从性能角度考虑,我们可以想办法减小内存分配中的开销,一个切入点就是自己管理内存的分配和释放。
自己管理内存分配和释放的一个通用策略是预先分配一块原始内存来保存未构造的对象,创建新元素时,可以在一个预先分配的对象中构造;释放元素时,将它们放回预先分配对象的块中,而不是将内存实际返还给系统。这种策略常被称为维持一个自由列表(freelist)。可以将自由列表实现为已分配但未构造的对象的链表。
我们在这实现一个通用的模板类来实现这种内存管理需求,以便任何应用这种内存管理方案的类可以通过继承该模板类而获得该功能。我们定义一个名为CacheObj的模板类来处理自由列表,它只有简单的接口:它的工作只是分配和管理已分配但未构造对象的自由列表。这个类将定义一个成员operator new,返回自由列表中的下一个元素,并将该元素从自由列表中删除。当自由列表为空的时候,operator new将分配新的原始内存。这个类还定义operator delete,在撤销对象时将元素放回到自由列表中。
另外,CachedObj只能用于不包含在继承层次中的类型,因为CachedObj类没有办法根据对象的实际类型分配不同大小的对象:它的自由列表保存单一大小的对象。因此,它只能用于不作基类使用的类。
至此,我们可以得到CachedObj类的基本模型,代码如下:
/* memory allocation class: * Pre-allocates objects and maintains a freelist of objects that are unused * When an object is freed, it is put back on the freelist * The memory is only returned when the program exists */ template <typename T> class CachedObj { public: void* operator new (std::size_t); void operator delete (void*, std::size_t); virtual ~CachedObj() { } protected: T *next; private: static void add_to_freelist(T*); static std::allocator<T> alloc_mem; static T *freeList; static const std::size_t chunk; }; |
它只提供了三个公有成员:operator new, operator delete和虚析构函数。new和delete成员分别从自由列表取走对象和将对象返回到自由列表。
用static成员管理自由列表。这些成员声明为static,是因为只为所有给定类型的对象维持一个自由列表。freeList指针指向自由列表的表头。名为chunk的成员指定每当自由列表为空时将分配的对象的数目。最后,add_to_freelist函数将对象放在自由列表中,operator new使用这个函数将新分配的对象放到自由列表,删除对象的时候,operator delete也使用该函数将对象放回自由列表。
下面实现类的成员函数。首先是定义operator new,它从自由列表中返回一个对象,如果自由列表为空,new必须首先分配chunk数目的新内存:
用static成员管理自由列表。这些成员声明为static,是因为只为所有给定类型的对象维持一个自由列表。freeList指针指向自由列表的表头。名为chunk的成员指定每当自由列表为空时将分配的对象的数目。最后,add_to_freelist函数将对象放在自由列表中,operator new使用这个函数将新分配的对象放到自由列表,删除对象的时候,operator delete也使用该函数将对象放回自由列表。
下面实现类的成员函数。首先是定义operator new,它从自由列表中返回一个对象,如果自由列表为空,new必须首先分配chunk数目的新内存:
template <typename T> void* CachedObj<T>::operator new (size_t sz) { // new should only be asked to build a T, // not and object derived from T, // theck that right size is requested if (sz != sizeof(T)) throw runtime_error("CachedObj: wrong size object in operator new"); if (!freeList) { // ths list is empty: grab a new chunk of memory // allocates chunk number of objects of type T T *array = alloc_mem.allocate(chunk); // now set the next pointers in each object in the allocated memory for (size_t i = 0; i != chunk; ++i) add_to_freelist(&array[i]); } T *p = freeList; freeList = freeList->CachedObj<T>::next; return p; // constructor of T will construct the T part of the object } |
在该函数中,首先验证要求它分配正确数量的空间,CachedObj类应该只被不是基类的类使用。CachedObj类在固定大小的自由列表上分配对象,这一事实意味着,继承层次中的类不能使用它来处理内存分配。另外,为了防止派生类中也有next成员,所以在freeList->CachedObj<T>::next用类名进行限制访问next成员。
operator delete成员只负责释放内存,即调用add_to_freelist成员将被删除对象放回到自由列表。由于operator delete的形参类型必须是void*类型,在调用add_to_freelist之前,必须将指针从void*强制转换为它的实际类型。代码如下:
template <typename T> void CachedObj<T>::operator delete (void *p, size_t) { if (p != 0) // put the "deleted" object back at head of freelist add_to_freelist(static_cast<T*>(p)); } |
add_to_freelist成员的任务是设置next指针,并且在将对象加到自由列表时更新freeList指针。代码如下:
template <typename T> void CachedObj<T>::add_to_freelist (T *p) { p->CachedObj<T>::next = freeList; freeList = p; } |
剩下的是定义类的静态数据成员:
template <typename T> allocator<T> CachedObj<T>::alloc_mem; template <typename T> T* CachedObj<T>::freeList = 0; template <typename T> const size_t CachedObj<T>::chunk = 24; |
最后,我们把所有代码放在一块进行测试:
#include <iostream> #include <stdexcept> #define _DEBUG_ /* memory allocation class: * Pre-allocates objects and maintains a freelist * of objects that are unused * When an object is freed, it is put back on the freelist * The memory is only returned when the program exists */ template <typename T> class CachedObj { public: void* operator new (std::size_t); void operator delete (void*, std::size_t); virtual ~CachedObj() { } protected: T *next; private: static void add_to_freelist(T*); static std::allocator<T> alloc_mem; static T *freeList; static const std::size_t chunk; }; using namespace std; template <typename T> void* CachedObj<T>::operator new (size_t sz) { // new should only be asked to build a T, // not and object derived from T, // theck that right size is requested #ifdef _DEBUG_ std::cout << "CachedObj<T>::operator new" << std::endl; #endif if (sz != sizeof(T)) throw runtime_error("CachedObj: wrong size object in operator new"); if (!freeList) { // ths list is empty: grab a new chunk of memory // allocates chunk number of objects of type T T *array = alloc_mem.allocate(chunk); // now set the next pointers in each object in the allocated memory for (size_t i = 0; i != chunk; ++i) add_to_freelist(&array[i]); } T *p = freeList; freeList = freeList->CachedObj<T>::next; return p; // constructor of T will construct the T part of the object } template <typename T> void CachedObj<T>::operator delete (void *p, size_t) { #ifdef _DEBUG_ std::cout << "CachedObj<T>::operator delete" << std::endl; #endif if (p != 0) // put the "deleted" object back at head of freelist add_to_freelist(static_cast<T*>(p)); } template <typename T> void CachedObj<T>::add_to_freelist (T *p) { p->CachedObj<T>::next = freeList; freeList = p; } template <typename T> allocator<T> CachedObj<T>::alloc_mem; template <typename T> T* CachedObj<T>::freeList = 0; template <typename T> const size_t CachedObj<T>::chunk = 24; class TestCachedObj: public CachedObj<TestCachedObj> { public: TestCachedObj() { std::cout << "TestCachedObj Constructor" << std::endl; } ~TestCachedObj() { std::cout << "TestCachedObj Destructor" << std::endl; } }; class Derived: public TestCachedObj { private: int stub; }; int main () { cout << "> Test 1:" << endl; TestCachedObj *p = new TestCachedObj; delete p; cout << "> Test 2:" << endl; try { TestCachedObj *p2 = new Derived; delete p2; } catch (const exception& e) { cout << " ! Catched exception: " << e.what() <<endl; } return 0; } |
运行结果: > Test 1: CachedObj<T>::operator new TestCachedObj Constructor TestCachedObj Destructor CachedObj<T>::operator delete > Test 2: CachedObj<T>::operator new ! Catched exception: CachedObj: wrong size object in operator new Terminated with return code 0 Press any key to continue ... |
解决释放派生类数组所遇到的问题
先看如下一段代码:
#include <iostream> #include <stdexcept> class Base { public: Base () { std::cout << "Base Constructor" << std::endl; } virtual ~Base () { std::cout << "Base Destructor" << std::endl; } private: int a; }; class Derived: public Base { public: Derived () { std::cout << "Derived Constructor" << std::endl; } virtual ~Derived () { std::cout << "Derived Denstructor" << std::endl; } private: int b; }; using namespace std; int main () { cout << "> Test 1:" << endl; Base *p = new Derived; cout << "> OK: new successfully" << endl; delete p; cout << "> OK: delete successfully" << endl; cout << "/n> Test 2:" << endl; Base *pa = new Derived[5]; cout << "> OK: new[] successfully" << endl; delete [] pa; cout << "> OK: delete[] successfully" << endl; return 0; } |
程序运行显示如下结果后出错:
> Test 1: Base Constructor Derived Constructor > OK: new successfully Derived Denstructor Base Destructor > OK: delete successfully > Test 2:
Base Constructor Derived Constructor Base Constructor Derived Constructor Base Constructor Derived Constructor Base Constructor Derived Constructor Base Constructor Derived Constructor > OK: new[] successfully Terminated with return code -1073741819 Press any key to continue ... |
当然,程序出错的原因是很明显的,先引用一段C++标准文档对此问题的描述:
In the second alternative (delete array) if the dynamic type of the object to be deleted differs from its static type, the behavior is undefined.
---ISO/IEC 14882-1998 5.3.5.3 |
因为对于动态分配的数组,我们在用delete[]进行释放时,不能通过基类指针,而必须使用派生类指针;否则,其行为是未定义的。
首先,解决这个问题的根本方法,也是正确的方法,应该使用正确类型的指针来保存动态分配的数组。可以这么说,上面程序中使用基类指针来保存动态分配的数组的设计是劣质的。
Base *pa = new Derived[5];
应改为:
Derived *pa = new Derived[5];
另一方面,从技术角度来看,在这种情况下,一个不太实用的方法就是在delete[]数组时,把指针类型转换为正确的类型:
首先,解决这个问题的根本方法,也是正确的方法,应该使用正确类型的指针来保存动态分配的数组。可以这么说,上面程序中使用基类指针来保存动态分配的数组的设计是劣质的。
Base *pa = new Derived[5];
应改为:
Derived *pa = new Derived[5];
另一方面,从技术角度来看,在这种情况下,一个不太实用的方法就是在delete[]数组时,把指针类型转换为正确的类型:
Base *pa = new Derived[5];
delete [] static_cast<Derived*>(pa);
当然,这种办法必须知道它的动态类型。既然这样,就完全没有必须写基类指针了,而是应该写成正确的派生类指针。