DonaldW's github pages
Unreal Source Explained (USE) is an Unreal source code analysis, based on profilers.
For more infomation, see the repo in github.
See Table of Contents for the complete content list. Some important contents are listed below,
As the above overview shows, a process has several important memory segments, from higher virtual address to lower:
brk()
manipulates this to increase or decrease the Heap;static char * NotInitChar;
static char * InitedChar = "Hello";
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,
SmallOSAlloc()
(link), which eventually calls malloc()
;AllocatePoolMemory()
(link), which calls OSAlloc()
and eventually calls mmap()
;OSAlloc()
, which is also eventually allocated by mmap()
;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;
}
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 ); } \
...\