使用對象池加速游戲內存分配
游戲開發中經常需要頻繁產生、銷毀大量對象,內存本身夠不夠用是一方面,尤其是在手機等內存本來就有限的設備上面,另外一點是分配的速度不會對游戲體驗造成影響,也就是不能影響幀率。
相比內存池,對象池更易用更容易管理,而且還可以利用臟數據,也就是上次被回收掉的對象的數據。而且偶爾的空間分配失敗其實不是那么重要(后面會講怎么在會失敗的情況下完成分配任務),游戲中還是速度更重要些。
原理
一次申請大量連續內存(整數個對象大小),最好用堆,當然如果用棧數組也沒人攔你,棧空間可是相當有限…
由于分配的對象生存期是不固定的(如下圖),池不可能保持已分配對象的連續性,這時進行塊移動會降低程序效率。
分配 | 分配 | 分配 | 分配 |
所以需要把閑置對象的指針放入容器中來管理。此容器必須能快速存取刪,而且不需要頻繁大距離移動容器元素指針,最好是剛從容器中釋放的元素能馬上讓 下一個元素使用,這時候棧就是一個很好的選擇了。初始時將所有閑置對象指針壓入棧,分配時pop,棧為空時返回空;釋放時將對象指針push進棧即可。
實現
其實boost已經提供了對象池了,那為什么還要自己實現一個呢?當然是要方便DIY了…其實你也可以用boost的對象池來第二次封裝
這部分直接參看附件源碼吧
使用
這才是真正的重點
分配時直接用Sobot* p = ObjPool<Sobot>::alloc()?不,還應該使用placement new調用其構造函數:
new(p) Sobot()
你想在你的代碼中充斥大量這樣的代碼嗎?放到工廠里面也許是一種辦法,但是工廠引用到了對象池了。而大師告訴我們好的設計要保持職責單一,用與不用對象池應該不影響原系統的正常運行。而且還有一點,用這種辦法,就只能和某些組件絕緣了,比如智能指針。
此時重載new與delete就至關重要了:
- static void* operator new(size_t) {
- return SobotPool::instance().alloc();
- }
- static void operator delete(void* p) {
- SobotPool::instance().free(reinterpret_cast<Sobot*>(p));
- }
一個對象中往往充斥著大量指針,而這些指針指向的空間往往大于包含他們的對象本身。如果將這些指針所在在類也應用對象池,一方面是池的容量你無法估 計,另一方面是使用起來麻煩。而且你也無法向上面這樣給每個類注入new與delete的重載。用代理?呵呵,項目中估計會出一堆問題。這時候我們不妨使 用臟數據,也就是說對象池中保存的對象全是可以直接使用的對象,而并非空對象,對象中的成員指針變量引用到的內存不在池中。為了保證安全,清空這些內存在 池銷毀時進行。
和上面的功能一起,我們可以定義一個宏,免得每次使用都得重復大量代碼。如下:
- #define USING_DIRTY_DATA true
- // 如果不是方便測試需要,可以將這行
- // typedef ObjPool<obj_class, max_size> obj_class##Pool; \
- // 標注為private
- #define DECLARE_USING_OBJ_POOL(obj_class, max_size, _using_dirty_data) \
- public: \
- typedef ObjPool<obj_class, max_size> obj_class##Pool; \
- friend class obj_class##Pool; \
- static const bool using_dirty_data = _using_dirty_data; \
- public: \
- ~obj_class() { \
- if (!_using_dirty_data) {this->purge();} \
- } \
- static void* operator new(size_t) { \
- return obj_class##Pool::instance().alloc(); \
- } \
- static void operator delete(void* p) { \
- obj_class##Pool::instance().free(reinterpret_cast<obj_class*>(p)); \
- } \
- static bool loadCache() { \
- while (true) { \
- obj_class* obj = new obj_class; \
- if (obj != NULL) { \
- if (!obj->init()) { \
- return false; \
- } \
- } else { \
- break; \
- } \
- }; \
- obj_class##Pool::instance().freeAll(); \
- return true; \
- }
調用時在類中加入如下代碼:
- // DECLARE_USING_OBJ_POOL(Bullet, BULLET_POOL_VOLUM, (NOT USING_DIRTY_DATA))
- DECLARE_USING_OBJ_POOL(Bullet, BULLET_POOL_VOLUM, USING_DIRTY_DATA)
LoadCache是游戲加載階段調用的,這里將進行所有池對象的初始化。為此,你還需要實現init和purge函數,分別是初始資源,銷毀資源 的,這些其實都只會被調用一次的。像狀態的初始化,大可放構造函數中,每次使用對象構造函數都會被調用的。外界是不能直接操作pool的。
如果池容量過小,分配失敗其實并不可怕。
見例子:
- // 大規模測試
- list<Entity*> timer;
- struct _Timer{
- list<Entity*>& _timer;
- _Timer(list<Entity*>& timer) : _timer(timer) {}
- void operator()() {
- for (list<Entity*>::iterator iter = _timer.begin();
- iter != _timer.end();) {
- Entity* entity = *iter;
- if (entity->isValid()) {
- (*iter)->update();
- } else {
- entity->destroy();
- iter = _timer.erase(iter);
- continue;
- }
- ++iter;
- } // end for
- }
- } update_timer(timer);
- const int num = 50;
- log << endl << "大規模測試:" << endl;
- for (int i = 0; i < num; ++i) {
- Entity* entity = ObjManager<Entity>::instance().make("Bullet");
- if (IS_VALID_POINTER(entity)) {
- log << " alloced index:" << i << endl;
- timer.push_back(entity);
- } else {
- log << " alloc bullet failed, waiting..." << endl;
- // 失敗了就多嘗試一次,反正任務量是20個
- --i;
- }
- update_timer();
- }
- // 不管使用什么模式都要自己回收所有的對象,
- // 不要依賴于池析構時的對象釋放
- for (list<Entity*>::iterator iter = timer.begin();
- iter != timer.end(); ++iter) {
- (*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 請按任意鍵繼續. . . |
附件下載