WEB开发网
开发学院软件开发C++ 对C++程序内存管理的精雕细琢 阅读

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

 2008-03-08 12:24:20 来源:WEB开发网   
核心提示:应用程序分配内存的方法,对程序的执行性能有着深刻的影响,对C++程序内存管理的精雕细琢,目前,通用的内存分配方法本质上已非常高效,祝大家编程愉快!表1:部分HeapLayers库顶层堆 mallocHeap 取代malloc的层mmapHeap 取代虚拟内存治理的层sbrkHeap取代sbrk(连续内存)构建块堆的层A

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

  内存分配,不可一层不变

  今天,对绝大多数程序来说,通用的内存分配方法--此处指代分配算符(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 速度不快,但碎片很少的堆

Tags:程序 内存 管理

编辑录入:爽爽 [复制链接] [打 印]
赞助商链接