我要发帖 回复

新手上路

1

主题

6

积分

0

专家分

:

私信
发表时间 : 2007-12-8 20:14:00 | 浏览 : 1455    评论 : 2
视景仿真引擎中内存分配与管理技术探析


要:
内存的分配与管理是视景仿真引擎中底层的核心技术之一,能否高效地利用和管理内存直接关系着视景仿真应用的实时性和真实性。结合C++语言对视景仿真引擎中的内存分配与管理技术进行了详细的分析和探讨,提出了层次性内存分配的概念和利用内存池对内存进行高效管理的方法。最后,实现了一个功能较为完善的内存管理器,利用该内存管理器不仅可以获得使用内存的灵活性,而且可以极大地提高视景仿真引擎的性能。研究成果可直接应用于视景仿真引擎的开发之中。

关键词:视景仿真;引擎;内存分配与管理;内存管理器;内存池
         



1
  
当前,信息技术飞速发展,三维可视化视景仿真技术在科研、生产、生活中的应用越来越广泛,尤其是在城市规划、作战模拟、三维游戏开发等领域表现的异常突出。三维视景仿真的关键在于视景仿真引擎,视景仿真引擎指的是用于驱动、控制、管理虚拟场景并支持快速复杂的视景仿真程序、快速创建各种实时交互的三维环境、快速建立大型沉浸式或非沉浸式的虚拟现实系统的软件平台[7][10]。
目前,国外的视景仿真引擎已逐步实现了商业化,如Vega、Vega Prime、VTree、OpenGVS等渲染引擎[10][11]。这些引擎大都采用了最新的计算机图形学技术,在图形图像表现、运行效率、上层应用开发便捷性和可伸缩性上都表现的非常优异。
相对而言,在国内,视景仿真引擎的研究起步较晚,到目前还没有出现严格意义上的完全自主研发的引擎,基本上还是基于国外一些开源引擎在作进一步的研究与发展[10][11]。并且所设计的引擎大多是针对某一个或者某一类型的仿真,要真正达到引擎的通用性和可扩展性的商业化层次,还需要很长的路要走。随着国内三维视景仿真市场的快速发展,对符合我国国情的引擎的需求也在不断增加,迫切需要国人自主知识产权的视景仿真引擎。
视景仿真引擎作为三维视景仿真平台的核心,对视景仿真应用品质的好坏具有决定性的意义,尤其体现在视景仿真程序运行的高效低耗性上。由于三维视景仿真对计算机系统资源消耗较大,所以在图形渲染内核中,如何有效地组织数据存储方式、设计渲染算法、进行资源管理等将对视景仿真程序运行的效率起着巨大的影响。而这些又无疑与引擎中底层的核心技术——内存的分配与管理有着直接的联系。因此,能否高效的利用和管理内存也就成了衡量视景仿真引擎性能的一项重要指标。本文以C++语言为例,着重对视景仿真引擎中的内存分配与管理技术进行详细的分析和探讨。
2
内存分配的空间

