C++中小对象内存分配的优化与封装

 

The Small Object Allocation Optimization and Implement

by Encapsulating in C++

ZHANG Hao  ,  YE Nian-yu

Department of Control Science and Engineering Huazhong University of Science and Technology

 

Abstract: The Small Object Allocation System using STL is composed of 4-layer classes. Memory is managed by the Chunk class on bottom layer. When allocation appears, The “Allocator” function on second layer find the newest Chunk node through checking Chunk pointer or searching linearly. And the third layer allocates using the second layer or “::operator new” function. The fourth layer overloads operator “new” and “delete”. Finally, any objects inherited from the fourth layer object can allocate in the designed method.

Keywords: memory, fragmentation, object-oriented, c++

 

1.引言

内存碎片是指计算机中未使用内存(free memory)和已使用内存(used memory)相互交错。严格地说,计算机内存时时刻刻都是存在碎片的,但只有当碎片的数量很大时碎片才引起人们的注意。这时未使用的内存块的平均大小变得很小。比如,一个程序可能有5MB的可用内存,但是不能分配一个大小为1MB续内存块。c++语法中动态分配和指针/引用的使用非常普遍,然而缺省的自由存储区分配器(比如::operator new::operator delete)只适用于大对象对分配,对小对象分配并不有效,甚至非常低劣,多次分配小对象后容易产生碎片。常用解决方法是,一在预先分配一大块内存以供程序使用,这是常用的内存池技术(memory pool),一种是设计一个基类,程序中使用到的小对象可以从这个类派生,以获得分配优化1上述2种方法,一个在分配时申请一大块内存,一个则是在释放时不直接交给系统。两种方法在不同时刻优化了内存管理。这2种技术也可以同时被使用。其内存分配算法最优选择法——选择一块最小的能够满足需求的内存块最劣选择法——选择能够满足条件中最大的一块存储区顺序选择法——按顺序查找可用内存块,选择首先遇到的能够满足大小的内存块。

自从1998C++ Standard定案以后,C++程序库有了大幅扩充和优化,其中STL功不可没。STL中内存的优化方法,以SGI STLGNU C++中所携带之版本)为例,采用了两级分配器2,第一级分配器直接调用malloc()分配内存。第二级分配器的作法是:如果需要分配的内存过大,超过128字节,就交给第一级分配器处理;当小于128字节的内存被请求分配时,采用前述内存池技术管理,每次分配一大块内存,并由一内置链表来管理,为此SGI第二级分配器会主动将小额内存分配请求上调至8的倍数,并维护16个内置链表,各自管理大小分别为8 ,16 , 24, 32, ……, 128字节的小块区域。由此得到启发,可以使用面向对象的方法设计一个代码可重用的“小对象”类,在某些无须或无法使用STL而又对内存碎片十分敏感的程序中十分有用。

 

2可重用的小对象分配体系

代码可重用的“小对象”内存分配体系由4个类层次组成:

ChunkàFixedAllocator àSmallObjAllocator à SmallObject

1)最底层是Chunk类。每一个Chunk对象封装并管理一大块由混和大小内存块组成的内存。Chunk对象实现了分配和释放内存块逻辑,当Chunk对象中内存块用完后,分配函数返回0表示失败。

Chunk的定义如下:

struct Chunk

{

void Init(std::size_t blockSize, unsigned char blocks);

void* Allocate(std::size_t blockSize);

void Deallocate(void* p, std::size_t blockSize);

unsigned char* pData_;

unsigned char firstAvailableBlock_,blocksAvailable_;

};

firstAvailableBlock_, 保存第一块可使用内存的索引

blocksAvailable_, Chunk中可以使用的内存块


Chunk的接口十分简单。包括初始化Chunk对象,Allocate分配内存块,Deallocate 释放内存块。注意必须传递字节数给Allocate Deallocate ,因为Chunk没有保留其字节数。

Chunk是由连续固定大小的内存块构成,初始化的时候必须提供每块内存大小(blockSize)和内存块的个数(blocks)。Chunk将在每块内存首字节写入序号以便管理,如以下代码:

void Chunk::Init(std::size_t blockSize, unsigned char blocks)

{

      …//pData_申请空间,初始化firstAvailableBlock_blocksAvailable_

//为每个block标上序号

unsigned char i = 0;unsigned char* p = pData_;

for (; i != blocks; p += blockSize)

*p = ++i;

}

分配函数如下:(释放函数略)

void* Chunk::Allocate(std::size_t blockSize)

{

if (!blocksAvailable_) return 0;

unsigned char* pResult =pData_ + (firstAvailableBlock_ * blockSize);

firstAvailableBlock_ = *pResult;

--blocksAvailable_;

return pResult;

}

    2)第二层是FixedAllocatorFixedAllocator知道怎样分配和释放混和大小的内存块,而不受限于Chunk的大小。它的容量只受限于可获得的堆内存。为了实现这个功能,FixedAllocator包含一个含有Chunkvector

class FixedAllocator

