轉自:http://blog.csdn.net/cartzhang/article/details/50524317html
本文章由cartzhang編寫,轉載請註明出處。 全部權利保留。
文章連接:http://blog.csdn.net/cartzhang/article/details/50524317
做者:cartzhangjava
本篇譯文同發與蠻牛譯館,
地址:http://www.manew.com/thread-46327-1-1.html?_dsign=ae91354agit
從我上次談論內存申請和跟蹤已經有一段時間了。我得抽出時間來在虛幻4上實現跟蹤,而且已經完成了。我假設你已經閱讀過來以前的博客:「虛幻引擎4中內存跟蹤功能的侷限性」和「內存申請和跟蹤」。github
虛幻引擎4中,有三種基本的內存分配和釋放方法:
1.使用GMalloc指針。這種方法是得到全局的分配器,分配器的使用依賴於GCreateMalloc()函數。
2.FMemory函數。有靜態函數好比:Malloc(),Realloc(),Free()。他們也是使用GMalloc來申請內存,可是在此以前,它會在每次allocation, reallocation或free以前檢查GMalloc 是否認義。若GMalloc 是空,就調用GCreateMalloc() 。
3.全局的的New和delete操做。缺省狀況下,只在模塊的ModuleBoilerplate.h 的文件中定義,也就是說,不少調用new和delete的操做不在虛幻4的內存系統中管理。重載操做實際上調用的是FMemory函數。api
這些狀況就會出現使用這些機制就可能出現內存不會釋放和清空的問題。爲了撲捉這些問題,我提交了一個申請,已經被整合併發布在版本4.9上,經過C運行時庫調用_CrtSetAllocHook()來獲取這些分配。其中一個例子,是引擎中Zlib集成並無使用引擎工具來分配,它使用了_CrtSetAllocHook() ,我提交了一個修復版本在4.9版本發佈。數組
直接調用GMalloc 和FMemory 函數這兩個基礎的API,以下:多線程
virtual void* Malloc( SIZE_T Count, uint32 Alignment = DEFAULT_ALIGNMENT ) = 0; virtual void* Realloc( void* Original, SIZE_T Count, uint32 Alignment = DEFAULT_ALIGNMENT ) = 0; virtual void Free( void* Original ) = 0; These are the places that would need to be modified if we want to add any new piece of data per allocation.
爲了寫法相似,我重載了分配器,我從FMalloc繼承了一個新類,叫作FMallocTracker,這樣就能夠勾到虛幻的內存分配系統上。由於一個有效的分配器必須在建立FMallocTracker 實例時全部實際的分配已經由其分配器完成。FMallocTracker 只是保存了跟蹤信息。可是這是不夠的,實際上須要知道分配器全部方法來跟蹤數據。所以,第一步就是當咱們使用內存跟蹤功能時,修改分配器函數。併發
#if USE_MALLOC_TRACKER virtual void* Malloc( SIZE_T Count, uint32 Alignment, const uint32 GroupId, const char * const Name ) = 0; virtual void* Realloc( void* Original, SIZE_T Count, uint32 Alignment, const uint32 GroupId, const char * const Name ) = 0; #else virtual void* Malloc( SIZE_T Count, uint32 Alignment = DEFAULT_ALIGNMENT ) = 0; virtual void* Realloc( void* Original, SIZE_T Count, uint32 Alignment = DEFAULT_ALIGNMENT ) = 0; #endif // USE_MALLOC_TRACKER
新參數:
名稱:分配名稱。這個名稱能夠任何名稱,可是建議寫易懂便於搜索。在本文後面,將會展現更多相關內容。
分組:組ID是針對於當前所分配的。有些分組我已經定義過來,可是有些你須要根據你的須要來定義。編輯器
這個改變就意味着,全部的分配器在引擎中都是透明的,一旦完成,你能夠標記分配器,而不用擔憂底層的實現。標記分配的目的不只僅是爲了跟蹤,也涉及到代碼調試。一旦這些便籤在很大的代碼庫中實現後,當內存飆升,處理不一樣組的交互,修復相關的內存崩潰時,就會有很大的好處了。ide
下一步是集成new和delete操做。我以前提到過,在引擎的ModuleBoilerplate.h文件中已經定義好,爲了更好的的覆蓋,我把它移動到MemoryBase.h中。下一步是定義新的重載操做,並傳入名稱和分組。
OPERATOR_NEW_MSVC_PRAGMA FORCEINLINE void* operator new (size_t Size, const uint32 Alignment, const uint32 GroupId, const char * const Name) OPERATOR_NEW_NOTHROW_SPEC{ return FMemory::Malloc(Size, Alignment, GroupId, Name); } OPERATOR_NEW_MSVC_PRAGMA FORCEINLINE void* operator new[](size_t Size, const uint32 Alignment, const uint32 GroupId, const char * const Name) OPERATOR_NEW_NOTHROW_SPEC{ return FMemory::Malloc(Size, Alignment, GroupId, Name); } OPERATOR_NEW_MSVC_PRAGMA FORCEINLINE void* operator new (size_t Size, const uint32 Alignment, const uint32 GroupId, const char * const Name, const std::nothrow_t&) OPERATOR_NEW_NOTHROW_SPEC { return FMemory::Malloc(Size, Alignment, GroupId, Name); } OPERATOR_NEW_MSVC_PRAGMA FORCEINLINE void* operator new[](size_t Size, const uint32 Alignment, const uint32 GroupId, const char * const Name, const std::nothrow_t&) OPERATOR_NEW_NOTHROW_SPEC { return FMemory::Malloc(Size, Alignment, GroupId, Name); }
爲避免使用USE_MALLOC_TRACKER來進行檢測,提供這些定義來建立這些申請,在使用USE_MALLOC_TRACKER設置,但當不設置時並不增長沒必要要的開銷。其目的是不增長任何沒必要要的開銷。下面是基本定義:
#if USE_MALLOC_TRACKER #define PZ_NEW(GroupId, Name) new(DEFAULT_ALIGNMENT, (Name), (GroupId)) #define PZ_NEW_ALIGNED(Alignment, GroupId, Name) new((Alignment), (Name), (GroupId)) #define PZ_NEW_ARRAY(GroupId, Name, Type, Num) reinterpret_cast<##Type*>(FMemory::Malloc((Num) * sizeof(##Type), DEFAULT_ALIGNMENT, (Name), (GroupId))) #define PZ_NEW_ARRAY_ALIGNED(Alignment, GroupId, Name, Type, Num) reinterpret_cast<##Type*>(FMemory::Malloc((Num) * sizeof(##Type), (Alignment), (Name), (GroupId))) #else #define PZ_NEW(GroupId, Name) new(DEFAULT_ALIGNMENT) #define PZ_NEW_ALIGNED(Alignment, GroupId, Name) new((Alignment)) #define PZ_NEW_ARRAY(GroupId, Name, Type, Num) reinterpret_cast<##Type*>(FMemory::Malloc((Num) * sizeof(##Type), DEFAULT_ALIGNMENT)) #define PZ_NEW_ARRAY_ALIGNED(Alignment, GroupId, Name, Type, Num) reinterpret_cast<##Type*>(FMemory::Malloc((Num) * sizeof(##Type), (Alignment))) #endif // USE_MALLOC_TRACKER
下面是兩個例子,說明在代碼中,帶標籤和不帶標籤內存分配的比較:
分配器的跟蹤由容器來完成
在命名分配空間時出現了一個問題,用一個簡單方法來識別,咱們使用容器來處理。引擎中全部對象幾乎不可能只有一個單例,在作遊戲時,可放置的例子,幾個玩家,因此引擎中使用不少容器。在使用帶容器的分配器時,使用一個通用的名字是沒有什麼用處的。咱們來看看這個FMeshParticleVertexFactory::DataType例子:
/** The streams to read the texture coordinates from. */ TArray<FVertexStreamComponent,TFixedAllocator<MAX_TEXCOORDS> > TextureCoordinates;
在容器中,一個普通的名字分配器相似於「TFixedAllocator::ResizeAllocation」。沒有太多意義。反之,對於容器來講,一個好的名字像這樣「FMeshParticleVertexFactory::DataType::TextureCoordinates」。所以,咱們須要給容器標記名稱和分組,如此以來,當一個分配器在容器中後,經過容器的名稱和分組得到全部的內存分配。所以,咱們須要改變容器,讓分配器額可使用這些容器。這將涉及到當使用USE_MALLOC_TRACKER時,須要爲每一個容器添加指針和一個32位的無符號整數,並修改必要的構造函數來添加選項信息。TArray的一個構造函數以下:
TArray(const uint32 GroupId = GROUP_UNKNOWN, const char * const Name = "UnnamedTArray") : ArrayNum(0) , ArrayMax(0) #if USE_MALLOC_TRACKER , ArrayName(Name) , ArrayGroupId(GroupId) #endif // USE_MALLOC_TRACKER {}
這樣以來,咱們能夠把必要的信息傳遞給分配器來標記這些分配。接下來是要保證把這些改變信息傳遞到底層內存分配器中被使用。這些容器分配器一般使用FMemory來分配內存,FContainerAllocatorInterface定義ResizeAllocation 函數來作實際的內存申請。與以前的修改同樣,咱們須要爲內存分配添加名稱和分組。
#if USE_MALLOC_TRACKER void ResizeAllocation(int32 PreviousNumElements, int32 NumElements, SIZE_T NumBytesPerElement, const uint32 GroupId, const char * const Name); #else void ResizeAllocation(int32 PreviousNumElements, int32 NumElements, SIZE_T NumBytesPerElement); #endif // USE_MALLOC_TRACKER
一樣,由於咱們不想使用那個ifdefs來填充引擎的代碼,咱們再次使用定義來簡化:
#if USE_MALLOC_TRACKER #define PZ_CONTAINER_RESIZE_ALLOCATION(ContainerPtr, PreviousNumElements, NumElements, NumBytesPerElement, GroupId, Name) (ContainerPtr)->ResizeAllocation((PreviousNumElements), (NumElements), (NumBytesPerElement), (GroupId), (Name)) #else #define PZ_CONTAINER_RESIZE_ALLOCATION(ContainerPtr, PreviousNumElements, NumElements, NumBytesPerElement, GroupId, Name) (ContainerPtr)->ResizeAllocation((PreviousNumElements), (NumElements), (NumBytesPerElement)) #endif // USE_MALLOC_TRACKER
這樣,咱們能夠把ArrayName和ArrayGroup傳遞給容器分配器。
在構造以後,還有一個須要修改容器的名稱或分組,由於給容器的容器命名分配器是很是有必要的。其中的一個例子就是,在任一TMap容器中FindOrAdd後,咱們須要設置名稱或分組。
/** Map of object to their outers, used to avoid an object iterator to find such things. **/ TMap<UObjectBase*, TSet<UObjectBase*> > ObjectOuterMap; TMap<UClass*, TSet<UObjectBase*> > ClassToObjectListMap; TMap<UClass*, TSet<UClass*> > ClassToChildListMap;
這樣以來,全部的容器內存分配器有了標籤屬性。如今,咱們須要的是給容器設置名稱。以FMeshParticleVertexFactory::DataType::TextureCoordinates爲例,咱們能夠設置它的名稱和分組:
DataType() : TextureCoordinates(GROUP_RENDERING, "FMeshParticleVertexFactory::DataType::TextureCoordinates") , bInitialized(false) { }
做爲「內存申請和跟蹤」博客中一部分,爲提供上下文連接,我說起到內存分配定義做用域的必要性。這個做用域與調用棧(它已經由MallocProfiler提供)不同。不少分配在同一棧中,可是涉及到徹底不一樣的對象。在使用藍圖過程當中更是廣泛。正是因爲這個,做用域在跟蹤或甚至帶藍圖的內存使用都是很是有用的。
爲利用引擎中已有的代碼,我採用了重用FScopeCycleCounterUObject 結構體的方法,這個結構體用來在狀態系統中定義做用域的相關對象。引擎已經給他們配置了必要的做用域,而且你也可使用FMallocTrackerScope 類來放置咱們的內存跟蹤特性的做用域。也在每一個FScopeCycleCounterUObject上自動建立的兩個域的可見性上作了改進,一個是對象類名的域,一個是對象名的域。這樣當咱們最終建立一個可視數據工具時,對每一個類名進行摺疊時就會更簡單。讓咱們從精靈Demo來看一看單獨做用域,它是一個感受還不錯的複雜東西。
咱們分析做用域下的內存分配,結果以下:
Address Thread Name Group Bytes Name
0x0000000023156420 Main Thread UObject 96 InterpGroupInst
0x00000000231cf000 Main Thread Unknown 64 UnnamedTSet
0x0000000023168480 Main Thread UObject 80 InterpTrackInstMove
0x0000000028ee8480 Main Thread Unknown 64 UnnamedTSet
0x0000000022bc2420 Main Thread Unknown 32 UnnamedTArray
0x00000000231563c0 Main Thread UObject 96 InterpGroupInst
0x00000000231cefc0 Main Thread Unknown 64 UnnamedTSet
0x0000000023168430 Main Thread UObject 80 InterpTrackInstMove
0x00000000231cef80 Main Thread Unknown 64 UnnamedTSet
0x0000000022bc2400 Main Thread Unknown 32 UnnamedTArray
0x0000000023156360 Main Thread UObject 96 InterpGroupInst
0x00000000231cef40 Main Thread Unknown 64 UnnamedTSet
0x00000000231683e0 Main Thread UObject 80 InterpTrackInstMove
0x0000000028ee8380 Main Thread Unknown 64 UnnamedTSet
0x0000000022bc23e0 Main Thread Unknown 32 UnnamedTArray
0x00000000231cef00 Main Thread UObject 64 InterpTrackInstAnimControl
0x00000000231ceec0 Main Thread UObject 64 InterpTrackInstVisibility
在藍圖的運行函數中只有17個內存分配。當我撲捉精靈Demo中實際的內存分配數爲584454。惟一名稱的做用域數量高達4175。還有咱們在捕捉時的內存分配爲607M,而內存峯值爲603M。這說明了對於這些須要內存跟蹤的必要性。
正如以前所說,MallocTracker 的使用方法與以前內存分配很類似。MallocTracker 是輕量級的,而且依據在「內存分配和跟蹤」文章中所說的性能需求。
使用方法在缺省狀態下是足夠快的,不會有太多的性能影響,並在內存方面有至關地的開銷。例如元素Demo顯示跟蹤開銷~30M,在Debug模式下CPU時間低於2ms,更不用說在優化發佈版本下了。與平時同樣,在內存消耗和性能以前有個取捨,這些數字取決於我所選取的方法。還有其餘的方法,能夠優化性能或內存開銷,可是我想仍是保持合理的平衡。
爲分析應用,咱們來看一個具體的例子。下面是當 FMemory::Malloc()調用時所發生的:
1.
FMemory::Malloc() 被調用,名字和分組須要必定的字節分配。
2.
FMemory::Malloc()調用帶一樣參數的FMallocTracker::Malloc(),假設GMalloc指針指向的是FMallocTracker 的實例。
3.
FMallocTracker::Malloc()在FMallocTracker建立期間,使用傳進來的分配器分配實際的內存,本例中是FMallocBinned類。
4.
FMallocTracker::Malloc() 自動修改一些全局內存分配狀態,例如內存分配峯值內存大小,內存峯值數等等。
5.
FMallocTracker::Malloc()關聯到當前線程PerThreadData 實例。
6.
FMallocTracker::Malloc() 調用PerThreadData::AddAllocation來保存在此線程容器中的的內存分配數據。
7.
FMallocTracker::Malloc() 返回指針給步驟三中的底層內存分配器。
幾乎不包含全局狀態。只是給你一個快速的概覽而已。全局狀態包括:
分配的字節。數據入棧時分配字節數。
分配次數。數據入棧時分配次數。數越大就會有更多的內存碎片。
分配字節的峯值。從MallocTracker 可用之後的最大字節分配數。
內存分配峯值次數。從MallocTracker 可用後,實時分配的最大數。
消耗字節。MallocTracker 的內部開銷字節數。
從全部線程分配內存開始,全部這些狀態自動更新。
爲提升性能和避免多線程下內存分配和釋放的資源競爭,大部分工做在每一個線程基礎中完成。全部內存分配和棧的做用域範圍被存儲在每一個線程中。全部分配有一個相對的棧域定義,最大域範圍爲GlobalScope。一樣的域名常出如今多域棧中。由此,爲最小化內存開銷,某個線程的全部域名被存儲爲獨一無二的,並關聯到域的棧上。由於域棧能夠在多個內存分配中顯示,全部咱們能夠獨一無二的保存在域棧上。咱們來看具體的例子,域名爲藍色,分配器爲桔黃色:
爲保存數據,咱們有三個不一樣的數組,它們在不一樣線程中不共享:
惟一域名。保存本線程中惟一域名。至少GlobalScope 要在這裏。它將會保存它們加入到棧中的新的域名。
惟一的域棧。它用一個固定長度的動態數組保存惟一棧,用索引指向相關的域名。
分配器。每一個分配器的數據。它包含分配器地址,字節大小,分組和名稱,還有惟一域棧的索引。
若咱們參考以前的圖,咱們能夠看到五個分配器。下面爲5個分配器的數據存儲:
從新分配內存和釋放內存有點複雜,由於實際上在虛幻引擎中在一個線程中分配多個實例,而後被從新分配或在其餘線程釋放或是很廣泛的。也就是說,咱們不能假設咱們找到每一個調用線程上的每一個線程數據。由於那樣的話,也就意味着咱們須要加一些鎖。爲減小競爭,使用全局鎖而不是每一個線程數據類有本身的鎖。在從新分配和釋放當前調用線程的每一個線程數據時,對當前存在分配器進行檢測。若沒有找到,當前分配器在其餘線程數據中在處理中被鎖定了。這用來確保減小競爭,並讓它們儘量的忙碌。
爲使MallocTracker足夠快和內存消耗可被接受,我不得不制定嚴格的命名規範,不管是分配內存或域名命名。這種限制是,這些名字與內存的生命週期內,必須一致或比實際申請或域的存在時間要長。緣由是,任何數據拷貝影響性能和內存開銷,因此只保存指針。雖然這貌似是一個複雜的規則,我以爲這個是徹底正常的,由於你應該知道你分配器的生命週期。若你不知道你分配器的生命週期,爲要知道這些名字要存在多久,那麼你有了大麻煩處理了。
另一個關於分配器和域名的特別應用是須要ANSI和寬字節。爲使這些更透明,全部指針假定爲ANSI,除非指針中的第63位字節被設置,它會假定指針指向一個寬字節。FMallocTracker提供了一個獲取爲寬字節設置位的指針操做方法,並對寬字節或ANSI的FNames在必要狀況下可設置。在輸出到文件時,名字是正確的並轉存到文件中。
只有當製做了一個可視的工具來展現數據時,我纔會說這個系統真的頗有用啊!你能夠繼續個人工做,它真的頗有用。查找碎片問題和處理內存使用使用這個數據會更加簡單。這個真的比引擎中已經提供的性能工具,內存消耗和數據質量要好。
下一步將真正全面轉換引擎使用標籤內存分配,可是仍有事情必須完成。若大部份內存沒有衝突,真的沒有必要花費太多時間來只是爲了標記內存分配。它不僅是更好標記大的內存分配,而是獲取更多的洞察細節的問題。儘管你會以爲標記分配器很是無聊,可是你仍會得到有用的數據。下面是精靈Demo中截取的最大分配器的數據:
爲說明問題,我提供了樣例數據。樣例數據來自測試版本的精靈Demo的修改版本的運行。你能夠在這裏下載,在支持大文件的文本編輯器中打開。
若Epic Game 接受了個人代碼更新請求,你能夠經過下載我上傳請求到Epic Games的代碼來查看。上傳請求的有效地址:虛幻4上傳請求地址。
Video overview.
視頻概述
[譯者注:28分鐘的視頻,有須要的能夠下!]
原文地址:https://pzurita.wordpress.com/2015/08/26/adding-memory-tracking-features-to-unreal-engine-4/