This is a very long question. The first half is situation/question, and the second half are my poor solutions.
I am working on a game project that, in my opinion, have a neat architecture.
Below is a simplified diagram of the whole architecture.
(No need to concern much about the class / function names.)
From above diagram, all code can be roughly divided into 3 layers. (red/orange/green)
-
Red (The big guys):
EverySystem_XXX
are derived fromSystem_Default
.
They call below layers, and sometimes call other red. -
Orange:
It contains a lot of game-specific library.
In short, they are just a utility to support the red. -
Green:
It is a utility to support red and orange.
It contains all of basic function.
The code is designed to be used by many games.
Most heap allocation (99% of new/delete) happen here.
Here is a snippet show how most functions are called:-
class System_EnemyAI : public System_default{
public: void update(){
EntityPointer entity= .... ;
systemGet<System_Projectile>()->damage(entity);
//^ system always call another system using this template function
TempMathState mathState;
mathState.someFunction( .... );
......
}
//... other member / function
}
class TempMathState{
MyArray<float> stateList; //<-- need dynamic allocation
//...... other member / function
}
Everything works fine until my greedy boss tell me that I should use custom allocators.
The proposal
-
Each
System_XXX
should have its own memory pool to promote data-coherence.
System_XXX
= all system the derived fromSystem_Default
Advantage: Less fragmentation, less cache miss, sound good! -
We know that 99% of function flow can be depicted similar as the black chains of command (below diagram).
-
In the left chain (1), starts at
findThreat()
,
the big guy in this chain isSystem_EnemyAI
.
Therefore, all memory allocation in the chain should use memory pool ofSystem_EnemyAI
. -
In the right chain (2), starts at
update()
,
the last big guy in this chain isSystem_Projectile
(via::damage(entity)
).
Therefore, all memory allocation in the chain should use memory pool ofSystem_Projectile
.
The logic looks reasonable, everyone agrees, end of meeting.
Question
What is the least painful approach to achieve this?
Soft requirement:-
- I don't have to refactor most function of most classes.
- It should support multi-threading.
My poor solutions
All three approaches share a certain disadvantage :-
I have to add at least one statement in every function in System-layer.
Solution 1
Pass custom allocator down to the chain.
Most functions in all layers have to receive allocator
as a parameter.
class System_EnemyAI : public System_default{
public: void update(){
.....
systemGet<System_Projectile>()->damage(this->allocator,entity); //ugly
TempMathState mathState=TempMathState(this->allocator); //ugly
}
}
Disadvantage:
- That would be an exhaustive refactoring!
- All code will look more ugly.
Solution 2A
Set a static global flag.
class System_EnemyAI : public System_default{
public: void update(){
GLOBAL_HEY_USE_ALLOCATOR = this->allocator;
.... other code ...
systemGet<System_Projectile>()->damage(entity); //don't change :)
GLOBAL_HEY_USE_ALLOCATOR = this->allocator;
//^ #steal the flag back,
// because it was stolen by System_Projectile (see below)
TempMathState mathState=TempMathState(); //don't change :)
}
}
It have to do in all function :-
class System_Projectile : public System_default{
public: void damage(EntityPointer entity){
GLOBAL_HEY_USE_ALLOCATOR = this->allocator; //#steal the flag
.... other code ...
}
}
#steal
: because the callee function (System_Projectile
) will set GLOBAL_HEY_USE_ALLOCATOR
, so caller (System_EnemyAI
) have to set GLOBAL_HEY_USE_ALLOCATOR
back.
Any allocation will use the Allocator* GLOBAL_HEY_USE_ALLOCATOR
as a memory pool.
This part is not hard.
Disadvantage:
- It uses global flag, so it doesn't support multi-threading.
- I have to set many flags manually.
Solution 2B
Use RAII to limit scope and manipulate constructor/destructor :-
class System_EnemyAI : public System_default{
public: void update(){
AllocatorScope scope = AllocatorScope(this->allocator);
//^ push "this->allocator" into a global stack
.... other code ...
systemGet<System_Projectile>()->damage(entity); //don't change :)
TempMathState mathState=TempMathState(); //don't change :)
}
};
class System_Projectile : public System_default{
public: void damage(EntityPointer entity){
AllocatorScope scope = AllocatorScope(this->allocator);
.... other code ...
//the "scope" is deleted, so pop stack to recover the previous state
}
};
Disadvantage:
- It still uses global variable, so it doesn't support multi-threading.
Aucun commentaire:
Enregistrer un commentaire