Привет!
Сегодня я хотел бы обсудить систему управления памятью в игре The Simpsons: Hit & Run 2003. Статья будет состоять из двух частей, в первой из которых будет обсуждаться само использование такой системы управления памятью, а во второй будет рассказано о внутреннем устройстве этой системы.
Управление памятью в данной игре представлено специальными аллокаторами. Аллокатор — класс, реализующий детали распределения и освобождения ресурсов компьютерной памяти. То есть какой-то аллокатор может выделять память через обычный malloc, с логгированием выделенной информации, а другой аллокатор может заранее выделять n-ое кол-во байт под свое хранилище, из которого в последствии будет запрашиваться память.
Часть 1
Виды аллокаторов
Полный список всех видов аллокаторов, представленных в игре, можно найти в файле srrmemory.h. Там он представлен в виде перечисления (enum):
enum GameMemoryAllocator { // GMA_DEFAULT = RADMEMORY_ALLOC_DEFAULT, // 0 GMA_TEMP = RADMEMORY_ALLOC_TEMP, // 1 #ifdef RAD_GAMECUBE GMA_GC_VMM = RADMEMORY_ALLOC_VMM, // 2 #endif GMA_PERSISTENT = 3, // 3 GMA_LEVEL, // 4 GMA_LEVEL_MOVIE, // 5 GMA_LEVEL_FE, // 6 GMA_LEVEL_ZONE, // 7 GMA_LEVEL_OTHER, // 8 GMA_LEVEL_HUD, // 9 GMA_LEVEL_MISSION, // 10 GMA_LEVEL_AUDIO, // 11 GMA_DEBUG, // 12 GMA_SPECIAL, // 13 GMA_MUSIC, // 14 GMA_AUDIO_PERSISTENT, // 15 GMA_SMALL_ALLOC, // 16 #ifdef RAD_XBOX GMA_XBOX_SOUND_MEMORY, // 17 #endif #ifdef USE_CHAR_GAG_HEAP GMA_CHARS_AND_GAGS, #else GMA_CHARS_AND_GAGS = GMA_LEVEL_OTHER, #endif GMA_ANYWHERE_IN_LEVEL = 25, // 25 GMA_ANYWHERE_IN_FE, // 26 GMA_EITHER_OTHER_OR_ZONE, // 27 GMA_ALLOCATOR_SEARCH = ALLOCATOR_SEARCH, // If you feel like using this one, see an example in FMVPlayer::LoadData NUM_GAME_MEMORY_ALLOCATORS };
Почти для каждого элемента этого enum представлен свой отдельный аллокатор, но для GMA_ALLOCATOR_SEARCH, GMA_ANYWHERE_IN_LEVEL , GMA_ANYWHERE_IN_FE и GMA_EITHER_OTHER_OR_ZONE отдельных хранилищ не представлено.
Практически каждый такой аллокатор представлен для отдельного типа информации, будь то музыка (GMA_MUSIC), информация связанная с уровнем (GMA_LEVEL) и так далее.
Использование
Рассмотрим использование такой системы управления памятью на примере из файла avatar.cpp:
HeapMgr()->PushHeap (GMA_LEVEL_OTHER); mpVehicleMappable = new VehicleMappable; mpVehicleMappable->AddRef(); #ifdef RAD_PS2 mpVehicleMappableUSB0 = new VehicleMappable; mpVehicleMappableUSB0->AddRef(); mpVehicleMappableUSB1 = new VehicleMappable; mpVehicleMappableUSB1->AddRef(); #endif mpHumanVehicleController = new HumanVehicleController; mpHumanVehicleController->AddRef(); mpInCarCharacterMappable = new InCarCharacterMappable; mpInCarCharacterMappable->AddRef(); mpBipedCharacterMappable = new BipedCharacterMappable; mpBipedCharacterMappable->AddRef(); mpCameraRelativeCharacterController = new CameraRelativeCharacterController; mpCameraRelativeCharacterController->AddRef(); HeapMgr()->PopHeap (GMA_LEVEL_OTHER);
В начале идет вызов метода PushHeap, в который мы передаем тип аллокатора, из которого будем запрашивать память. Затем идет само выделение памяти через вызов перегруженного оператора new. А в конце идет вызов метода PopHeap, в который мы должны передать тип аллокатора, из которого происходило выделение памяти.
Разберем этот код по порядку:
HeapManager
HeapManager является singleton классом посредником между классом HeapStack и пользователем (HeapManager хранит в своих полях экземпляр класса HeapStack). В свою очередь HeapStack является обычным стэком (структура данных), который хранит элементы типа GameMemoryAllocator.
Метод PushHeap представляет собой вызов метода Push у экземпляра HeapStack, который в свою очередь просто кладет переданный объект типа GameMemoryAllocator на вершину стэка.
Метод PopHeap представляет собой вызов метода Pop у экземпляра HeapStack, который удаляет объект, находящийся на вершине стэка.
Скрытый текст
(*) Инициализация аллокаторов происходит в методах PrepareHeapsStartup, PrepareHeapsFeSetup, PrepareHeapsInGame и PrepareHeapsSuperSprint.
Оператор new
Перегруженный оператор new имеет вид:
void* operator new( size_t size ) #ifdef RAD_PS2 #ifndef RAD_MW throw( std::bad_alloc ) // может бросить исключение std::bad_alloc #endif #endif { if( gMemorySystemInitialized == false ) { INIT_MEM(); } void* pMemory; if (g_NoHeapRoute) { pMemory = radMemoryAlloc( 0, size ); } else { GameMemoryAllocator curr = HeapMgr()->GetCurrentHeap(); pMemory = AllocateThis( curr, size ); #ifdef MEMORYTRACKER_ENABLED ::radMemoryMonitorIdentifyAllocation (pMemory, HeapMgr()->GetCurrentGroupID ()); #endif } //MEMTRACK_ALLOC( pMemory, size, 0 ); return( pMemory ); }
Разберем каждую строчку оператора new по порядку:
INIT_MEM
Инициализация макроса INIT_MEM может отличаться в зависимости от платформы на которой запущена игра. Реализации их схожи, но для простоты разберем реализацию этого макроса для PS2:
#define INIT_MEM() Memory::InitializeMemoryUtilities();PS2Platform::InitializeMemory();
Memory::InitializeMemoryUtilities() — функция, которая вызывается один раз за всю работу программы, и выполняет расчет максимального кол-ва свободной памяти на устройстве (а конкретно только под PS2):
void InitializeMemoryUtilities() { static bool alreadyCalled = false; if( alreadyCalled ) { return; } alreadyCalled = true; #ifdef RAD_PS2 //this is the largest amount of memory that is free g_MaxFreeMemory = GetFreeMemoryProfile(); #endif }
Скрытый текст
(*) Стоит учитывать, что память PS2 была представлена в виде карт памяти.
Функция GetFreeMemoryProfile проходится по всем картам памяти PS2 и расчитывает, сколько памяти свободно на момент запуска игры.
Разберем код функции GetFreeMemoryProfile по порядку:
Инициализация:
const int size = 256; void* pointers[ size ]; size_t sizes[ size ]; int index = 0; int i; for( i = 0; i < size; i++ ) { pointers[ i ] = NULL; sizes[ i ] = 0; }
Сначала мы создаем два массива на 256 элементов, где массив pointers будет хранить всю выделенную память за время работы функции, а массив sizes будет хранить кол-во памяти, которое свободно под каждую карту памяти.
Далее идет главная часть функции, а именно расчет максимального кол-ва свободной памяти:
do { int lo = 0; int hi = 1024*1024*256; int pivot; void* memory = NULL; do { pivot = ( hi + lo ) / 2; if( memory != NULL ) { free( memory ); memory = NULL; } memory = malloc( pivot ); if( memory != NULL ) { lo = pivot; } else { memory = malloc( lo ); hi = pivot; } } while( ( hi - lo ) > 1 ); if( ( memory == NULL ) && ( retrys < 2 ) ) { ++retrys; } else { sizes[ index ] = lo; pointers[ index ] = memory; memory = NULL; ++index; } } while( ( pointers[ index - 1 ] != NULL ) && ( index < size ) );
Расчет кол-ва свободной памяти ведется для каждого блока в 256 МБ по отдельности (так как за один вызов функции malloc система может выделять память только из одной карты памяти) методом бинарного поиска. Изначально мы задаем переменные lo = 0, hi = 1024*1024*256 (256 МБ) и pivot (середина отрезка [lo; hi]). Далее мы пробуем выделить pivot байт функцией malloc и проверяем, не вернула ли она значение NULL (не получилось выделить pivot байт). Если malloc вернул NULL, то hi смещается к середине. Иначе смещение к середине происходит у lo. Так происходит до тех пор, пока lo и hi не сблизятся. В итоге, значение lo есть максимальное кол-во свободной памяти у текущей проверяемой карты памяти.
Затем все полученные значения lo записываются в массив sizes, а указатель memory на выделенную память размером lo записывается в массив pointers для дальнейней ее очистки.
Скрытый текст
(*) Хотя настоящий максимальный размер одной карты памяти составлял 8 МБ, разработчики The Simpsons Hit & Run задали максимальный размер одной карты памяти в 256 МБ. Это связано с тем, что в то время на рынке существовали китайские карты памяти для PS2 размерами намного большими чем 8 МБ. Китайская карта памяти наибольшего размера могла вмещать в себя 256 МБ.
Суммирование всей свободной памяти:
size_t total = 0; for( i = 0; i < size; i++ ) { total += sizes[ i ]; }
Очистка выделенной памяти:
for( i = 0; i < size; i++ ) { void* pointer = pointers[ i ]; if( pointer != NULL ) { free( pointer ); pointers[ i ] = NULL; } else { break; } }
В итоге функция GetFreeMemoryProfile возвщарает переменную total, которая и хранит кол-во байт свободной памяти.
Скрытый текст
(*) Функция GetFreeMemoryProfile вызывается только для PS2 потому, что для других платформ существовали специальные функции на проверку того, сколько памяти свободно в данный момент. Например для WIN32 существует функция GlobalMemoryStatus, через которую можно узнать информацию о текущем использовании виртуальной и физической памяти. А для PS2 такой функции небыло.
PS2Platform::InitializeMemory() — функция, которая вызывает radMemoryInitialize, тем самым инициализируя систему памяти (принцип работы функции radMemoryInitialize будет рассмотрен позже).
g_NoHeapRoute
g_NoHeapRoute — булевая переменная, которая говорит о том, включена ли система аллокаторов (false) или нет (true). Если эта переменная равна true, то вызывается функция radMemoryAlloc, куда передается параметр GMA_DEFAULT и размер запроса на выделение памяти в байтах.
То есть, если система аллокаторов отключена, то все запросы на выделение памяти будут обращены к аллокатору GMA_DEFAULT.
Если же переменная g_NoHeapRoute равна false, то происходит вызов функции AllocateThis, в которую передается тип аллокатора, являющийся текущей вершиной стэка (HeapStack) , а также кол-во запрошенных на выделение байт.
Скрытый текст
(*) Функция radMemoryAlloc как раз таки и отвечает за само выделение памяти в зависимости от аллокатора. Она запрашивает n-ое кол-во байт у некоторого аллокатора и возвращает указатель на выделенную память.
AllocateThis
AllocateThis — функция, которая запрашивает n-ое кол-во байт у некоторого аллокатора и возвращает указатель на выделенную память.
inline void* AllocateThis( GameMemoryAllocator allocator, size_t size ) { void* pMemory = NULL; #ifdef CORRAL_SMALL_ALLOCS if( size < 201 ) { if( allocator != GMA_AUDIO_PERSISTENT && allocator != GMA_PERSISTENT && allocator != GMA_TEMP) { pMemory = radMemoryAlloc( GMA_SMALL_ALLOC, size ); } else { pMemory = radMemoryAlloc( allocator, size ); } } else #endif { if ( allocator >= GMA_ANYWHERE_IN_LEVEL && allocator != ALLOCATOR_SEARCH ) { pMemory = FindFreeMemory( allocator, size ); } else { pMemory = radMemoryAlloc( allocator, size ); } } return pMemory; }
Если под переданный тип аллокатора не существует собственного аллокатора (allocator >= GMA_ANYWHERE_IN_LEVEL), то вызывается функция FindFreeMemory. Иначе вызывается функция radMemoryAlloc.
FindFreeMemory
FindFreeMemory — функция, которая делает запрос на выделение памяти среди некоторого списка аллокаторов.
void* FindFreeMemory( GameMemoryAllocator allocator, size_t size ) { GameMemoryAllocator* list = NULL; unsigned int numAvailable = 0; if ( allocator == GMA_ANYWHERE_IN_LEVEL ) { list = AVAILABLE_FOR_RENT_IN_LEVEL; numAvailable = sizeof ( AVAILABLE_FOR_RENT_IN_LEVEL ) / sizeof( GameMemoryAllocator ); } else if ( allocator == GMA_ANYWHERE_IN_FE ) { list = AVAILABLE_FOR_RENT_IN_FE; numAvailable = sizeof ( AVAILABLE_FOR_RENT_IN_FE ) / sizeof( GameMemoryAllocator ); } else if ( allocator == GMA_EITHER_OTHER_OR_ZONE ) { list = AVAILABLE_IN_OTHER_OR_ZONE; numAvailable = sizeof ( AVAILABLE_IN_OTHER_OR_ZONE ) / sizeof( GameMemoryAllocator ); } else { rAssert( false ); } if ( list != NULL ) { ::radMemorySetUsableAllocators( (radMemoryAllocator*)list, numAvailable ); void* memory = radMemoryAlloc( ALLOCATOR_SEARCH, size ); return memory; } return NULL; }
Сначала мы создаем указатель на некоторый список аллокаторов и переменную, которая будет хранить кол-во элементов этого списка. Далее, в зависимости от переданного типа аллокатора в функцию, указатель list начинает указывать на начало одного из трех массивов, содержащих типы аллокаторов так или иначе связанных между собой:
static GameMemoryAllocator AVAILABLE_FOR_RENT_IN_LEVEL[] = { GMA_TEMP, GMA_LEVEL_ZONE, // 7 6 6 GMA_LEVEL_OTHER, // 8 7 7 GMA_LEVEL_MISSION, // 10 9 9 GMA_LEVEL_HUD // 9 8 8 }; static GameMemoryAllocator AVAILABLE_FOR_RENT_IN_FE[] = { GMA_LEVEL_MOVIE, // 5 4 4 GMA_LEVEL_FE, // 6 5 5 GMA_LEVEL_AUDIO // 11 10 10 }; static GameMemoryAllocator AVAILABLE_IN_OTHER_OR_ZONE[] = { GMA_LEVEL_OTHER, // 8 7 7 GMA_LEVEL_ZONE // 7 6 6 };
Затем, вызывается функция radMemorySetUsableAllocators, загружающая данные о типах аллокаторах, которые будут участвовать в запросах на выделение памяти, и, наконец вызывается функция radMemoryAlloc, куда передается ALLOCATOR_SEARCH в качестве типа аллокатора.
(1*) При передаче ALLOCATOR_SEARCH в функцию radMemoryAlloc в качестве типа аллокатора, radMemoryAlloc ведет себя по другому: он создает запросы на выделение памяти не у одного аллокатора, а у целого списка, загруженного ранее через функцию radMemorySetUsableAllocators. Соответственно он берет память у одного (или нескольких если память предыдущих уже закончилась) из этих аллокаторов.
Скрытый текст
(*) Обычно в функцию radMemoryAlloc тип аллокатора больший или равный GMA_ANYWHERE_IN_LEVEL передают тогда, когда вам все равно, какой именно из аллокаторов общего типа (AVAILABLE_FOR_RENT_IN_LEVEL, AVAILABLE_FOR_RENT_IN_FE или AVAILABLE_IN_OTHER_OR_ZONE) должен выделять память.
Заключение 1 части
Скрытый текст
(*) Вообще существует еще одна перегрузка оператора new, которая никак не взаимодействует с HeapManager. Использование: new(GMA_PERSISTENT) HeapManager Эта перегрузка полезна, когда вам нужно выделить память только под один объект. Реализация этого оператора new почти никак не отличается от реализации разобранного оператора new.
В заключение первой части я хотел бы представить итоговую блок-схему системы выделения памяти:

Часть 2
IRadMemoryAllocator
IRadMemoryAllocator — базовый интерфейс для всех аллокаторов в игре.
struct IRadMemoryAllocator : public IRefCount { virtual void* GetMemory( unsigned int size ) = 0; virtual void FreeMemory( void* pMemory ) = 0; virtual bool CanFreeMemory( void * pMemory ) = 0; virtual void* GetMemoryAligned( unsigned int size, unsigned int alignment ) = 0; virtual void FreeMemoryAligned( void * pMemory ) = 0; virtual bool CanFreeMemoryAligned( void * pMemory ) = 0; // // Memory statistics // virtual void GetStatus( unsigned int * totalFreeMemory, unsigned int * largestBlock, unsigned int * numberOfObjects, unsigned int * highWaterMark ); virtual unsigned int GetSize( void ); };
Кратко пробежимся по всем обязательным методам, которые должны содержать аллокаторы:
GetMemory — выделение памяти из аллокатора и возвращение указателя на выделенную память.
FreeMemory — очищает память по переданному указателю.
CanFreeMemory — проверка на принадлежность переданного указателя к данному аллокатору (при очистке памяти).
GetMemoryAligned — то же самое что и GetMemory, только при выделении памяти также учитывается выравнивание.
FreeMemoryAligned — очищает выравненную память по переданному указателю.
CanFreeMemoryAligned — то же самое что и CanFreeMemory (с учетом выравнивания).
radMemoryInitialize
radMemoryInitialize — Функция, которая инициализирует систему аллокаторов (задает стандартные аллокаторы).
Тело функции radMemoryInitialize:
if( g_Initialized ) { return; } #ifdef RAD_GAMECUBE ::radMemoryPlatInitialize( sizeVMMainMemory, sizeVMARAM ); #else ::radMemoryPlatInitialize( ); #endif //This is memory reserved to really bad situations where we need to printf. #ifndef RAD_GAMECUBE gEmergencyMemory = radMemoryPlatAlloc( 1024 * 32 ); #endif rAssert( g_Initialized == false ); g_Initialized = true; g_pRadMemoryAllocator_Malloc = new ( g_MemoryForMalloc ) radMemoryAllocatorMalloc( ); g_AllocatorTreeNode_Root.m_pIRadMemoryAllocator = g_pRadMemoryAllocator_Malloc; g_AllocatorTreeNode_Root.m_pChildren_Head = NULL; g_AllocatorTreeNode_Root.m_pSibling_Next = NULL; g_AllocatorTreeNode_Root.m_pParent = NULL; for( unsigned int i = 0; i < ALLOCATOR_TABLE_SIZE; i ++ ) { g_AllocatorTreeNodes[ i ].m_pChildren_Head = NULL; g_AllocatorTreeNodes[ i ].m_pParent = NULL; g_AllocatorTreeNodes[ i ].m_pSibling_Next = NULL; g_AllocatorTreeNodes[ i ].m_pIRadMemoryAllocator = g_pRadMemoryAllocator_Malloc; } #ifdef RAD_GAMECUBE unsigned aramSize = (1024 * 1024 * 16) - sizeVMARAM; radMemorySpaceInitialize( aramSize ); sVMMDLHeapInitialized = false; if ((sizeVMMainMemory != 0) && (sizeVMARAM != 0)) { bool ok = VMAlloc( 0x7E000000, sizeVMARAM ); rAssert( ok ); vmmHeap = radMemoryCreateDougLeaHeap( (void *)0x7E000000, sizeVMARAM, RADMEMORY_ALLOC_DEFAULT, "GameCube_VMM" ); rAssert(vmmHeap != NULL); radMemoryRegisterAllocator( RADMEMORY_ALLOC_VMM, RADMEMORY_ALLOC_DEFAULT, vmmHeap ); sVMMDLHeapInitialized = true; #ifndef RAD_RELEASE VMSetLogStatsCallback(&gcnVMMLogStats); #endif } #else radMemorySpaceInitialize( ); #endif // // Initialize static heap // //g_StaticHeap.CreateHeap( STATIC_HEAP_SIZE );
g_AllocatorTreeNodes — массив типа radMemoryAllocatorTreeNode (кол-во элементов g_AllocatorTreeNodes совпадает с кол-вом элементов перечисления GameMemoryAllocators)
struct radMemoryAllocatorTreeNode { IRadMemoryAllocator * m_pIRadMemoryAllocator; // the allocator pointer radMemoryAllocatorTreeNode * m_pChildren_Head; // list of sub allocators radMemoryAllocatorTreeNode * m_pSibling_Next; // list pointer used by parent radMemoryAllocatorTreeNode * m_pParent; // optomization };
В функции radMemoryInitialize массив g_AllocatorTreeNodes заполняется стандартными значениями, а именно:
for( unsigned int i = 0; i < ALLOCATOR_TABLE_SIZE; i ++ ) // ALLOCATOR_TABLE_SIZE = 30 { g_AllocatorTreeNodes[ i ].m_pChildren_Head = NULL; g_AllocatorTreeNodes[ i ].m_pParent = NULL; g_AllocatorTreeNodes[ i ].m_pSibling_Next = NULL; g_AllocatorTreeNodes[ i ].m_pIRadMemoryAllocator = g_pRadMemoryAllocator_Malloc; }
Где g_pRadMemoryAllocator_Malloc является экземпляром класса radMemoryAllocatorMalloc (radMemoryAllocatorMalloc также наследуется от IRadMemoryAllocator).
radMemoryAllocatorMalloc — это аллокатор, который не имеет своего собственного хранилища. То есть он в конечном итоге выделяет память через malloc.
radMemoryAlloc
radMemoryAlloc — функция, которая отвечает за само выделение памяти в зависимости от аллокатора. Она запрашивает n-ое кол-во байт у переданного аллокатора и возвращает указатель на выделенную память.
Тело функции radMemoryAlloc:
#ifdef RAD_PS2 if( (numberOfBytes<201) && gbSmallAllocCreated && (allocator != HACK_AUDIO_PERSISTENT) && (allocator != HACK_PERSISTENT) && (allocator != RADMEMORY_ALLOC_TEMP) ) { allocator = HACK_SMALL_ALLOC; } #endif #if ( defined RAD_XBOX ) || ( defined RAD_GAMECUBE ) || ( defined RAD_MW ) if ( !g_Initialized ) { MemoryHackCallback(); } #endif if ( numberOfBytes == 0 ) { return NULL; } rAssert( g_Initialized == true ); rAssert( allocator < ALLOCATOR_TABLE_SIZE || allocator == ALLOCATOR_SEARCH ); #ifdef RAD_XBOX //rAssert( allocator != 2 ); #endif void * pMem; if ( allocator == ALLOCATOR_SEARCH ) { pMem = radMemoryAllocSearch( numberOfBytes, 0 ); } else { IRadMemoryAllocator * pIRadMemoryAllocator = g_AllocatorTreeNodes[ allocator ].m_pIRadMemoryAllocator; pMem = pIRadMemoryAllocator->GetMemory( numberOfBytes ); ::radMemoryMonitorIdentifyAllocation ( pMem, g_CurrentMemoryIdentification ); } if (g_MemoryActivityCallback) { g_MemoryActivityCallback->MemoryAllocated( allocator, pMem, numberOfBytes ); } CheckForOutOfMemory( pMem, numberOfBytes, allocator ); // логи LEAK_DETECTION_ADD_ALLOCATION( pMem, numberOfBytes, allocator ); // логи return pMem;
Как можно увидеть, если переданный тип аллокатора не является ALLOCATOR_SEARCH, то из массива g_AllocatorTreeNodes берется аллокатор под индексом allocator (pIRadMemoryAllocator), и у взятого pIRadMemoryAllocator вызывается метод GetMemory, который и выделяет нужное количество памяти. В случае же, если переданный тип аллокатора является ALLOCATOR_SEARCH, то вызывается функция radMemoryAllocSearch, работа которой не отличается от описанной в (1*).
Создание аллокаторов
Создание аллокаторов (как и было отмечено ранее) происходит в методах PrepareHeapsStartup, PrepareHeapsFeSetup, PrepareHeapsInGame и PrepareHeapsSuperSprint класса HeapManager. Для создания каждого отдельного аллокатора вызывается функция CreateHeap (из файла createheap.cpp), которая и занимается созданием аллокатора.
IRadMemoryHeap
Все GameMemoryAllocators (у которых имеется аллокатор) являются аллокаторами, производными от класса IRadMemoryHeap (несмотря на название Heap, никакой кучи как структуры данных там не реализовано).
IRadMemoryHeap — аллокатор, который поддерживает выделение памяти для объектов разного размера. При этом сам механизм выделения памяти у аллокаторов, производных от IRadMemoryHeap, может отличаться.
Heap аллокаторов существует три разновидности: Static Heap, Tracking Heap и Doug Lea Heap:
-
Static Heap — обычный Pool аллокатор, в котором заранее выделяется память под хранилище, и потом из этого хранилища берется память. Освобождение памяти для данного аллокатора не предусмотрено (то есть метод FreeMemory в StaticHeap пустой).
-
Tracking Heap — аллокатор, который выделяет память через malloc, но информацию о выделенной памяти он сохраняет у себя в хранилище для дальнейшей очистки. В отличие от StaticHeap может очищать выделенную память.
-
Doug Lea Heap — аллокатор, который выделяет память через dlmalloc, но информацию о выделенной памяти он сохраняет у себя в хранилище для дальнейшей очистки. В отличие от StaticHeap может очищать выделенную память.
Скрытый текст
(*) dlmalloc имеет свои минусы и плюсы по сравнению со стандартным malloc, но главный его плюс заключается в том, что реализация dlmalloc не зависит от платформы (в отличие от malloc). Также считается, что dlmalloc выгоднее при выделении малого кол-ва памяти (<= 201 байт) чем malloc.
CreateHeap
Тело функции CreateHeap:
void CreateHeap ( GameMemoryAllocator allocator, const unsigned int size ) { unsigned int index = static_cast< unsigned int >( allocator ); rAssert( g_HeapArray[ index ] == NULL ); HeapType type = g_HeapCreationData[ index ].type; const char* name = g_HeapCreationData[ index ].name; GameMemoryAllocator parent = g_HeapCreationData[ index ].parent; rReleasePrintf ("Creating Heap: %s (%d)\n", name, size ); #ifdef RAD_RELEASE if( type == HEAP_TYPE_TRACKING ) { type = HEAP_TYPE_NONE; } #endif switch( type ) { case HEAP_TYPE_STATIC : { HeapMgr()->PushHeap( GMA_DEBUG ); g_HeapArray[ index ] = radMemoryCreateStaticHeap( size, RADMEMORY_ALLOC_DEFAULT, name ); g_HeapArray[ index ]->AddRef(); HeapMgr()->PopHeap( GMA_DEBUG ); break; } case HEAP_TYPE_TRACKING : { HeapMgr()->PushHeap( GMA_DEBUG ); g_HeapArray[ index ] = radMemoryCreateTrackingHeap( size, RADMEMORY_ALLOC_DEFAULT, name ); HeapMgr()->PopHeap( GMA_DEBUG ); break; } case HEAP_TYPE_DOUG_LEA : { HeapMgr()->PushHeap( GMA_DEBUG ); g_HeapArray[ index ] = radMemoryCreateDougLeaHeap( size, RADMEMORY_ALLOC_DEFAULT, name ); g_HeapArray[ index ]->AddRef(); HeapMgr()->PopHeap( GMA_DEBUG ); break; } case HEAP_TYPE_NONE : { //rAssert( false ); return; } default: { rAssert( false ); return; } } radMemoryRegisterAllocator( allocator, parent,g_HeapArray[ index ] ); }
g_HeapCreationData — массив типа HeapCreationData, который содержит информацию о создаваемых аллокаторах, а именно: тип, родитель, название:
struct HeapCreationData { HeapType type; GameMemoryAllocator parent; char name[ 256 ]; }; HeapCreationData g_HeapCreationData[] = { { HEAP_TYPE_DOUG_LEA, GMA_DEFAULT, "Default" }, { HEAP_TYPE_DOUG_LEA, GMA_DEFAULT, "Temp" }, { HEAP_TYPE_NONE, GMA_DEFAULT, "Gamecube VMM" }, #ifdef RAD_WIN32 { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Persistent" }, // no static heap for pc #else { HEAP_TYPE_STATIC, GMA_DEFAULT, "Persistent" }, #endif // RAD_WIN32 { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Level" }, { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Level Movie" }, { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Level FE" }, { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Level Zone" }, { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Level Other" }, { HEAP_TYPE_DOUG_LEA, GMA_DEFAULT, "Level Hud" }, { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Level Mission" }, { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Level Audio" }, { HEAP_TYPE_NONE, GMA_DEFAULT, "Debug" }, { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Special" }, { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Music" }, { HEAP_TYPE_DOUG_LEA, GMA_DEFAULT, "Audio Persistent" }, { HEAP_TYPE_DOUG_LEA, GMA_DEFAULT, "Small Alloc" }, #ifdef RAD_XBOX { HEAP_TYPE_TRACKING, GMA_DEFAULT, "XBOX Sound" }, #endif #ifdef USE_CHAR_GAG_HEAP { HEAP_TYPE_DOUG_LEA, GMA_DEFAULT, "Characters and Gags" }, #endif { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Anywhere in Level" }, { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Anywhere in FE" }, { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Either Other or Zone" }, { HEAP_TYPE_TRACKING, GMA_DEFAULT, "Search" }, { HEAP_TYPE_NONE, GMA_DEFAULT, "???" }, { HEAP_TYPE_NONE, GMA_DEFAULT, "???" }, };
В конце, после самого создания аллокатора в функциях radMemoryCreateStaticHeap, radMemoryCreateTrackingHeap или radMemoryCreateDougLeaHeap вызывается функция radMemoryRegisterAllocator, которая заменяет аллокатор, лежащий по индексу allocator в массиве g_AllocatorTreeNodes, на созданный нами аллокатор.
radMemoryCreateStaticHeap
Для примера рассмотрим создание Static Heap.
radMemoryCreateStaticHeap — функция, которая занимается созданием Pool аллокатора.
Тело функции radMemoryCreateStaticHeap:
rReleasePrintf("%s ", pName ); StaticHeap* pHeap = new ( allocator ) StaticHeap; // выделение памяти через radMemoryAllocatorMalloc pHeap->CreateHeap( size ); return pHeap;
Тело функции StaticHeap::CreateHeap:
m_FreeingAllowed = false; m_TotalAllocations = 0; m_TotalSize = size; m_BasePointer = reinterpret_cast< char* >( radMemoryPlatAlloc( size ) ); rAssert( m_BasePointer != NULL ); m_CurrentPointer = m_BasePointer; m_End = reinterpret_cast< char* >( reinterpret_cast< size_t >( m_BasePointer ) + size ); m_Overflow = 0; rReleasePrintf("StaticHeap Start: 0x%x, End:0x%x\n", m_BasePointer, m_End ); #ifdef RADMEMORYMONITOR { radMemoryMonitorIdentifyAllocation( (void*)m_BasePointer, g_nameFTech, "StaticAllocator::m_StartOfMemory" ); radMemoryMonitorDeclareSection( (void*)m_BasePointer, m_TotalSize, IRadMemoryMonitor::MemorySectionType_DynamicData ); char szName[ 128 ]; sprintf( szName, "[StaticAllocator]" ); radMemoryMonitorIdentifySection( (void*)m_BasePointer, szName ); } #endif // RADMEMORYMONITOR
Как можно видеть, в функции StaticHeap::CreateHeap память под хранилище выделяется функцией radMemoryPlatAlloc, которая представляет из себя обычный вызов функции malloc с дополнительным логгированием.
Скрытый текст
Как можно заметить по массиву g_HeapCreationData, только GMA_PERSISTENT соответствует StaticHeap. Также стоит отметить, что все самые главные классы в игре выделяются именно через аллокатор GMA_PERSISTENT.
Код из файла (ps2main.cpp):
HeapMgr()->PushHeap (GMA_PERSISTENT); //Process any commandline options from the command.txt file ProcessCommandLineArgumentsFromFile(); // // Instantiate all the singletons before doing anything else. // CreateSingletons(); // // Construct the platform object. // PS2Platform* pPlatform = PS2Platform::CreateInstance(); rAssert( pPlatform != NULL ); // // Create the game object. // Game* pGame = Game::CreateInstance( pPlatform ); rAssert( pGame != NULL ); // // Initialize the game. // pGame->Initialize(); HeapMgr()->PopHeap (GMA_PERSISTENT);
Поэтому можно сказать, что выделять память через GMA_PERSISTENT стоит приемущественно для тяжелых типов данных, потому что выделения памяти через GMA_PERSISTENT происходят намного быстрее, чем те же выделения через другие аллокаторы.
Вывод
Для выделения памяти под тяжелые типы данных лучше использовать Static Heap (GMA_PERSISTENT), под легкие Doug Lea Heap, а под средние Tracking Heap.
Надеюсь, эта статья была полезна и интересна мододелам и просто прохожим.
Полный исходный код игры The Simpsons: Hit & Run 2003 вы можете посмотреть здесь:
ссылка на оригинал статьи https://habr.com/ru/articles/839380/
Добавить комментарий