使用对象池加速游戏内存分配

开发 前端
游戏开发中经常需要频繁产生、销毁大量对象,内存本身够不够用是一方面,尤其是在手机等内存本来就有限的设备上面,另外一点是分配的速度不会对游戏体验造成影响,也就是不能影响帧率。

游戏开发中经常需要频繁产生、销毁大量对象,内存本身够不够用是一方面,尤其是在手机等内存本来就有限的设备上面,另外一点是分配的速度不会对游戏体验造成影响,也就是不能影响帧率。

相比内存池,对象池更易用更容易管理,而且还可以利用脏数据,也就是上次被回收掉的对象的数据。而且偶尔的空间分配失败其实不是那么重要(后面会讲怎么在会失败的情况下完成分配任务),游戏中还是速度更重要些。

原理

一次申请大量连续内存(整数个对象大小),最好用堆,当然如果用栈数组也没人拦你,栈空间可是相当有限…

由于分配的对象生存期是不固定的(如下图),池不可能保持已分配对象的连续性,这时进行块移动会降低程序效率。

  分配       分配 分配   分配  

所以需要把闲置对象的指针放入容器中来管理。此容器必须能快速存取删,而且不需要频繁大距离移动容器元素指针,最好是刚从容器中释放的元素能马上让 下一个元素使用,这时候栈就是一个很好的选择了。初始时将所有闲置对象指针压入栈,分配时pop,栈为空时返回空;释放时将对象指针push进栈即可。

实现

其实boost已经提供了对象池了,那为什么还要自己实现一个呢?当然是要方便DIY了…其实你也可以用boost的对象池来第二次封装

这部分直接参看附件源码吧

使用

这才是真正的重点

分配时直接用Sobot* p = ObjPool<Sobot>::alloc()?不,还应该使用placement new调用其构造函数:

new(p) Sobot()

你想在你的代码中充斥大量这样的代码吗?放到工厂里面也许是一种办法,但是工厂引用到了对象池了。而大师告诉我们好的设计要保持职责单一,用与不用对象池应该不影响原系统的正常运行。而且还有一点,用这种办法,就只能和某些组件绝缘了,比如智能指针。

