从零开始手敲次世代引擎(十九)

上一篇我们实现了一个简单的基于块链的Allocator。

接下来我们来实现我们的内存管理模块:Memory Manager

根据之前我们的讨论,我们设计Memory Manager为这样一个角色,它总管着所有动态分配的内存。(但是严格来说,诸如堆栈,自动变量,Memory Manager创建之前创建的对象,以及一些全局对象,比如代表我们架构里的各种模块,也就是***Manager,并不由其管理)

这种管理,是通过管理一系列的Allocator来实现的。每种Allocator,代表了一种分配策略。Allocator以页(Page)为单位获取资源,再以块(Block)为单位分配资源。

采用这种结构的最大好处是:我们可以很方便地添加新类型的Allocator,并通过修改内存分配需求(Request)与分配器(Allocator)之间的映射关系(Allocator Lookup Policy)来快速地实现新的内存分配策略。

另外的好处是:我们可以通过一个线程与Allocator之间的绑定关系,迅速地实现线程的本地堆(Thread Local Storage)。这个堆由于为某个线程所独占,所以并不需要互锁机制,从而可以大大地加速线程的执行速度。

而且,这种结构还可以纵向拓展。如参考引用2那样,只要稍加改造,我们可以在Allocator之间形成层级关系以及兄弟(slibing)关系。

这种层级关系的意义在于,如果我们将一个很复杂的处理划分为一些单纯的短片段的话,那么每个片段的内存访问模式(access pattern)是有规律可循的。也就是说,有的片段总是倾向于频繁的小块内存使用;有的则是大块大块的使用;有的不怎么使用;有的则突发性大量使用,等等。这些不同的使用频率和使用强度,如果我们在同一个层级对其进行管理,那么状况就十分复杂,变得不确定性很强,很难预测;然而如果我们能归纳它们的特征,尽量将类似频率和强度的大量处理组织在同一个层级,那么同一个层级的互相随机叠加,此消彼长,从整体上就会呈现出一种相对的确定性。这种趋势随着并行运行的处理的数量和不确定性增加而增强。

我们的游戏引擎设计为多线程多模块异步平行执行模式。每个模块的任务类型很不一样,执行频率也不同。比如,渲染模块需要逐帧运行,涉及到大量的大块内存使用,但是这些buffer往往生命周期很短;场景加载模块则相对来说以很长的周期运行,其数据结构可能会在内存当中保持数分钟甚至数十分钟;而AI等逻辑模块则是典型的计算模块,会涉及到大量小buffer的高频分配与释放。

于此同时,游戏场景是由场景物体组成的,我们的很多模块都需要以场景物体为单位进行处理。同一个模块对于不同场景物体的处理是类似的,也就是说对于内存的访问模式是类似的。我们可以很自然地把他们组织成为一个内存管理上的兄弟关系。

好,接下来就让我们把这些想法落实到代码当中。因为我们目前还没有其它模块,我们还不需要完成上面所设计的全部内容。我们先将我们上一篇所写的Allocator组织到我们的Memory Manager当中,提供一个最基本的,单层的但是支持不同分配尺寸的,线程不安全的内存管理模块。

代码主要参考了参考引用1,结合我们的架构与命名规则进行了封装,并且进行了跨平台方面的一些改造。

#pragma once
#include "IRuntimeModule.hpp"
#include "Allocator.hpp"
#include <new>

namespace My {
    class MemoryManager : implements IRuntimeModule
    {
    public:
        template<typename T, typename... Arguments>
        T* New(Arguments... parameters)
        {
            return new (Allocate(sizeof(T))) T(parameters...);
        }

        template<typename T>
        void Delete(T *p)
        {
            reinterpret_cast<T*>(p)->~T();
            Free(p, sizeof(T));
        }

    public:
        virtual ~MemoryManager() {}

        virtual int Initialize();
        virtual void Finalize();
        virtual void Tick();

        void* Allocate(size_t size);
        void  Free(void* p, size_t size);
    private:
        static size_t*        m_pBlockSizeLookup;
        static Allocator*     m_pAllocators;
    private:
        static Allocator* LookUpAllocator(size_t size);
    };
}
#include "MemoryManager.hpp"
#include <malloc.h>

using namespace My;

namespace My {
    static const uint32_t kBlockSizes[] = {
        // 4-increments
        4,  8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48,
        52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, 

        // 32-increments
        128, 160, 192, 224, 256, 288, 320, 352, 384, 
        416, 448, 480, 512, 544, 576, 608, 640, 

        // 64-increments
        704, 768, 832, 896, 960, 1024
    };

    static const uint32_t kPageSize  = 8192;
    static const uint32_t kAlignment = 4;

    // number of elements in the block size array
    static const uint32_t kNumBlockSizes = 
        sizeof(kBlockSizes) / sizeof(kBlockSizes[0]);

    // largest valid block size
    static const uint32_t kMaxBlockSize = 
        kBlockSizes[kNumBlockSizes - 1];
}

int My::MemoryManager::Initialize()
{
    // one-time initialization
    static bool s_bInitialized = false;
    if (!s_bInitialized) {
        // initialize block size lookup table
        m_pBlockSizeLookup = new size_t[kMaxBlockSize + 1];
        size_t j = 0;
        for (size_t i = 0; i <= kMaxBlockSize; i++) {
            if (i > kBlockSizes[j]) ++j;
            m_pBlockSizeLookup[i] = j;
        }

        // initialize the allocators
        m_pAllocators = new Allocator[kNumBlockSizes];
        for (size_t i = 0; i < kNumBlockSizes; i++) {
            m_pAllocators[i].Reset(kBlockSizes[i], kPageSize, kAlignment);
        }

        s_bInitialized = true;
    }

    return 0;
}

void My::MemoryManager::Finalize()
{
    delete[] m_pAllocators;
    delete[] m_pBlockSizeLookup;
}

void My::MemoryManager::Tick()
{
}

Allocator* My::MemoryManager::LookUpAllocator(size_t size)
{

    // check eligibility for lookup
    if (size <= kMaxBlockSize)
        return m_pAllocators + m_pBlockSizeLookup[size];
    else
        return nullptr;
}

void* My::MemoryManager::Allocate(size_t size)
{
    Allocator* pAlloc = LookUpAllocator(size);
    if (pAlloc)
        return pAlloc->Allocate();
    else
        return malloc(size);
}

void My::MemoryManager::Free(void* p, size_t size)
{
    Allocator* pAlloc = LookUpAllocator(size);
    if (pAlloc)
        pAlloc->Free(p);
    else
        free(p);
}

参考引用

  1. Memory Management part 2 of 3: C-Style Interface | Ming-Lun “Allen” Chou
  2. How tcmalloc Works
  3. Memory management
  4. operator new, operator new[]

本作品采用知识共享署名 4.0 国际许可协议进行许可。

发表评论

Fill in your details below or click an icon to log in:

WordPress.com 徽标

您正在使用您的 WordPress.com 账号评论。 登出 /  更改 )

Google photo

您正在使用您的 Google 账号评论。 登出 /  更改 )

Twitter picture

您正在使用您的 Twitter 账号评论。 登出 /  更改 )

Facebook photo

您正在使用您的 Facebook 账号评论。 登出 /  更改 )

Connecting to %s

%d 博主赞过: