高效内存管理系列(1)之子分配器

引言

 如果我们的程序使用的内存资源相对来说较大,或 者程序需要长时间的运行,就应该考虑内存碎片的问题了。早在程序开发阶段,就应该搞清楚是否需要采取特别的措施来减轻内存碎片对程序的影响。如果内存碎片 对系统形成了不良影响,目标系统就无法执行适用于程序和系统需要的内存管理方案,这时就应该在软件开发中引入专门的高效内存管理器(Effective Memory Manager, EMM. 以下“内存管理器”简称为MM),可提高目标系统内存管理方案的性能,典型的性能改善包括提高内存访问和内存分配的速度与有效的解决内存碎片的问题,有时 二者可同时实现。专门的MM可以有不同的范围,从简单的间接方案,例如在程序和操作系统之间使用子分配器,到完全替代操作系统内存管理功能的专门的可执行程序。

一、子分配器 

 子分配器(Suballocator)是设置在一段程序和操作系统之间的内存管理器。它向操作系统申请占用内存,在程序使用完此内存后,就把内存返回给操作系统。在申请占用内存和返回内存的过程中,子分配器完成了程序内存管理的功能。
1.快速和脏(dirty, not bowel)MM 
 在内存管理中,最简单的子分配器是只分配内存的分配器。

 
 //脏内存管理

 #define MAXSIZE 512000
 unsigned char membuf[MAXSIZE]
 unsigned char *memptr = (unsigned char*)membuf;
 inline void *DirtyMalloc(long size)
 {
  void* p = (void*)memptr;
  memptr += size;
  return p;
 }

优点:
 该内存管理器执行效率非常高(其它的MM根本没有这么快的速度,比诸如malloc和new等动态分配快数倍),而且不存在内存碎片问题。
缺点:
 DirtyMalloc根本没进行内存管理,申请的内存通常在程序终止时释放。
 
2.使用可利用空间表进行内存管理 
 动 态内存管理是指我们的程序可以在程序运行时进行内存占用和释放操作。动态内存的分配是通过操作系统来完成的,分配的是系统内存资源中称作堆(heap)的 那一部分内存。在程序中频繁的使用动态内存分配和释放时,程序执行效率下降。这就是我们为什么应该考虑,是立即将不用的内存释放,返回给OS,还是将程序 中部分或者全部的空闲内存保存在程序中,以便以后使用。可以使用一个表,我们将之称作可利用空间表(freelist),来记录空闲内存的路径。如果我们 的程序经常需要分配和释放同样大小的内存块,并且分配和释放的频率非常高,则使用可利用空间表技术就显得特别有效。试想使用一个结构作为一个链表或者数组 元素的情况,用来保存已经删除列表元素的内存可以重新使用,如果要建立一个新列表就可以使用这些内存空间。这样的话,就不需要再向OS提出申请内存了。
 
 // 可利用空间表的基本功能

#include <stdio.h>
#include <stdlib.h> // base class
class FreeListBase
{
 static FreeListBase* freelist; // 指向可利用空间表的指针
 FreeListBase* next;
public:
 FreeListBase() {}
 ~FreeListBase() {}
 
