Asset Manager Memory Optimization Design


Overview

CAssetManager is a singleton that stores various resource managers required for game execution, such as textures, sprites, animations, UI, fonts, and sounds. Through this, all necessary resources can be accessed and managed consistently. During the development, I considered several different implementation approaches, and I’ve summarized their pros and cons below.

⚠️ Note: During framework initialization, CDataLoader reads CSV data and passes it to the various managers within CAssetManager, which then use this data for resource management at runtime.

View Repository


Design and Implementation Approaches | Full View

Three implementation approaches considered during the asset manager design are shown in the table and diagram below.

Implementation MethodsProsCons
(1) CAssetManager dynamically allocated, all resource managers dynamically allocated separately.low compile dependencyhigh memory fragmentation
(2) CAssetManager dynamically allocated, all resource managers declared as value member variables.low memory fragmentationhigh compile dependency
(3) Memory block allocated with malloc, CAssetManager and all resource managers created with placement new.low compile dependency,
no memory fragmentation
more complex to implement

After carefully considering the pros and cons, I ultimately chose approach (3).


Code Breakdown

template <typename T>
T* PlacementNew(void*& memoryBlock)
{
    T* manager  = new (memoryBlock) T;
    memoryBlock = (char*)memoryBlock + sizeof(T);
    
    return manager;
}

  • The PlacementNew<T>() function creates an object of type T at the specified memoryBlock location using placement new, and then moves the memory block pointer to the next position for subsequent objects.

CAssetManager::CAssetManager(void* memoryBlock)
{
    mTextureManager   = PlacementNew<CTextureManager>(memoryBlock);
    mSpriteManager    = PlacementNew<CSpriteManager>(memoryBlock);
    mAnimationManager = PlacementNew<CAnimationManager>(memoryBlock);
    mUIManager        = PlacementNew<CUIManager>(memoryBlock);
    mFontManager      = PlacementNew<CFontManager>(memoryBlock);
    mSoundManager     = PlacementNew<CSoundManager>(memoryBlock);
}

  • This constructor takes a memory block allocated with malloc and uses PlacementNew<T>() to create all resource managers sequentially in contiguous memory.

CAssetManager* CAssetManager::GetInst()
{
    if (!mInst)
    {
        const size_t totalSize = sizeof(CAssetManager)
            + sizeof(CTextureManager)   + sizeof(CSpriteManager)
            + sizeof(CAnimationManager) + sizeof(CUIManager)
            + sizeof(CFontManager)      + sizeof(CSoundManager);
		
        void* memoryBlock = malloc(totalSize);
        mInst = new (memoryBlock) CAssetManager((char*)memoryBlock + sizeof(CAssetManager));
    }
    return mInst;
}

  • This function returns the CAssetManager singleton object. On the first call, it allocates memory for CAssetManager and all resource managers at once using malloc, and initializes them using placement new.

Conclusion

Initially, I implemented approaches (1) and (2), but their drawbacks were clear. I then chose approach (3), which preserves their advantages and fixes the flaws. Although more complex, using malloc to allocate a single memory block and initializing with placement new was straightforward, thanks to my experience with a memory pool library. This method reduces compile-time dependencies, prevents memory fragmentation, and improves cache efficiency, offering a balanced solution for performance and memory management.