可可西

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):直接调用mallocfreerealloc函数   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来启用该分配器

Mimallochttps://github.com/microsoft/mimalloc

UE4内置的内存分配器

Binned(第一代箱式内存分配器):用于32位地址空间  例如:iOS(虽然iOS系统是64位地址空间,但App的虚拟内存子系统被OS限制在32位范围)

Binned2(第二代箱式内存分配器):可用于Linux、Android

Binned3(第三代箱式内存分配器,仅支持64bits):比Binned2更快一些,内存也更省一些

 

对于不同平台而言,都有自己对应的平台内存管理类,它们继承自FGenericPlatformMemory,封装了平台相关的内存操作。

具体而言,包含FAndroidPlatformMemoryFApplePlatformMemoryFIOSPlatformMemoryFWindowsPlatformMemoryFLinuxPlatformMemoryFUnixPlatformMemoryFMacPlatformMemory等。

通过调用FPlatformMemoryBaseAllocator函数,我们取得平台对应的FMalloc类型,基类默认返回默认的Ansi内存分配器(标准C),而不同平台会有自己特殊的实现。

如果想知道当前平台使用了哪种分配器,可以查看对应的PlatformMemory文件。例如WindowsPlatformMemory.cpp文件,找到BaseAllocator函数,会发现Windows平台默认使用了TBB分配器。

FMemory::Malloc申请内存时,会先判断FMalloc* GMalloc(全局的内存分配器)是否已创建,如果没有创建,void FMemory::GCreateMalloc() --》int FMemory_GCreateMalloc_ThreadUnsafe() --》FMallocFPlatformMemory::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高阶开发和调试指南(官方字幕)

参考2:内存扩展:UE 中利用 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实现newnew[]、deletedelete[]操作符,主要是为了防止new FMalloc对象时调用到全局的newnew[]、deletedelete[],导致死循环

注2:在游戏线程中,可显示调用Trim函数来释放未被使用的内存页。另外,在gc mark时会调用该函数。  -- FMallocBinned没有实现该函数

注3:FMalloc逻辑详见:UnrealEngine\Engine\Source\Runtime\Core\Public\HAL\MemoryBase.hUnrealEngine\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工具,其LLMAllocLLMFree内存函数就是基于VirtualAlloc/VirtualFreemmap/munmap实现的

       详见:WindowsPlatformMemory.cppAndroidPlatformMemory.cppApplePlatformMemory.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

TMapTSet缺省内存分配器为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);
    }
    
    // ... ...
};

 

posted on 2021-04-13 00:22  可可西  阅读(7326)  评论(1编辑  收藏  举报

导航