分享
 
 
 

对C++程序内存管理的精雕细琢

王朝c/c++·作者佚名  2008-06-01
窄屏简体版  字體: |||超大  

应用程序分配内存的方法,对程序的执行性能有着深刻的影响。目前,通用的内存分配方法本质上已非常高效,但仍有改进的空间。

内存分配,不可一层不变

今天,对绝大多数程序来说,通用的内存分配方法--此处指代分配算符(Allocator:即malloc或new),已达到了理想的速度及满足了低碎片率的要求,然而,在内存分配领域,一丁点的信息都值得探讨很久,某些特定程序关于分配模式的信息,将有助于实现专门的分配算符,可显著地提高大多数高性能要求程序的性能底线。有时,当通用内存分配算符平均耗费几百个时钟周期时,一个良好的自定义内存分配算符可能只需要不到半打的周期。

这就是为什么大多数高性能、高要求的应用程序(如GCC、Apache、Microsoft SQL Server),都有着它们自己的内存分配算符。也许,把这些专门的内存分配算符归纳起来,放进一个库中,是个不错的想法,但是,你的程序可能有不同的分配模式,其需要另外的内存分配算符,那怎么办呢?

等等,还有呢,假如我们设计了一种非凡用途的内存分配算符,就可以不断发展下去,由此可从中筛选出一些,来组成一个通用目的的内存分配算符,假如此通用分配算符优于现有的通用分配算符,那么此项设计就是有效及实用的。

