[WIP] Unreal Source Explained

DonaldW's github pages

[WIP] Unreal Source Explained

Unreal Source Explained (USE) is an Unreal source code analysis, based on profilers.
For more infomation, see the repo in github.

Contents

See Table of Contents for the complete content list. Some important contents are listed below,

Memory Management

Some Basics

Process Virtual Memory Segments

As the above overview shows, a process has several important memory segments, from higher virtual address to lower:

Native Memory APIs

C++ operator new and delete use C malloc() and free() to allocate or release memory.

malloc() uses brk() for small size allocations and mmap() for larger size allocations.

brk() was POSIX API but now it’s not. mmap() is POSIX API.
brk() moves the program break (see the picture above), hence increase or decrease the heap size.
mmap() maps a file for access and lazy load the actual content into the virtual memory. When an anonymous file is mapped (by MAP_ANONYMOUS flag or "/dev/zero" file) , it’s similar to memory allocation.

FMemory and FMallocBinned

Most heap memory is allocated via FMallocBinned::Malloc()(link), which is called by FMemory::Malloc()(link) and the like.

FMallocBinned is commentted as “Optimized virtual memory allocator”, it’s actually implemented as Memory Pool, where objects with specific size (8B, 16B, …, 32KB)(link) is allocated from corresponding pool(link). This can help to reduce memory fragmentation to some degree.
Allocation is thread-safe and locked for the specific pool.(link)

Actually, most of the memory is allocated via mmap(), rather than malloc().

Allocating via mmap()/munmap() gives the engine developer more freedom to customize memory management, because they are lower and simpler system calls than malloc()/free(). Another reason is, malloc() and free() in some platform, may only reduce the Resident set size (RSS), but the Virtual set size (VSS) may not decrease even if free() is correctly called.

As the following code snippet shows,

void* FMallocBinned::Malloc(SIZE_T Size, uint32 Alignment)
{
	...
	bool bUsePools = true;
	if (Size <= Private::SMALL_BLOCK_POOL_SIZE) // 224B in iOS
	{
		...
		// SmallOSAlloc() calls malloc()
		Free = (FFreeMem*)Private::SmallOSAlloc(*this, AlignedSize, ActualPoolSize);
		...
	}
	if (bUsePools)
	{
	if( Size < BinnedSizeLimit)
	{
		// Allocate from pool.
		...
		if( !Pool )
		{
			// AllocatePoolMemory() calls mmap() eventually
			Pool = Private::AllocatePoolMemory(*this, Table, Private::BINNED_ALLOC_POOL_SIZE/*PageSize*/, Size);
		}
		Free = Private::AllocateBlockFromPool(*this, Table, Pool, Alignment);
	}
	else if ( ((Size >= BinnedSizeLimit && Size <= PagePoolTable[0].BlockSize) ||
		(Size > PageSize && Size <= PagePoolTable[1].BlockSize)))
	{
		// Bucket in a pool of 3*PageSize or 6*PageSize
		...
		if( !Pool )
		{
			// AllocatePoolMemory() calls mmap() eventually
			Pool = Private::AllocatePoolMemory(*this, Table, PageCount*PageSize, BinnedSizeLimit+BinType);
		}

		Free = Private::AllocateBlockFromPool(*this, Table, Pool, Alignment);
	}
	else
	{
		// Use OS for large allocations.
		...
		// OSAlloc() calls mmap()
		Free = (FFreeMem*)Private::OSAlloc(*this, AlignedSize, ActualPoolSize);
		...
	}
	}

	MEM_TIME(MemTime += FPlatformTime::Seconds());
	return Free;
}

Global override new operator

Engines (e.g. Unity) usually use Global overloaded new operator to hook the new opeartor and make its own custom memory management.
Unreal also overloads the global operator new()(link), which uses FMemory::Malloc() to allocate and manage memory.

#define REPLACEMENT_OPERATOR_NEW_AND_DELETE \
	void* operator new  ( size_t Size ) { return FMemory::Malloc( Size ); } \
	void* operator new[]( size_t Size ) { return FMemory::Malloc( Size ); } \
	...\
	void operator delete  ( void* Ptr ) { FMemory::Free( Ptr ); } \
	void operator delete[]( void* Ptr ) { FMemory::Free( Ptr ); } \
	...\