標籤:

從零開始手敲次世代引擎(十九)

上一篇我們實現了一個簡單的基於塊鏈的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 oncen#include "IRuntimeModule.hpp"n#include "Allocator.hpp"n#include <new>nnnamespace My {n class MemoryManager : implements IRuntimeModulen {n public:n template<typename T, typename... Arguments>n T* New(Arguments... parameters)n {n return new (Allocate(sizeof(T))) T(parameters...);n }nn template<typename T>n void Delete(T *p)n {n reinterpret_cast<T*>(p)->~T();n Free(p, sizeof(T));n }nn public:n virtual ~MemoryManager() {}nn virtual int Initialize();n virtual void Finalize();n virtual void Tick();nn void* Allocate(size_t size);n void Free(void* p, size_t size);n private:n static size_t* m_pBlockSizeLookup;n static Allocator* m_pAllocators;n private:n static Allocator* LookUpAllocator(size_t size);n };n}n

#include "MemoryManager.hpp"n#include <malloc.h>nnusing namespace My;nnnamespace My {n static const uint32_t kBlockSizes[] = {n // 4-incrementsn 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48,n 52, 56, 60, 64, 68, 72, 76, 80, 84, 88, 92, 96, nn // 32-incrementsn 128, 160, 192, 224, 256, 288, 320, 352, 384, n 416, 448, 480, 512, 544, 576, 608, 640, nn // 64-incrementsn 704, 768, 832, 896, 960, 1024n };nn static const uint32_t kPageSize = 8192;n static const uint32_t kAlignment = 4;nn // number of elements in the block size arrayn static const uint32_t kNumBlockSizes = n sizeof(kBlockSizes) / sizeof(kBlockSizes[0]);nn // largest valid block sizen static const uint32_t kMaxBlockSize = n kBlockSizes[kNumBlockSizes - 1];n}nnint My::MemoryManager::Initialize()n{n // one-time initializationn static bool s_bInitialized = false;n if (!s_bInitialized) {n // initialize block size lookup tablen m_pBlockSizeLookup = new size_t[kMaxBlockSize + 1];n size_t j = 0;n for (size_t i = 0; i <= kMaxBlockSize; i++) {n if (i > kBlockSizes[j]) ++j;n m_pBlockSizeLookup[i] = j;n }nn // initialize the allocatorsn m_pAllocators = new Allocator[kNumBlockSizes];n for (size_t i = 0; i < kNumBlockSizes; i++) {n m_pAllocators[i].Reset(kBlockSizes[i], kPageSize, kAlignment);n }nn s_bInitialized = true;n }nn return 0;n}nnvoid My::MemoryManager::Finalize()n{n delete[] m_pAllocators;n delete[] m_pBlockSizeLookup;n}nnvoid My::MemoryManager::Tick()n{n}nnAllocator* My::MemoryManager::LookUpAllocator(size_t size)n{nn // check eligibility for lookupn if (size <= kMaxBlockSize)n return m_pAllocators + m_pBlockSizeLookup[size];n elsen return nullptr;n}nnvoid* My::MemoryManager::Allocate(size_t size)n{n Allocator* pAlloc = LookUpAllocator(size);n if (pAlloc)n return pAlloc->Allocate();n elsen return malloc(size);n}nnvoid My::MemoryManager::Free(void* p, size_t size)n{n Allocator* pAlloc = LookUpAllocator(size);n if (pAlloc)n pAlloc->Free(p);n elsen free(p);n}n

參考引用

  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 國際許可協議進行許可。

推薦閱讀:

《Inside》開發秘辛:沒有腳本、沒有設計文檔,同一個場景做了5年迭代
如何成為一名技術美術
從零開始手敲次世代遊戲引擎(三十四)

TAG:游戏开发 |