UE4内存分配器概述
UE4支持多种内存分配器:
/** Which allocator is being used */ enum EMemoryAllocatorToUse { Ansi, // Default C allocator Stomp, // Allocator to check for memory stomping TBB, // Thread Building Blocks malloc Jemalloc, // Linux/FreeBSD malloc Binned, // Older binned malloc Binned2, // Newer binned malloc Binned3, // Newer VM-based binned malloc, 64 bit only Platform, // Custom platform specific allocator Mimalloc, // mimalloc };
具体包括如下几类:
Ansi内存分配器(标准C):直接调用malloc、free、realloc函数 Android下Ansi分配器为Jemalloc
TBB(Thread Building Blocks)内存分配器:Intel 提供的第三方库的一个可伸缩内存分配器(Scalable Memory Allocator)
Jemalloc内存分配器(Linux / FreeBSD):适合多线程下的内存分配管理 http://www.canonware.com/jemalloc/ How to know if a device uses jemalloc 'new' ? Jemalloc (UncP's Blog)
Stomp:用于查非法内存操作(如:内存越界,野指针)的管理方式,目前只支持windows、mac、unix等pc平台。带命令行参数-stompmalloc来启用该分配器
Mimalloc:https://github.com/microsoft/mimalloc
UE4内置的内存分配器
① Binned(第一代箱式内存分配器):用于32位地址空间 例如:iOS(虽然iOS系统是64位地址空间,但App的虚拟内存子系统被OS限制在32位范围)
② Binned2(第二代箱式内存分配器):可用于Linux、Android
③ Binned3(第三代箱式内存分配器,仅支持64bits):比Binned2更快一些,内存也更省一些
对于不同平台而言,都有自己对应的平台内存管理类,它们继承自FGenericPlatformMemory,封装了平台相关的内存操作。
具体而言,包含FAndroidPlatformMemory,FApplePlatformMemory,FIOSPlatformMemory,FWindowsPlatformMemory、FLinuxPlatformMemory、FUnixPlatformMemory、FMacPlatformMemory等。
通过调用FPlatformMemory的BaseAllocator函数,我们取得平台对应的FMalloc类型,基类默认返回默认的Ansi内存分配器(标准C),而不同平台会有自己特殊的实现。
如果想知道当前平台使用了哪种分配器,可以查看对应的PlatformMemory文件。例如WindowsPlatformMemory.cpp文件,找到BaseAllocator函数,会发现Windows平台默认使用了TBB分配器。
在FMemory::Malloc申请内存时,会先判断FMalloc* GMalloc(全局的内存分配器)是否已创建,如果没有创建,void FMemory::GCreateMalloc() --》int FMemory_GCreateMalloc_ThreadUnsafe() --》FMalloc* FPlatformMemory::BaseAllocator()来初始化GMalloc
FMemory是一个静态工具类,对FMalloc* GMalloc进行了封装,具体逻辑详见:
UnrealEngine\Engine\Source\Runtime\Core\Public\HAL\UnrealMemory.h
UnrealEngine\Engine\Source\Runtime\Core\Public\HAL\FMemory.inl
UnrealEngine\Engine\Source\Runtime\Core\Private\HAL\UnrealMemory.cpp
Ansi | TBB | Jemalloc | Binned | Binned2 | Binned3 | Mimalloc | Stomp | |
Android | 支持 | 支持 | 默认 | 支持(64bits) | ||||
IOS | 支持 | 默认 | ||||||
Windows | 支持 | 默认 | 支持 | 支持 | 支持(64bits) | 支持 | 支持 | |
Linux | 支持 | 支持 | 支持 | 默认 | 支持 | |||
Mac |
支持 |
默认 | 支持 | 支持 | 支持 | |||
HoloLens |
支持 |
默认 |
对于IOS的Binned、Ansi的对比:
① Binned内存相较于Ansi要高 10-30M左右
② Binned在3,4档机器帧率相较于Ansi要好1-2FPS, 1,2档机器无明显差异
③ Binned的卡顿率相较于Ansi要好一些
Android Memory子系统
① 48位可寻址内存空间
② 内存页为4KB
③ 受OS限制,每个进程有64K个虚拟内存区域(Virtual Memory Areas,VMA,一个内核的内存数据结构)
如果在极限情况下(每个VMA只有4KB),则进程最多只能有256MB内存
Binned2通过Hold住一些page不还给OS,来减少空洞,避免VMA碎片化,来缓解VMA个数限制带来的问题
VMA被一个线程安全的红黑树管理,用来管理内核分配的所有虚拟内存
一个VMA由相同属性的连续page组成
在中间部位释放内存或修改page的属性,将导致1个VMA变成2个VMA
iOS上只有非常有限的虚拟地址空间,几乎等于物理RAM内存的大小,使得无法使用Binned2、Binned3使用一些很酷的技巧,只能使用32位的Binned
iOS14开始提供了Extended Virtual Addressing Entitlement
① 扩展虚拟内存地址空间到55GB
② 可以使用Binned2、Binned3
③ UE5.2打算支持该特性
iOS15开始提供了increased-memory-limit
UE5.1 目前是ios14为最低OS版本,后续打算把ios15设为最低OS版本
参考1:[UF2022]安卓iOS高阶开发和调试指南(官方字幕)
扩展:com.apple.developer.sustained-execution(维持高性能)
Android下可在/storage/emulated/0/UE4Game目录中放置flag文件来动态启用哪种内存分配器:
FMalloc* FAndroidPlatformMemory::BaseAllocator() { // ... ... if (access("/storage/emulated/0/UE4Game/binned3.flag", 0) == 0) { return new FMallocBinned3(); } // ... ... }
iOS下可在Documents目录中放置flag文件来动态启用哪种内存分配器:
FMalloc* FApplePlatformMemory::BaseAllocator() { // ... ... #if PLATFORM_IOS NSString* DocumentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]; NSString* FlagFilePath = [DocumentsPath stringByAppendingPathComponent : @"/ansi.flag"]; if (access([FlagFilePath UTF8String], 0) == 0) { AllocatorToUse = EMemoryAllocatorToUse::Ansi; } #endif // ... ... }
注1:FUseSystemMallocForNew实现new、new[]、delete、delete[]操作符,主要是为了防止new FMalloc对象时调用到全局的new、new[]、delete、delete[],导致死循环
注2:在游戏线程中,可显示调用Trim函数来释放未被使用的内存页。另外,在gc mark时会调用该函数。 -- FMallocBinned没有实现该函数
注3:FMalloc逻辑详见:UnrealEngine\Engine\Source\Runtime\Core\Public\HAL\MemoryBase.h、UnrealEngine\Engine\Source\Runtime\Core\Private\HAL\MemoryBase.cpp
游戏启动时,会打印出当前所用的内存分配器(Alloctator) 详见:void FApp::PrintStartupLogMessages()函数
LogInit: WinSock: version 1.1 (2.2), MaxSocks=32767, MaxUdp=65467
LogOnline: OSS: TryLoadSubsystemAndSetDefault: Loaded subsystem for module [NULL]
LogInit: Build: ++UE4+Release-4.26-CL-0
LogInit: Engine Version: 4.26.1-0+++UE4+Release-4.26
LogInit: Compatible Engine Version: 4.26.0-0+++UE4+Release-4.26
LogInit: Net CL: 0
LogInit: OS: Windows 10 (Release 1903) (), CPU: Intel(R) Core(TM) i9-9900 CPU @ 3.10GHz, GPU: NVIDIA GeForce RTX 2080
LogInit: Compiled (64-bit): Mar 25 2021 19:11:23
LogInit: Compiled with Visual C++: 19.27.29111.00
LogInit: Build Configuration: Debug
LogInit: Branch Name: ++UE4+Release-4.26
LogInit: Command Line: ThirdPersonExampleMap -messaging -Windowed ResX=800 ResY=600 -game -skipcompile -debug
LogInit: Base Directory: G:/svn/UnrealEngine/Engine/Binaries/Win64/
LogInit: Allocator: TBB
LogInit: Installed Engine Build: 0
内存体系结构
注:LLM作为UE底层的内存Tracking工具,其LLMAlloc和LLMFree内存函数就是基于VirtualAlloc/VirtualFree,mmap/munmap实现的
详见:WindowsPlatformMemory.cpp、AndroidPlatformMemory.cpp、ApplePlatformMemory.cpp
重写全局new、delete
详见:UnrealEngine\Engine\Source\Runtime\Core\Public\Modules\Boilerplate\ModuleBoilerplate.h
OPERATOR_NEW_MSVC_PRAGMA void* operator new ( size_t Size ) OPERATOR_NEW_THROW_SPEC { return FMemory::Malloc( Size ); } // 正常版本new OPERATOR_NEW_MSVC_PRAGMA void* operator new[]( size_t Size ) OPERATOR_NEW_THROW_SPEC { return FMemory::Malloc( Size ); } // 正常版本new[] OPERATOR_NEW_MSVC_PRAGMA void* operator new ( size_t Size, const std::nothrow_t& ) OPERATOR_NEW_NOTHROW_SPEC { return FMemory::Malloc( Size ); } // 兼容早期版本的new,内存分配失败不会抛出异常 OPERATOR_NEW_MSVC_PRAGMA void* operator new[]( size_t Size, const std::nothrow_t& ) OPERATOR_NEW_NOTHROW_SPEC { return FMemory::Malloc( Size ); } // 兼容早期版本的new[],内存分配失败不会抛出异常 void operator delete ( void* Ptr ) OPERATOR_DELETE_THROW_SPEC { FMemory::Free( Ptr ); } // 正常版本delete void operator delete[]( void* Ptr ) OPERATOR_DELETE_THROW_SPEC { FMemory::Free( Ptr ); } // 正常版本delete[] void operator delete ( void* Ptr, const std::nothrow_t& ) OPERATOR_DELETE_NOTHROW_SPEC { FMemory::Free( Ptr ); } // 兼容早期版本的delete,内存释放失败不会抛出异常 void operator delete[]( void* Ptr, const std::nothrow_t& ) OPERATOR_DELETE_NOTHROW_SPEC { FMemory::Free( Ptr ); } // 兼容早期版本的delete,内存释放失败不会抛出异常 void operator delete ( void* Ptr, size_t Size ) OPERATOR_DELETE_THROW_SPEC { FMemory::Free( Ptr ); } // 正常版本delete(带size) void operator delete[]( void* Ptr, size_t Size ) OPERATOR_DELETE_THROW_SPEC { FMemory::Free( Ptr ); } // 正常版本delete[](带size) void operator delete ( void* Ptr, size_t Size, const std::nothrow_t& ) OPERATOR_DELETE_NOTHROW_SPEC { FMemory::Free( Ptr ); } // 兼容早期版本的delete(带size),内存释放失败不会抛出异常 void operator delete[]( void* Ptr, size_t Size, const std::nothrow_t& ) OPERATOR_DELETE_NOTHROW_SPEC { FMemory::Free( Ptr ); } // 兼容早期版本的delete[](带size),内存释放失败不会抛出异常
裸指针 new / delete
{ int32* p1 = new int32; delete p1; }
申请内存时:
释放内存时:
裸指针 new [] / delete []
{ float* p2 = new float[5]; delete[] p2; }
申请内存时:
释放内存时:
智能指针 new / delete
{ TUniquePtr<double> up1(new double(10.8)); }
申请内存时:
释放内存时:
智能指针 new[] / delete[]
{ TUniquePtr<char[]> up2 = MakeUnique<char[]>(25); }
申请内存时:
释放内存时:
UE4容器
TArray缺省内存分配器为FDefaultAllocator --》TSizedDefaultAllocator<32> --》TSizedHeapAllocator
TMap、TSet缺省内存分配器为FDefaultSetAllocator --》TSetAllocator --》TSparseArrayAllocator<FDefaultAllocator,FDefaultBitArrayAllocator>
详见:UnrealEngine\Engine\Source\Runtime\Core\Public\Containers\ContainersFwd.h
template<int IndexSize> class TSizedDefaultAllocator; using FDefaultAllocator = TSizedDefaultAllocator<32>; using FDefaultAllocator64 = TSizedDefaultAllocator<64>; class FDefaultSetAllocator; template<typename T, typename Allocator = FDefaultAllocator> class TArray; template<typename KeyType, typename ValueType, bool bInAllowDuplicateKeys> struct TDefaultMapHashableKeyFuncs; template<typename KeyType, typename ValueType, typename SetAllocator = FDefaultSetAllocator, typename KeyFuncs = TDefaultMapHashableKeyFuncs<KeyType, ValueType, false> > class TMap; template<typename KeyType, typename ValueType, typename SetAllocator = FDefaultSetAllocator, typename KeyFuncs = TDefaultMapHashableKeyFuncs<KeyType, ValueType, true > > class TMultiMap; template <typename T = void > struct TLess; template <typename> struct TTypeTraits; template<typename KeyType, typename ValueType, typename ArrayAllocator = FDefaultAllocator, typename SortPredicate = TLess<typename TTypeTraits<KeyType>::ConstPointerType> > class TSortedMap; template<typename ElementType,bool bInAllowDuplicateKeys = false> struct DefaultKeyFuncs; template<typename InElementType, typename KeyFuncs = DefaultKeyFuncs<InElementType>, typename Allocator = FDefaultSetAllocator> class TSet;
FString
{ FString str1 = TEXT("Hello World!");// 字符串长度为12,Realloc中NewSize值:(字符串长度+1) * 2 = 26 } // 离开作用域,调用Free释放内存
注:FString中包含一个TArray<TCHAR> Data的成员变量
申请内存时:
释放内存时:
TArray
{ TArray<int32> arr1; arr1.Add(12); // Add第一个元素时,ArrayMax为4,Realloc中NewSize值:16 注:TArray申请的空间要多一些,不会Add一个,申请一个的内存,当空间不够时会按照一定的算法来扩容 详见:DefaultCalculateSlackGrow函数 arr1.Add(13); } // 离开作用域,调用Free释放内存
申请内存时:
释放内存时:
TSet
{ TSet<double> set1; set1.Add(1.2); // Add第一个元素时,申请内存,Realloc中NewSize值:64 注:TSet申请的空间要多一些,不会Add一个,申请一个的内存,当空间不够时会按照一定的算法来扩容 set1.Add(2.5); } // 离开作用域,调用Realloc释放内存(NewSize传入0)
申请内存时:
释放内存时:
TMap
{ TMap<int32, int32> map1; map1.Add(1, 18);// Add第一个元素时,申请内存,Realloc中NewSize值:64 注:TMap申请的空间要多一些,不会Add一个,申请一个的内存,当空间不够时会按照一定的算法来扩容 map1.Add(2, 28); map1.Add(3, 38); } // 离开作用域,调用Realloc释放内存(NewSize传入0)
申请内存时:
释放内存时:
UObject
FString MyBPObjectPath = TEXT("/Game/ThirdPersonCPP/Blueprints/MyBlueprintObject.MyBlueprintObject_C"); UClass* MyBPObjectClass = LoadClass<UObject>(nullptr, *MyBPObjectPath); // MyBPObjectClass为UBlueprintGeneratedClass*类型 UMyBPObject* BPObj1 = NewObject<UMyBPObject>(this, MyBPObjectClass); // sizeof(UMyBPObject)为48 注:考虑到内存对齐,AllocateUObject时传入的Size为64 CollectGarbage(RF_NoFlags); // 全量阻塞gc
申请内存时:
释放内存时:
PhysX物理
FPhysXAllocator从PhysX引擎的PxAllocatorCallback类上派生。在引擎初始化时调用PxCreateFoundation函数传入全局的FPhysXAllocator GPhysXAllocator对象来接管物理相关的内存分配。
// UnrealEngine\Engine\Source\Runtime\PhysicsCore\Public\PhysXSupportCore.h class PHYSICSCORE_API FPhysXAllocator : public PxAllocatorCallback { // 。。。 。。。 virtual void* allocate(size_t size, const char* typeName, const char* filename, int line) override; virtual void deallocate(void* ptr) override; // 。。。 。。。 }; // UnrealEngine\Engine\Source\Runtime\PhysicsCore\Private\PhysicsInitialization.cpp // 在FEngineLoop::PreInitPreStartupScreen函数中会调用InitGamePhys() --> InitGamePhysCore() bool InitGamePhysCore() { // 。。。 。。。 // Create Foundation GPhysXAllocator = new FPhysXAllocator(); FPhysXErrorCallback* ErrorCallback = new FPhysXErrorCallback(); GPhysXFoundation = PxCreateFoundation(PX_FOUNDATION_VERSION, *GPhysXAllocator, *ErrorCallback); // 。。。 。。。 }
FMemory::SystemMalloc(SIZE_T Size) / FMemory::SystemFree(void* Ptr)
这2个函数封装了malloc和free,调用它们会直接使用C标准分配器分配和释放内存
struct CORE_API FMemory { // ... ... // // C style memory allocation stubs that fall back to C runtime // static FORCEINLINE void* SystemMalloc(SIZE_T Size) { return ::malloc(Size); } static FORCEINLINE void SystemFree(void* Ptr) { ::free(Ptr); } // ... ... };