{

...

private:

std::size_t blockSize_;

unsigned char numBlocks_;

typedef std::vector<Chunk> Chunks;

Chunks chunks_;

Chunk* allocChunk_;

Chunk* deallocChunk_;

};

    为了实现快速查找,FixedAllocator并未使用STL库中的iterator概念。它保存了指向最后一次分配内存时使用过的Chunk指针——allocChunk_。当一个内存块被请求时,Allocator函数检测allocChunk_,如果allocChunk_是一个可以使用的内存块,则直接使用这个块,分配需求是迅速的。如果不是,那么将进行线性查找(有可能新的Chunk被加入vector);allocChunk_始终保证指向最新的Chunk节点。通过这种方法,FixedAllocator的内存分配大多数情况满足常数时间复杂度。

    然而,内存释放时会出现问题3。由于释放阶段很多信息都消失了,我们所知道的只是需要释放的内存指针,并不知道指针属于哪一个Chunk。当然,逐个线性搜索vector是可行的,但是效率不高。

    为加快释放过程,可采用前述freelist法,将释放的内存块放入一块cache区,FixedAllocator首先在cache区查找适当的可用内存块,如果cache区内有可用内存那么分配过程是很快的。只有当cache区没有可用内存块,FixedAllocator才调用Chunk::Allocate()。

    33层是可以分配任何大小对象的SmallObjAllocatorSmallObjAllocator也是包含FixedAllocator组成的vector。当SmallObjAllocator接受到内存分配请求时,自动选择通过FixedAllocator分配内存,或者交给全局::operator new处理。以下是SmallObjAllocator的定义:

class SmallObjAllocator

{

public:

SmallObjAllocator(

std::size_t chunkSize,

std::size_t maxObjectSize);

void* Allocate(std::size_t numBytes);

void Deallocate(void* p, std::size_t size);

...

private:

std::vector<FixedAllocator> pool_;

FixedAllocator* pLastAlloc_;

FixedAllocator* pLastDealloc_;

};

    构造函数包含2个参数。参数chunkSize Chunk的大小(字节表示的每个Chunk对象的长度)的默认值,参数 maxObjectSize 是被视为“小对象”的对象大小的最大值。SmallObjAllocator把大于 maxObjectSize的对象分配交给::operator newFixedAllocator类似,我们也可以保存指向最后一次使用过的FixedAllocator的指针来提高速度。

    请求分配时,pLastAlloc_首先被检测,如果它的大小不合适,那么SmallObjAllocator ::Allocate将在vector中进行二分查找(pool_可以按顺序排列)。

    4最上一层是SmallObject类,它包含了SmallObjAllocator提供的函数。SmallObject类重载了全局new delete运算符,任何从SmallObject继承的对象都会按照我们定义的方法执行内存分配。

    SmallObject的定义十分简单:

class SmallObject

{

public:

static void* operator new(std::size_t size);

static void operator delete(void* p, std::size_t size);

virtual ~SmallObject() {}

};

    上述代码中析构函数被定义为虚函数,这是因为SmallObject类是当作基类来使用,并且释放一个指向派生类的基类指针会引起不确定行为。操作符newdelete定义如下:

SmallObjAllocator MyAlloc;

void* SmallObject::operator new(std::size_t size)

{

return MyAlloc.Allocate(size);

}

void SmallObject::operator delete(void* p, std::size_t size)

{

MyAlloc.Deallocate(p, size);

}

    以上代码不够完善,因为MyAlloc对象实际上应该用c++领域的一个常用设计方式实现——singleton这种技术称为"单一模式",即保证整个程序内只存在该类的一个对象。限于篇幅本文不再赘述Singleton pattern

    使用SingletonSmallObject类作如下修改:

typedef Singleton<SmallObjAllocator> MyAlloc;

void* SmallObject::operator new(std::size_t size)

{

return MyAlloc::Instance().Allocate(size);

}

void SmallObject::operator delete(void* p, std::size_t size)

{

MyAlloc::Instance().Deallocate(p, size);

}

3.总结

至此,小对象分配已经用面向对象的方法封装起来。在软件开发中遇到大量小对象的分配时,可以让这些对象直接从SmallObject继承,这样所有的内存分配操作将得到优化。对被视为“小对象”的对象大小的最大值可依实际应用所确定,笔者对本方法和全局new/delete方法进行了测试比较,64字节以下大小的对象引起的内存浪费是可观的,所以一般情况64字节以下大小的对象都可以认为是“小对象”。如果使用SmallObjAllocator分配的对象过大,操作过程中会发生大量多余内存被分配(由于在释放所有小对象时FixedAllocator仍然需保留整个Chunk)。

 

参考文献:

1.R.AlexanderG.Bensley .C++ Footprint and Performance Optimization [M].Sams Publishing 2000.

2.侯捷.STL源码剖析 [CP/DK].台湾:基峰脑图书资料有限公司.2002.

3.Andrei Alexandrescu.Modern C++ Design: Generic Programming and Design Patterns Applied [M].Addison Wesley 2001.

4.Erich Gamma Richard Helm Ralph Johnson John Vlissides.Design Patterns: Elements of Reusable Object-Oriented Software [M].Addison-Wesley 1995.

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值