此时重载new与delete就至关重要了:

  1. static void* operator new(size_t) { 
  2.  
  3.     return SobotPool::instance().alloc(); 
  4.  
  5.  
  6. static void operator delete(void* p) { 
  7.  
  8.     SobotPool::instance().free(reinterpret_cast<Sobot*>(p)); 
  9.  

一个对象中往往充斥着大量指针,而这些指针指向的空间往往大于包含他们的对象本身。如果将这些指针所在在类也应用对象池,一方面是池的容量你无法估 计,另一方面是使用起来麻烦。而且你也无法向上面这样给每个类注入new与delete的重载。用代理?呵呵,项目中估计会出一堆问题。这时候我们不妨使 用脏数据,也就是说对象池中保存的对象全是可以直接使用的对象,而并非空对象,对象中的成员指针变量引用到的内存不在池中。为了保证安全,清空这些内存在 池销毁时进行。

和上面的功能一起,我们可以定义一个宏,免得每次使用都得重复大量代码。如下:

  1. #define USING_DIRTY_DATA true 
  2.  
  3. // 如果不是方便测试需要,可以将这行 
  4.  
  5. // typedef ObjPool<obj_class, max_size> obj_class##Pool; \ 
  6.  
  7. // 标注为private 
  8.  
  9. #define DECLARE_USING_OBJ_POOL(obj_class, max_size, _using_dirty_data) \ 
  10.  
  11.     public: \ 
  12.  
  13.         typedef ObjPool<obj_class, max_size> obj_class##Pool; \ 
  14.  
  15.         friend class obj_class##Pool; \ 
  16.  
  17.         static const bool using_dirty_data = _using_dirty_data; \ 
  18.  
  19.     public: \ 
  20.  
  21.     ~obj_class() { \ 
  22.  
  23.         if (!_using_dirty_data) {this->purge();} \ 
  24.  
  25.     } \ 
  26.  
  27.     static void* operator new(size_t) { \ 
  28.  
  29.         return obj_class##Pool::instance().alloc(); \ 
  30.  
  31.     } \ 
  32.  
  33.     static void operator delete(void* p) { \ 
  34.  
  35.         obj_class##Pool::instance().free(reinterpret_cast<obj_class*>(p)); \ 
  36.  
  37.     } \ 
  38.  
  39.     static bool loadCache() { \ 
  40.  
  41.         while (true) { \ 
  42.  
  43.             obj_class* obj = new obj_class; \ 
  44.  
  45.             if (obj != NULL) { \ 
  46.  
  47.                 if (!obj->init()) { \ 
  48.  
  49.                     return false; \ 
  50.  
  51.                 } \ 
  52.  
  53.             } else { \ 
  54.  
  55.                 break; \ 
  56.  
  57.             } \ 
  58.  
  59.         }; \ 
  60.  
  61.         obj_class##Pool::instance().freeAll(); \ 
  62.  
  63.         return true; \ 
  64.  
  65.     } 

调用时在类中加入如下代码:

  1. // DECLARE_USING_OBJ_POOL(Bullet, BULLET_POOL_VOLUM, (NOT USING_DIRTY_DATA)) 
  2.  
  3. DECLARE_USING_OBJ_POOL(Bullet, BULLET_POOL_VOLUM, USING_DIRTY_DATA) 

LoadCache是游戏加载阶段调用的,这里将进行所有池对象的初始化。为此,你还需要实现init和purge函数,分别是初始资源,销毁资源 的,这些其实都只会被调用一次的。像状态的初始化,大可放构造函数中,每次使用对象构造函数都会被调用的。外界是不能直接操作pool的。

如果池容量过小,分配失败其实并不可怕。

见例子:

  1. // 大规模测试 
  2.  
  3.  list<Entity*> timer; 
  4.  
  5.  struct _Timer{ 
  6.  
  7.      list<Entity*>& _timer; 
  8.  
  9.      _Timer(list<Entity*>& timer) : _timer(timer) {} 
  10.  
  11.      void operator()() { 
  12.  
  13.          for (list<Entity*>::iterator iter = _timer.begin(); 
  14.  
  15.              iter != _timer.end();) { 
  16.  
  17.              Entity* entity = *iter; 
  18.  
  19.              if (entity->isValid()) { 
  20.  
  21.                  (*iter)->update(); 
  22.  
  23.              } else { 
  24.  
  25.                  entity->destroy(); 
  26.  
  27.                  iter = _timer.erase(iter); 
  28.  
  29.                  continue
  30.  
  31.              } 
  32.  
  33.              ++iter; 
  34.  
  35.          } // end for 
  36.  
  37.      } 
  38.  
  39.  } update_timer(timer); 
  40.  
  41.  const int num = 50
  42.  
  43.  log << endl << "大规模测试:" << endl; 
  44.  
  45.  for (int i = 0; i < num; ++i) { 
  46.  
  47.      Entity* entity = ObjManager<Entity>::instance().make("Bullet"); 
  48.  
  49.      if (IS_VALID_POINTER(entity)) { 
  50.  
  51.          log << "  alloced index:" << i << endl; 
  52.  
  53.          timer.push_back(entity); 
  54.  
  55.      } else { 
  56.  
  57.          log << "  alloc bullet failed, waiting..." << endl; 
  58.  
  59.          // 失败了就多尝试一次,反正任务量是20个 
  60.  
  61.          --i; 
  62.  
  63.      } 
  64.  
  65.      update_timer(); 
  66.  
  67.  } 
  68.  
  69.  // 不管使用什么模式都要自己回收所有的对象, 
  70.  
  71.  // 不要依赖于池析构时的对象释放 
  72.  
  73.  for (list<Entity*>::iterator iter = timer.begin(); 
  74.  
  75.      iter != timer.end(); ++iter) { 
  76.  
  77.      (*iter)->destroy(); 

池容量为3,这是运行结果:

[0sec] 加载缓存
[0sec] Bullet1 with HP:2
[0sec] init Bullet1
[0sec] Bullet2 with HP:3
[0sec] init Bullet2
[0sec] Bullet3 with HP:5
[0sec] init Bullet3
[0sec]
大规模测试:
[0sec] Bullet10 with HP:5
[0sec]   alloced index:0
[0sec] Bullet11 with HP:1
[0sec]   alloced index:1
[0sec] Bullet12 with HP:1
[0sec]   alloced index:2
[0sec] destroy entity11
[0sec] Bullet13 with HP:2
[0sec]   alloced index:3
[0sec] destroy entity12
[0sec] Bullet14 with HP:3
[0sec]   alloced index:4
[0sec]   alloc bullet failed, waiting...
[0sec] destroy entity10
[0sec] destroy entity13
[0sec] Bullet15 with HP:2
(这里省略很多行…)
[1sec]   alloced index:46
[1sec] Bullet57 with HP:4
[1sec]   alloced index:47
[1sec]   alloc bullet failed, waiting...
[1sec] destroy entity55
[1sec] Bullet58 with HP:2
[1sec]   alloced index:48
[1sec]   alloc bullet failed, waiting...
[1sec]   alloc bullet failed, waiting...
[1sec] destroy entity56
[1sec] destroy entity57
[1sec] destroy entity58
[1sec] Bullet59 with HP:5
[1sec]   alloced index:49
[1sec] destroy entity59
[1sec]
释放池
[1sec] purge Bullet59
[1sec] freeing sprite buf. size:3
[1sec] purge Bullet56
[1sec] freeing sprite buf. size:2
[1sec] purge Bullet57
[1sec] freeing sprite buf. size:1
请按任意键继续. . .

附件下载

 

责任编辑:陈四芳 来源: cguage.com
相关推荐

2018-02-08 14:57:22

对象内存分配

2018-04-08 08:45:53

对象内存策略

2015-11-16 11:22:05

Java对象内存分配

2021-03-22 11:51:22

Java内存栈上

2020-06-04 12:15:37

Go内存池对象池

2021-07-30 07:22:51

JVM虚拟机栈 Stack

2021-11-29 05:32:47

内存规避安全工具恶意软件

2021-07-14 10:00:32

Python内存测量

2010-09-25 14:12:50

Java内存分配

2021-02-28 13:22:54

Java内存代码

2022-03-16 08:39:19

StackHeap内存

2013-10-12 13:01:51

Linux运维内存管理

2011-05-25 09:58:46

C#

2023-10-18 13:31:00

Linux内存

2022-03-07 10:54:34

内存Linux

2018-02-07 16:23:58

连接池内存池AI

2022-01-13 10:30:21

C语言内存动态

2011-07-15 01:10:13

C++内存分配

2021-12-16 06:52:33

C语言内存分配

2023-03-26 00:43:42

JVM对象测试
点赞
收藏

51CTO技术栈公众号