下面的示例使用了Emery小组的库--HeapLayers(http://heaplayers.org/),为了定义可配置的分配算符,其中使用了mixins(在C++社区中,也被称为Coplien递归模式):通过参数化的基来定义类,每一层中只定义两个成员函数,malloc和free:

template <class T>

strUCt Allocator : public T {

void * malloc(size_t sz);

void free(void* p);

//系统相关的值

enum { Alignment = sizeof(double) };

//可选接口e

size_t getSize(const void* p);

};

在每一层的实现中,都有可能向它的基类请求内存,一般来说,一个不依靠于外界的内存分配算符,都会处在层次的顶层--直接向前请求系统的new和delete操作符、malloc和free函数。在HeapLayers的术语中,没有顶层堆,以下是示例:

struct MallocHeap {

void * malloc(size_t sz) {

return std::malloc(sz);

}

void free(void* p) {

return std::free(p);

}

};

为获取内存,顶层堆也能通过系统调用来实现,如Unix的sbrk或mmap。getSize函数的情况就比较非凡,不是每个人都需要它,定义它只是一个可选项。但假如定义了它,你所需做的只是插入一个存储内存块大小的层,并提供getSize函数,见例1:

例1:

template <class SuperHeap>

class SizeHeap {

union freeObject {

size_t sz;

double _dummy; //对齐所需

};

public:

void * malloc(const size_t sz) {

//添加必要的空间

freeObject * ptr = (freeObject *)SuperHeap::malloc(sz + sizeof(freeObject));

//存储请求的大小

ptr->sz = sz;

return ptr + 1;

}

void free(void * ptr) {

SuperHeap::free((freeObject *) ptr - 1);

}

static size_t getSize (const void * ptr) {

return ((freeObject *)ptr - 1)->sz;

}

};

SizeHeap是怎样实现一个实用的层,并挂钩于它基类的malloc与free函数的最好示例,它在完成一些额外的工作之后,把修改好的结果返回给使用者。SizeHeap为存储内存块大小,分配了额外的内存,再加上适当的小心调整(指union),尽可能地避免了内存数据对齐问题。不难想像,我们可构建一个debug堆,其通过特定模式在内存块之前或之后填充了一些字节,通过检查是否模式已被保留,来确认内存的溢出。事实上,这正是HeapLayers的DebugHeap层所做的,非常的简洁。

让我们再来看看,以上还不是最理想的状态,某些系统已经提供了计算已分配内存块大小的原语(此处指操作符,即前述的分配算符),在这些系统上,SizeHeap实际上只会浪费空间。在这种情况下(如Microsoft Visual C++),你将不需要SizeHeap与MallocHeap的衔接,因为MallcoHeap将会实现getSize:

struct MallocHeap {

... 与上相同 ...

size_t getSize(void* p) {

return _msize(p);

}

};

但似乎还有一些不足之处。想一想,我们是在统计时钟周期,假如一个系统的malloc声明了内存的块大小将存储在实际块之前的一个字中,那将会怎样呢?在这种情况下,SizeHeap还是会浪费空间,因为它仍会在紧接着系统已植入的块后存储一个字。此处所需的,只是一个用SizeHeap的方法实现了getSize的层,但未挂钩malloc与free。这就是为什么HeapLayers把前面的SizeHeap分成了两个,见例2:

例2:

template <class Super>

struct UseSizeHeap : public Super {

static size_t getSize(const void * ptr) {

return ((freeObject *) ptr - 1)->sz;

}

protected:

union freeObject {

size_t sz;

double _dummy; //对齐所需

};

};

template <class SuperHeap>

class SizeHeap: public UseSizeHeap<SuperHeap>{

typedef typename

UseSizeHeap<SuperHeap>::freeObject

freeObject;

public:

void * malloc(const size_t sz) {

//添加必要的空间

freeObject * ptr = (freeObject *)SuperHeap::malloc(sz + sizeof(freeObject));

//存储请求的大小

ptr->sz = sz;

return (void *) (ptr + 1);

}

void free(void * ptr) {

SuperHeap::free((freeObject *)ptr - 1);

}

};

现在,SizeHeap就会正确地添加UseSizeHeap层,并利用它的getSize实现了,而UseSizeHeap也能通过其他配置来使用--这是一个非常优雅的设计。

一个实用的示例:FreelistHeap

到目前为止,我们还处于一个预备的阶段,只有架构,还不知怎样利用这些层来编写一个高效专用的内存分配算符,也许一个比较合适的开发步骤可如下所示:

·收集有关程序为每种内存块大小进行分配次数的信息。

·为最经常请求的大小(在此称为S),维持一个私有、逐一链接的列表。

·对S的内存分配尽可能地从列表中返回内存,或者从默认分配算符中返回(在分层架构中,从上级层中)。

·对S大小内存块的释放,把内存块放回至列表中。

·一个精心设计的分配策略,应可对范围大小从S1至S2,使用相同的释放列表,并消耗同等的内存。而所需链接列表的操作开销为O(1),实际上只有几条指令。另外,指向下一条目的指针,能存储在实际的块中(块中存储了无用的数据--总为一个释放了的块),因此,对每个块就不需要额外的内存了。正因为大多数应用程序分配内存的大小都是不同的,所以,对任何分配算符的实现来说,释放列表就必不可少了。

下面让我们来实现一个层,由其对已知静态范围大小从S1至S2,实现了一个释放列表,见例3:

例3:

template <class Super, size_t S1, size_t S2>

struct FLHeap {

~FLHeap() {

while (myFreeList) {

freeObject* next = myFreeList->next;

Super::free(myFreeList);

myFreeList = next;

}

}

void * malloc(const size_t s) {

if (s < S1 s > S2)) {

return Super::malloc(s);

}

if (!myFreeList) {

return Super::malloc(S2);

}

void * ptr = myFreeList;

myFreeList = myFreeList->next;

return ptr;

}

void free(void * p) {

const size_t s = getSize(p);

if (s < S1 s > S2) {

return Super::free(p);

}

freeObject p =reinterpret_cast<freeObject *>(ptr);

p->next = myFreeList;

myFreeList = p;

}

private:

// 嵌入在释放的对象中的链接列表指针

class freeObject {

public:

freeObject * next;

};

//释放的对象链接列表头

freeObject * myFreeList;

};

现在,你像如下所示可定义一个自定义的堆:

typedef FLHeap<

SizeHeap<MallocHeap>,

24,

32>

SmartoHeapo;

SmartoHeapo在分配的大小在24至32之间时,速度相当快,对其它大小来说,也基本上一样。

QQRead.com 推出数据恢复指南教程 数据恢复指南教程

数据恢复故障解析

常用数据恢复方案

硬盘数据恢复教程