 void* operator new(size_t sz); // 重载new()
 void operator delete(void* vp); // 重载delete() 
};
inline void* FreeListBase::operator new(size_t sz)
{
 if (freelist)
 {
  // 如果freelist有内存,从其中获得内存
  FreeListBase* p = freelist;
  freelist = freelist->next;
  return p;
 }
 return malloc(sz); // 其它情况下调用malloc()
}
inline void FreeListBase::operator delete(void* vp)
{
 FreeListBase* p = (FreeListBase*)vp;
 // 链表释放的内存与freelist
 p->next = freelist;
 freelist = p;
}
// 把freelist指针设置为NULL
FreeListBase* FreeListBase::freelist = NULL;

 从上面的代码片断,我们可以看出此内存子分配器在释放的时候,并没有真正要求os去delete它,而是把它指向该对象的所在内存地址的指针 放在可利用空间表的head,指针freelist总是指向可利用空间表中第一段被释放的内存空间。如果新创建了类FreeListBase的一个对象, 就会查找可利用空间表,如果可利用空间表非空,指向内存中第一段内存的指针就会返回,指针freelist指向可利用空间表中的下一段内存。当 然,freelist为空时,通过调用os的malloc为程序分配一段内存。
 可利用空间表功能是设置在基类中。但是,当在实际的程序设计中使 用了可利用空间表的技术时,最好还是将该功能直接引入到使用该功能的类中。这一点非常重要,因为类FreeListBase包含了一个指向可用内存块的静 态指针,此静态指针要用在所有从类FreeListBase派生的类中。如果我们使用了从类FreeListBase派生的类,在一定程度上,可利用空间 表中就包含了不同大小的内存空间,因为不同类的对象可以占用不同的内存空间。同样,我们也可以考虑扩充运算符delete的功能,使得该运算符更加灵活。 例如,可以在可利用空间表达到一定的规模以后就开始将占用的内存释放,将使用权归还给OS.
 
3.简单的堆内存管理
 无专用内存管理的堆:

#define MAXSIZE 100000
class Stack
{
 struct elem
 {
  elem *previous;
  char *name;
  int  size;
  int  id;
 };
public:
 Stack() {last = NULL; totalSize = 0;}
 ~Stack() {}
 
 // store {name, id}
 void push(const char* s, const int nr);
 
 // search {name, id}
 void pop(char* s, int& nr);
private:
 elem *last;
 int  totalSize;
};
// add elem on top of stack
inline void Stack::push(const char* s, const int nr)
{
 int newsize = strlen(s) + 1;
 if (totalSize + newsize > MAXSIZE)
 {
  cout << "Error, Stack overflow!!" << endl;
 }
 else
 {
  elem *newElem = new elem;
  newElem->name = new char [newsize];
  strcpy(newElem->name, s);
  newElem->id = nr;
  newElem->previous = last;
  newElem->size = newsize;
  
  last = newElem;
  totalSize += newsize;
 }
}
// pop elem from top of stack and free it.
inline void Stack::pop(char* s, int& nr)
{
 if (NULL != last)
 {
  elem* popped = last;
  strcpy(s, popped->name);
  nr = popped->id;
  last = popped->previous;
  totalSize -= popped->size;
  delete [] popped->name;
  delete popped;
 }
 else
 {
  cout << "Error, Stack underflow!!" << endl;
 }
}

 上述的堆MM可以用来存储字符串和ID值。字符串的存储使用后进先出(LIFO)的工作方式。如果频繁使用此堆MM,随着内存不断的分配和释放,慢慢的内存就会出现碎片。
 
4.BigChunkStack内存管理 
 堆MM之所以会带来内存碎片问题,直接是由程序处理内存请求的动态特性造成的。在这种情况下,为了避免内存碎片的产生,最简单的方法就是为程序分配一大块内存,根据需要将大块内存分配给程序。
 
 使用专用内存管理的堆内存:

#ifndef ADAPTIVE_VER
#define MAXSIZE 100000
#endif
class BigChunkStack
{
 struct elem
 {
  unsigned int id;
  unsigned int previousElemSize;
  unsigned int nameSize;
  char* name;
 };
 
public:
 BigChunkStack()
 {
  totalsize = 0;
  emptyElemSize = sizeof(elem);
  lastElemSize = 0;
  #ifdef ADAPTIVE_VER
  MAXSIZE = 0;
  #endif
 }
 ~BigChunkStack() {}
 
 // store {name, id}
 void push(const char* s, const unsigned int nr);
 