2.1栈分配
栈是一个有次序的空间,新元素一般追加在栈的顶部,出栈时按“后进先出”的原则进行。在程序运行期间,如果需要创建的对象仅仅打算在限定的范围内使用,那么在栈里创建这些对象就行了,如传递给函数的参数或者声明在函数内的局部变量都是在栈里创建的。但是当创建的对象并不是临时的或者它没有限定在一定使用范围之内时,就需要采用另一种分配方法——堆分配来为对象分配内存空间。
2.2堆分配
堆分配内存是通过使用运算符new或者malloc进行并分别通过delete或者free来释放。堆分配内存无任何次序可言,它没有任何规则限制。也正因如此,堆成为了以下麻烦问题的源头:内存碎片、悬指针、内存泄漏等。
内存碎片是由于堆分配的性质而导致的【1】。堆的原始状态是一块很大的、连续的内存区,不管请求有多大,堆都是同样对待,随着越来越多的、大小不一的内存被随机的分配和释放,也就导致了堆由一块连续的内存块演变为一块块的小片段,与此同时,还会有一定比例的没有被使用的内存分散在这些较小的碎片中。到最后,可能只是需要申请单个的内存,分配都会失败。失败的原因不是由于剩余的空间不够了,而是因为没有一块单独的空间能够满足这样大小的内存请求,这也就是所谓的内存碎片问题。
悬指针是指一个指针,它用来引用一个有效的特定区域,但是后来由于某种原因,该区域被释放了,而指针却没有及时地进行更新。此时即会产生悬指针现象,而此时试图通过该指针访问其指向的内容时,就会产生异常【1】。
内存泄漏是指有程序分配了一个内存块,但是程序又“忘记”了这个内存块,亦即没有将控制权返回给系统【1】。这样随着程序的运行,内存的消耗会越来越大,内存碎片会越来越多,最终导致内存的耗尽。或者如果系统使用了虚拟内存,就会迫使系统把内存倒入硬盘里,这样会导致程序的运行速度严重变慢。
由于以上堆分配过程中出现的问题,因此必须对堆内存分配进行有效的管理,从而高效地使用有限而又宝贵的内存资源。
3
内存分配的方式

3.1静态分配
静态分配是指在对象被创建之前提前为对象分配所需的固定大小的内存空间。如:
#define MAX_PARTICLES 1000
PARTICLE s_Particle [MAX_PARTICLES];
这个分配方式有一个明显的优点就是可以避免产生内存碎片和潜在的内存用光的情况,这是由于所有对象都是被编译器静态分配的,在整个视景仿真程序运行期间不会发生改变。另外一个优点就是静态对象的初始化非常直观,便于跟踪内存以及了解每一种数据类型使用了多少内存。但是此种分配方式的缺点也是很明显的,其一就是会导致内存的浪费。必须提前决定视景仿真程序的每个方面需要使用多少内存,如需为模型、纹理、粒子特效等分配多少内存。若一个视景仿真程序有许多绚丽多彩的屏幕表现,并且内容也需要动态更新,如爆炸特效、火焰、灰尘以及寻路的临时信息等,采用静态分配方式则必须对这些对象提前分配好空间,那么在内存的使用上将会造成很大的浪费。如若采用动态分配的方式,只有当前需要的东西才为其分配内存,如炮弹命中目标时,才为产生的爆炸效果分配内存空间,而爆炸消失后再将内存空间进行回收,这将大大提高内存的使用效率。另外需要指出的是提前预知一个视景仿真程序有多少对象也是不可能的,那么提前为这些对象分配内存空间也就无从谈起;其二、采用静态分配的对象只会提前被创建但并未进行初始化,这就意味着需要额外编写代码进行专门的对象初始化工作,以及在不释放对象的前提下多次关闭对象。动态分配对象则没有如此麻烦,它会在对象第一次构造的时候初始化,也会在销毁的时候调用析构函数关闭自己。
3.2动态分配
动态分配是指在程序运行过程中,实时的根据需要为对象分配内存空间,如:
SpecialEffect *pEffect = new SpecialEffect ( );
它只为当前程序需要提供内存空间,而对一些无用的废弃空间进行回收,这在视景仿真中有相当大的需求。采用此方式可以极大地增强内存使用的灵活性,但也正因如此而导致产生一系列诸如内存碎片、悬指针、内存泄漏等相关问题。
综上所述,静态内存分配避免了一些动态分配的缺点,但却丧失了内存使用的灵活性,而动态内存分配拥有了这种灵活性却导致了一系列问题的产生。那么,究竟如何使用就要取决于具体的情况。如果一个视景仿真程序是一个静止的场景,那么为对象静态分配内存是有利的。然而,对于目前的大多数视景仿真程序,总希望能与三维虚拟场景中的所有对象进行交互,如炮弹命中目标时要伴随有爆炸、烟雾、火光等,这些情况动态分配可以更好地满足。既然静态分配和动态分配各有所长,那么集各自所长,将它们结合起来使用岂不是更好。即在视景仿真程序运行过程中不会改变的对象采用静态的创建,其它一些会改变的对象动态的创建。此种想法固然可行,但往往会带来更糟糕的问题。对那些动态创建的对象,不得不应对它带来的性能问题和碎片问题。对于那些静态分配的对象,还要保证这些对象有单独的初始化过程和关闭的过程,于是,除非释放的过程是自动的,否则假如忘记了某个对象到底是静态分配的还是动态分配的,而又以错误的方式释放了它,这将导致更为严重的后果。
与混合这两种内存分配方式的办法相反,一个更好的办法就是仅仅采用动态内存分配,并对所引发的问题采用相关的技术进行克服,对性能敏感的内存分配使用内存池技术,这将极大地提高内存使用的效率,从而为视景仿真程序的运行提供一个更加安全、可靠的平台。
4
内存管理器的设计与实现