数据保护方法

数据恢复软件

专业数据恢复服务指南

原地重新分配(Inplace Resizing)

许多的C++程序员都梦寐以求有一种标准的原语(也即操作符),用于原地重新分配内存。众所周知,C语言中有realloc,其尽可能的原地重新分配内存,并在涉及到复制数据时使用memcpy,但memcpy并不适合于C++对象,所以,realloc也不适用于C++的对象。因此,任何一种renew原语都不能用标准C分配符来实现,这就是为什么C++中没有renew的原因。

以下演示了一种改进后的方法,可应用于C++代码中的原地重新分配,请看:

const int n = 10000;

Vec v;

for (int i = 0; i < n; ++i)

v.push_back(0);

Metrowerks的Howard Hinnant一直在为实现应用于CodeWarrior标准库的原地扩展而努力,用他自己的话来说:

现在有一个可进行原地重新分配的vector<T, malloc_allocator<T>>,当Vec为一个不带原地扩展的vector<int>时,耗时为0.00095674秒;当Vec为一个带有原地扩展的vector<int>时,耗时为0.000416943。由此可看出,内存的原地重新分配,所带来的性能提升,非常之明显。

既然有了原地重新分配所带来的好处,而堆中的每个层都能控制其自己的分配算法和数据结构,请看下面的堆层接口:

template <class T>

struct Allocator : public T {

void * malloc(size_t sz);

void free(void* p);

size_t eXPand(void* p, size_t min, size_t max);

};

扩展在语义上的意思是,尝试通过p扩展指向在两者之间最大尺寸的块,并返回期望扩展的任意大小内存块。幸运的是,一个层不必关心用于扩展的子程序,假如所有顶层的分配方法都继续自以下的类,那么一切都将工作正常:

struct TopHeap {

size_t expand(void*, size_t, size_t) {

return 0;

}

protected:

~TopHeap() {}

};

结论

可配置的内存分配算符,是一种实用的、一体化的解决方案,可取代专门或通用的内存分配操作符。此外,HeapLayers的分层架构支持更简单的调试,并且具有非并行的可扩展性。表1演示了一个在HeapLayers中,层实现的相关子集,其中有许多值得讨论的地方,如多线程操作中的闭锁堆、STL适配程序、各种不同的工具堆、还有怎样结合多个层来创建一个通用的内存分配算符,另外,千万记住不要忘了在析构函数中释放内存,祝大家编程愉快!

表1:部分HeapLayers库

顶层堆

mallocHeap

取代malloc的层

mmapHeap

取代虚拟内存治理的层

sbrkHeap

取代sbrk(连续内存)构建块堆的层

AdaptHeap

使数据结构可作为堆使用

BoundedFreelistHeap

有长度限制的释放列表

ChunkHeap

以给定大小的块来治理内存

CoalesceHeap

执行拼接与拆分

FreelistHeap

一个释放列表(用于捕捉释放的对象)

组合堆

HybridHeap

对小对象使用一个堆,而对大对象使用另一个堆

SegHeap

用于分配方法的一般分割

StrictSegHeap

用于分配方法的严格分割

工具层

ANSIWrapper

提供与ANSI-malloc的兼容性

DebugHeap

检查多种分配错误

LockedHeap

为保证线程安全的闭锁堆

PerClassHeap

使用一个堆作为每个类的分配算符

PHOThreadHeap

带有自有分配算符私有堆

ProfileHeap

收集并输出碎片统计

ThreadHeap

一个纯私有堆分配算符

ExceptionHeap

当父类堆超出内存时,抛出一个异常

TraceHeap

输出有关内存分配的跟踪信息

UniqueHeap

引用一个堆对象的堆类型

对象表示

CoalesceableHeap

为拼接提供支持

SizeHeap

在头部中记录对象大小

非凡用途的堆

ObstackHeap

专门优化用于类似堆栈行为或快速大小调整的堆

ZoneHeap

一个区域分配算符

XallocHeap

优化用于类似堆栈行为的堆

通用堆

KingsleyHeap

快速但多碎片的堆

LeaHeap

速度不快,但碎片很少的堆

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有