 // search {name, id}
 void pop(char* s, unsigned int &nr);
#ifdef ADAPTIVE_VER
private:
 bool grow();
 void shrink();
#endif
private:
#ifdef ADAPTIVE_VER
 char* pool;
 unsigned int MAXSIZE;
#else
 char pool[MAXSIZE];
#endif
 unsigned int totalsize;
 unsigned int emptyElemSize;
 unsigned int lastElemSize; 
};
// push a new elem on the top of stack
inline void BigChunkStack::push(const char*s, const unsigned int nr)
{
 unsigned int newsize = strlen(s) + 1;
 unsigned int totalElemSize = newsize + emptyElemSize;
 
#ifdef ADAPTIVE_VER
 while(totalsize + totalElemSize > MAXSIZE)
 {
  if (!grow())
  {
   cout << "Error, Stack overflow & grow failed." << endl;
   return;
  }
 }
#else
 if (totalsize + totalElemSize > MAXSIZE)
 {
  cout << "Error, Stack overflow!!" << endl;
  return;
 }
#endif
 
 elem *newElem = (elem*)(pool+totalsize);
  
 newElem->id = nr;
 newElem->nameSize = newsize;
 newElem->previousElemSize = lastElemSize;
 strcpy(&newElem->name, s);
  
 lastElemSize = totalElemSize;
 totalsize += totalElemSize; 
}
// pop elem from top of stack and free it
inline void BigChunkStack::pop(char* s, unsigned int nr)
{
#ifdef ADAPTIVE_VER
 if (totalsize*4 <= MAXSIZE)
  shrink();
#endif
 if (totalsize > 0)
 {
  totalsize -= lastElemSize;
  elem* popped = (elem*)(pool+totalsize);
  lastElemSize = popped->previousElemSize;
  
  strcpy(s, &popped->name);
  nr = popped->id;
 }
 else
 {
  cout << "Error, Stack underflow!!" << endl;
 }
}
#ifdef ADAPTIVE_VER
#define INIT_SIZE 1024
// grow memory pool
inline bool BigChunkStack::grow()
{
 if (0 == MAXSIZE)
 {
  MAXSIZE = INIT_SIZE;
  return true;
 }
 else
 {
  // expand 2 times
  MAXSIZE <<= 1;
  
  char* temppool = (char*)realloc(pool, MAXSIZE);
  if (NULL == temppool)
   return false;
  
  pool = temppool;
  return true;
 }
}
// reduce memory pool
inline void BigChunkStack::shrink()
{
 const unsigned int nAdjust = MAXSIZE >> 1;
 if (nAdjust >= INIT_SIZE)
 {
  // reduce pool
  char* temppool = (char*)realloc(pool, nAdjust);
  if (NULL == temppool)
   return;
  MAXSIZE = nAdjust;
  pool = temppool;
  return;  
 }
}
#endif

 类BigChunkStack的动态特性与堆内存管理类的动态特性有很大的差异。在创建类BigChunkStack的实例时,它可以包含一 大块内存空间,我们把此内存空间作内存池(pool),内存池的大小是由MAXSIZE来定义。这种技术看起来似乎浪费内存空间。但是,我们应该知道,堆 管理内存后,内存中就会出现碎片,因而,一段时间后,使用过的内存就不是很有用了。当一个程序在运行中需要内存空间,它皆可以把一定数量的元素放入到程序 的堆栈中,使用类BigChunkStack会大有帮助。如果堆栈的生命周期与使用该堆栈的程序的周期相同或相近,效果就更好了。应用这种内存管理方式, 我们可以事先计算程序需要的最大内存的数量,这样可以保证程序正确运行。
 下面,我们进入如何动态扩大内存块的容量的主题上。
 限于篇幅,动态版本的BigChunkStack, 以在原来的基础上加上宏 ADAPTIVE_VER标出部分。

 扩大内存池:应用了一种原用于确定一个对象是否可以压入堆栈中的检查方法,来确认内存池的空间是否充足。
 减小内存池:当占用的内存池的容量仅为25%时,则需要减小。如果内存池的容量比所选的INIT_SIZE大,或与之相等,shrink()就会将内存池容量减小为原来的50%。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值