一个功能全面的内存管理器不仅能够克服上面提到的内存碎片、悬指针、内存泄漏等问题,而且还要能提供足够的内存使用信息,如某个内存块是由哪一部分代码创建的、什么时间创建的、这块内存有多大等这些原始的分配情况以及一个相对比较高层的情况如有多少内存用于纹理、多少用于几何形体或者多少用于寻路算法等,这种类型的内存情况报告在视景仿真引擎的开发中相当重要,尤其对于调整仿真级别关卡时的内存消耗更是如此。下面就内存管理器涉及到的关键技术进行具体的分析。
4.1运算符new和delete
在请求一个内存分配时,每一个对象的创建都是从这样的代码开始的:
SpecialEffect *pEffect = new SpecialEffect( );
而在编译时编译器会在内部替换这样的调用为两个单独的调用,一个是分配正确数量的内存,另一个是调用SpecialEffect的构造函数。即:
SpecialEffect *pEffect = _new (sizeof (SpecialEffect));
pEffect -> SpecialEffect ( );
在大多数new运算的标准实现里,全局运算符new仅仅是调用malloc函数:
void *operator new (unsigned int nSize)
{
return malloc (nSize);
}
在此,调用序列并未结束,malloc并不是一个原子操作。相反,它将调用平台相关的内存分配函数,在堆里为它分配正确数量的内存。通常,这将导致好几个函数的调用,还有代价高昂的算法来查找合适的未被使用的内存块。全局运算符delete的处理流程与此相似,但是它调用的是析构函数和free函数。这样的分配过程根本无法对其进行控制,为了在分配过程中得到所需要的信息,往往需要重载运算符new和delete。
4.1.1全局运算符new和delete
为了说明内存使用的偏好,首先创建一个类Heap
class Heap
{
public:

Heap (const char *name);


const char *GetName( ) const ;

private:

char m_Name[NAMELENGTH];

};
另外,创建一结构体用于获取内存块的信息:
struct AllocHeader
{

Heap *pHeap;


int
nSize;

};
那么全局的运算符new和delete大概是:
void *operator new ( size_t size, Heap *pHeap )
{
size_t nRequestBytes =

size + sizeof( AllocHeader );

char *pMem = (char) malloc( nRequestBytes );
AllocHeader *pHeader = (AllocHeader *) pMem;
pHeader -> pHeap = pHeap;
pHeader -> nSize = size;
pHeap -> AddAllocation( size );
void *pStartMemBlock =

pMem + sizeof( AllocHeader );

return pStartMemBlock;
}
void operator delete( void *pMem )
{
AllocHeader *pHeader = (AllocHeader*)
((char) pMem-sizeof(AllocHeader));
PHeader -> pHeap ->
RemoveAllocation( pHeader->nSize );
free( pHeader );
}
现在,在任何时候都可以遍历所有的heap,打印出它们的名字,如每个heap分配了几块内存、每块内存的大小、内存使用的高峰值等信息。
4.1.2具体类的运算符new和delete
使用具体类的new和delete运算符,可以使内存管理器的某些工作自动化。由于通常把一个类的所有对象放到一个特定的堆里,所以可以让类的new和delete运算符重载全局的new和delete。以后在创建一个类的对象的时候,就会自动地放到正确的堆里,如:
// Object.h
class Object
{
public:
static void *operator new( size_t size );
static void operator delete( void *p, size_t size );
private:
static Heap *s_pHeap;
};
//Object.cpp
Heap *Object::s_pHeap = NULL;
void *Object::operator new( size_t size )
{
if( NULL == s_pHeap )
{

s_pHeap = HeapFactory::Createheap( “Object” );

}
return ::operator new( size, s_pHeap );
}
void Object::operator delete( void *p,size_t size )
{

::operator delete( p );

}
为避免敲错代码同时出于代码简洁的考虑,可以采用宏的形式来完成同样的功能,那么上面的类将变为:
//Object.h
class Object
{

private:


DECLARE_HEAP;

};
//Object.cpp
DEFINE_HEAP(Object, “Object” );
这样,任何一个由父类派生出的新类都会有着和父类一样的new和delete运算符,除非它们又重载了new和delete运算符。在上面的例子里,如果从Object类派生出一个新类ObjectWeapon,那么该类将使用Object的堆。新类像钩子一样“钩住”了内存管理系统,为引擎中所有比较重要的类使用此项技术可以使引擎在内存的使用上保持较好的性能。
4.2错误检查
出于内存管理器可能出现的错误以及内存误用的考虑,必须进行错误检查,以确保要释放的内存是由内存管理器分配的。
在实现该功能时,只需为结构体AllocHeader添加一个特别的整数标示nSignature并为其取一个容易识别的值即可,此处取为0xDEADCODE。
在运算符new和delete的实现里,添加以下代码:
//new运算符
pHeader->nSignature = 0xDEADCODE;
//delete运算符
assert ( pHeader-> nSignature == 0xDEADCODE );
4.3遍历堆
为了检查内存的连续性,需要提供遍历一个堆的所有内存分配区域的功能,这样不仅可以搜集更多的信息,而且还能消除内存碎片。实现该功能需要为结构体AllocHeader添加两个字段:
AllocHeader *pNext;
AllocHeader *pPrevior;
它们分别指向下一个分配区域和前一个分配区域,这样通过该双向链表就可以实现遍历整个堆。
4.4内存书签和内存泄漏
找出内存泄漏从原理上来说很简单。在某个时刻,给内存的使用情况作个标记(内存书签),过一段时间,再做一个标记(内存标签),同时列出第二次分配的内存即可查出内存是否泄漏。
实现时需要做的也就是加一个保存分配次数的计数器即可,每一次分配内存的时候就递增该变量。此时需要给结构体AllocHeader添加一个整形变量nAllocNumber。
最后需要的是报告内存泄漏的函数,该函数需要两个参数,分别记录内存地址的开始和结束,函数要能报告出这段区间内存内依然有效的已分配内存块。该函数在实现时,只需遍历堆里所有的已经分配的内存块,看看其地址是否在指定的内存起始地址和结束地址之间就行了。
4.5层次性的堆
把堆设计成具有层次性结构的一个突出优点就是便于管理同类对象并且易于了解其使用内存的情况。在设计时,一般采用树型结构,相同属性的对象有一个共同的父类,为每个对象分配的内存都处于父类所在的内存堆中。一个典型的使用层次性结构的堆的内存使用情况如表1所示:
表1 层次性结构的堆的内存使用情况表


