mardi 31 janvier 2017

Adopt custom allocators into well-managed code - chain of command (using RAII?)

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.)

enter image description here

From above diagram, all code can be roughly divided into 3 layers. (red/orange/green)

  • Red (The big guys):
    Every System_XXX are derived from System_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 from System_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).

enter image description here

  • In the left chain (1), starts at findThreat(),
    the big guy in this chain is System_EnemyAI.
    Therefore, all memory allocation in the chain should use memory pool of System_EnemyAI.

  • In the right chain (2), starts at update(),
    the last big guy in this chain is System_Projectile (via ::damage(entity)).
    Therefore, all memory allocation in the chain should use memory pool of System_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