本地

全部

内存

峰值

对象实例

内存

峰值

对象实例

渲染

0

0

0

38536

40036

4341


0

0

0

16304

17804

3790

材质

0

0

0

22232

22232

551

纹理

220

229

230

220

229

230


4.6内存池
到目前为止,内存管理器已基本克服了堆分配过程中出现的问题,并且它还可以提供足够的内存使用信息,唯一需要的就是如何提高内存分配的性能问题。解决该问题的一个常用的方法就是使用内存池技术,内存池是一块预先分配好的内存,其内存大小一定,可以在仿真程序运行时为对象分配一定数量的内存,一旦这些对象被程序释放,它们使用的内存并不归还给堆,而是归还给了内存池。如再有内存分配请求的时候,内存池会直接返回已预先分配好的内存中的第一个未使用的块,这就避免了在查找未使用的内存堆上付出较高的代价。
在仿真程序运行的过程中,使用内存池只需进行一次内存分配,这部分内存从不释放,这样做不仅可以避免出现内存碎片,而且还减少了堆内存分配的数量,从而可获得较好的性能。另外,使用内存池一个附带的好处就是可以一次性的释放掉内存池中的所有对象,而无需调用它们的析构函数。
实现内存池相对比较复杂,此处只列出其大概的类的定义:
class MemoryPool
{
public:
MemoryPool( size_t nObjectSize );
~MemoryPool ( );
void *Alloc ( size_t nSize );
void Free ( void *p, size_t
nSize );

}
对于预分配的内存块,可以采用链表的结构进行管理。当调用Alloc成员函数的时候,就返回空闲内存片链表的第一个内存块的地址,当调用Free的时候,再将这些内存加到空闲内存片链表的头部即可。在把内存池整合到内存管理器中时,只需采用具体类的new和delete运算符中介绍的两个宏即可。图1显示的是使用内存管理器为对象进行内存分配的情况(为查看内存泄漏,留一对象未删除)。



1.jpg
图1 内存分配情况图

5
结论

内存的分配与管理是视景仿真渲染引擎中底层的核心技术之一,能否高效地利用和管理内存直接关系着视景仿真应用的实时性和真实性。对于视景仿真渲染引擎中的内存分配与管理而言,利用内存管理器不仅可以获得使用内存的灵活性,而且可以极大地提高渲染引擎的性能。运用本文介绍的理论和方法,可以较为方便的开发一个内存管理器并将其整合到所开发的渲染引擎之中。

参考文献:
[1]
Noel Llopis. C++ for Game Programming [M], Premier Press, 2003

[2]
Meyers, Scott. Effective C++ Second Edition [M], Addision-Wesley, 1998

[3]
http://www.memorymanagement.org/

[4]
http://www.boost.org/libs/pool/doc/index.html

[5]
Alexandrescu Andrei, Modern C++ Design[M], Addision-Wesley, 2001

[6]
Stefan Zerbst, Oliver Duvel. 3D Game Engine Programming [M], Premier Press, 2004

[7]
Lars Bishop,Dave Eberly, March Finch etc. Designing a PC Game Engine[J], Computer Graphics in Entertainment.1998(January)

[8]
Erik Bethke. Game Development and Production[M],Wordware Publishing,Inc.2003

[9]
David H Eberly. 3D Game Engine Design[M],Magic Software,Inc.2001

[10]
王乘,李利军,周均清,陈大炜. Vega实时三维视景仿真技术[M].

武汉:华中科技大学出版社,2005
[11]
龚卓容. Vega程序设计[M].北京:国防工业出版社,2002

[12]
郑莉,董渊. C++语言程序设计[M]. 北京:清华大学出版社,2001









[ 本帖最后由 obuil 于 2007-12-8 08:55 PM 编辑 ]

最近VR访客

obuil 评论于2007-12-8 20:40:43
帮你把附件贴了出来
不错的东西

[ 本帖最后由 obuil 于 2007-12-8 08:48 PM 编辑 ]
chenweili2005 评论于2008-10-8 08:54:57
好东东,谢谢分享!

手机版|VR开发网 统计 津ICP备18009691号
网安备12019202000257

GMT+8, 2021-10-29 04:45 AM

返回顶部