[TOC]
此优化需要使用高版本的NDK,并且只支持 arm64-v8a 架构
建议使用骁龙855或更强的CPU来调试大型手游项目
虚幻引擎建议打Test包,去修改FMallocBinned来达到最佳性能
测试环境:
- Unreal Engine 4.25 / 4.26,Android NDK r21c
- Unreal Engine 4.24.3,Android NDK r14b
Unreal Engine 4.25 使用 FMallocBinned2 作为安卓平台默认的内存分配器。
建议修改此分配器,而不是直接切换到使用系统malloc的FMallocAnsi,此分配器性能远超系统malloc。
--- a/Engine/Source/Runtime/Core/Public/HAL/MallocBinned2.h
+++ b/Engine/Source/Runtime/Core/Public/HAL/MallocBinned2.h
@@ -89,6 +89,21 @@ extern int32 RecursionCounter;
#endif
+#ifdef USE_LOLI_PROFILER
+#ifdef __cplusplus
+extern "C" {
+#endif // __cplusplus
+
+ extern void (*loli_alloc_ptr)(void*, size_t);
+ extern void (*loli_free_ptr)(void*);
+
+#ifdef __cplusplus
+}
+#endif // __cplusplus
+#endif // USE_LOLI_PROFILER
@@ -444,6 +459,13 @@ public:
--RecursionCounter;
return Result;
#else
+#ifdef USE_LOLI_PROFILER
+ void* Ptr = MallocInline(Size, Alignment);
+ loli_alloc_ptr(Ptr, Size);
+ return Ptr;
+#endif
return MallocInline(Size, Alignment);
#endif
}
@@ -529,6 +551,14 @@ public:
--RecursionCounter;
return Result;
#else
+#ifdef USE_LOLI_PROFILER
+ loli_free_ptr(Ptr);
+ void* NewPtr = ReallocInline(Ptr, NewSize, Alignment);
+ loli_alloc_ptr(NewPtr, NewSize);
+ return NewPtr;
+#endif
return ReallocInline(Ptr, NewSize, Alignment);
#endif
}
@@ -613,6 +643,11 @@ public:
}
--RecursionCounter;
#else
+#ifdef USE_LOLI_PROFILER
+ loli_free_ptr(Ptr);
+#endif
FreeInline(Ptr);
#endif
}
--- a/Engine/Source/Runtime/Core/Private/HAL/MallocBinned2.cpp
+++ b/Engine/Source/Runtime/Core/Private/HAL/MallocBinned2.cpp
@@ -11,6 +11,34 @@
#include "HAL/PlatformMisc.h"
#include "Misc/App.h"
+#ifdef USE_LOLI_PROFILER
+#ifdef __cplusplus
+extern "C" {
+#endif // __cplusplus
+
+ __attribute__((noinline, optnone)) void loli_alloc(void* ptr, size_t size) {
+ (void)ptr;
+ (void)size;
+ }
+ __attribute__((noinline, optnone)) void loli_free(void* ptr) {
+ (void)ptr;
+ }
+
+ void (*loli_alloc_ptr)(void*, size_t) = &loli_alloc;
+ void (*loli_free_ptr)(void*) = &loli_free;
+
+ __attribute__((visibility("default"), noinline, optnone)) void loli_set_allocandfree(void (*alloc_ptr)(void*, size_t), void (*free_ptr)(void*)) {
+ loli_alloc_ptr = alloc_ptr;
+ loli_free_ptr = free_ptr;
+ }
+
+#ifdef __cplusplus
+}
+#endif // __cplusplus
+#endif // USE_LOLI_PROFILER
+
PRAGMA_DISABLE_UNSAFE_TYPECAST_WARNINGS
Unreal Engine 4.24 使用 FMallocBinned 作为安卓平台默认内存分配器。
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
__attribute__((noinline, optnone)) void loli_alloc(void* ptr, size_t size) {
(void)ptr;
(void)size;
}
__attribute__((noinline, optnone)) void loli_free(void* ptr) {
(void)ptr;
}
void (*loli_alloc_ptr)(void*, size_t) = &loli_alloc;
void (*loli_free_ptr)(void*) = &loli_free;
__attribute__((visibility("default"), noinline, optnone)) void loli_set_allocandfree(void (*alloc_ptr)(void*, size_t), void (*free_ptr)(void*)) {
loli_alloc_ptr = alloc_ptr;
loli_free_ptr = free_ptr;
}
#ifdef __cplusplus
}
#endif // __cplusplus
loli_alloc_ptr 是分配内存时的记录接口,loli_free_ptr 则是释放内存时的接口。
如果你们的引擎、框架使用类似虚幻的这种内存池结构,也建议使用这些记录接口来记录数据。这种模式获取的数据更有参考价值也更精准。
void* FMallocBinned::Malloc(SIZE_T Size, uint32 Alignment) {
// ....
#ifdef USE_LOLI_PROFILER
loli_alloc_ptr(Free, Size); // <-- record allocation
#endif
MEM_TIME(MemTime += FPlatformTime::Seconds());
return Free;
}
void* FMallocBinned::Realloc(void* Ptr, SIZE_T NewSize, uint32 Alignment) {
// ...
#ifdef USE_LOLI_PROFILER
loli_free_ptr(Ptr); // <-- free old allocation
loli_alloc_ptr(NewPtr, NewSize); // <-- record new allocation
#endif
MEM_TIME(MemTime += FPlatformTime::Seconds());
return NewPtr;
}
void FMallocBinned::Free(void* Ptr) {
#ifdef USE_LOLI_PROFILER
loli_free_ptr(Ptr); // <-- record free allocation
#endif
Private::PushFreeLockless(*this, Ptr);
}
除非你的 Unreal Engine 版本较低,否则不建议使用 FMallocAnsi。
它直接使用系统的malloc(),有性能上的问题。
--- a/Engine/Source/Runtime/Core/Private/Android/AndroidPlatformMemory.cpp
+++ b/Engine/Source/Runtime/Core/Private/Android/AndroidPlatformMemory.cpp
@@ -293,7 +293,11 @@ FMalloc* FAndroidPlatformMemory::BaseAllocator()
FLowLevelMemTracker::Get().SetProgramSize(Stats.UsedPhysical);
#endif
-#if USE_MALLOC_BINNED3 && PLATFORM_ANDROID_ARM64
+#ifdef USE_LOLI_PROFILER
+ return new FMallocAnsi();
+#elif USE_MALLOC_BINNED3 && PLATFORM_ANDROID_ARM64
return new FMallocBinned3();
#elif USE_MALLOC_BINNED2
return new FMallocBinned2();
我们需要添加这个编译器选项: -fno-omit-frame-pointer
此选项会将堆栈函数指针存储到一个寄存器中,从而大大提升堆栈回溯性能,此方案是目前性能最优的方案。
--- a/Engine/Source/Programs/UnrealBuildTool/Configuration/TargetRules.cs
+++ b/Engine/Source/Programs/UnrealBuildTool/Configuration/TargetRules.cs
@@ -1043,6 +1043,12 @@ namespace UnrealBuildTool
[XmlConfigFile(Category = "BuildConfiguration")]
public bool bUseMallocProfiler = false;
+ /// <summary>
+ /// If true, then enable loli memory profiling in the build (defines USE_LOLI_PROFILER=1).
+ /// </summary>
+ [XmlConfigFile(Category = "BuildConfiguration")]
+ public bool bUseLoliProfiler = false;
@@ -2373,6 +2379,11 @@ namespace UnrealBuildTool
get { return Inner.bUseMallocProfiler; }
}
+ public bool bUseLoliProfiler
+ {
+ get { return Inner.bUseLoliProfiler; }
+ }
+
public bool bUseSharedPCHs
{
+++ b/Engine/Source/Programs/UnrealBuildTool/System/RulesAssembly.cs
@@ -572,6 +572,13 @@ namespace UnrealBuildTool
UEBuildPlatform Platform = UEBuildPlatform.GetBuildPlatform(Rules.Platform);
Platform.ValidateTarget(Rules);
+ // Setup the loli profiler
+ if (Platform.Platform == UnrealTargetPlatform.Android && Rules.bUseLoliProfiler)
+ {
+ Rules.GlobalDefinitions.Add("USE_LOLI_PROFILER=1");
+ Rules.AdditionalCompilerArguments += " -fno-omit-frame-pointer";
+ }
当你合入这些修改后,可打开构建系统的开关来构建一个LoliProfiler专用版APK。
打开BuildConfiguration.xml中的bUseLoliProfiler选项:
<?xml version="1.0" encoding="utf-8" ?>
<Configuration xmlns="https://www.unrealengine.com/BuildConfiguration">
<BuildConfiguration>
<bUseLoliProfiler>true</bUseLoliProfiler>
</BuildConfiguration>
</Configuration>
此配置可放置在以下任意文件夹中:
# create if not exist
Engine/Saved/UnrealBuildTool/BuildConfiguration.xml
User Folder/AppData/Roaming/Unreal Engine/UnrealBuildTool/BuildConfiguration.xml
My Documents/Unreal Engine/UnrealBuildTool/BuildConfiguration.xml
可使用 readelf 工具检测你打的包是否正确:
$ readelf -S file.so | grep "gnu_debugdata\|eh_frame\|debug_frame"
[12] .eh_frame_hdr PROGBITS 000000000000c2b0 0000c2b0
[13] .eh_frame PROGBITS 0000000000011000 00011000
[24] .gnu_debugdata PROGBITS 0000000000000000 000f7292
如果包含如下section,证明此编译选项已生效:
.gnu_debugdata
.eh_frame (+ preferably .eh_frame_hdr)
.debug_frame.
当你的编译器比较老,或使用32位架构时,可使用此函数插桩优化方案。
此编译器选项会在编译期为所有函数的头和尾插入一个函数调用,因此我们可以利用此接口实现自己的堆栈回溯方案,此方案比默认的 libunwind.so 要快很多。
第一步与上一章一样,先修改 FMallocBinned:
打开编译器选项 -finstrument-functions-after-inlining。编译器会在 inline 优化后,为所有函数插入我们自定义的函数调用。
--- a/Engine/Source/Programs/UnrealBuildTool/Configuration/TargetRules.cs
+++ b/Engine/Source/Programs/UnrealBuildTool/Configuration/TargetRules.cs
@@ -1043,6 +1043,12 @@ namespace UnrealBuildTool
[XmlConfigFile(Category = "BuildConfiguration")]
public bool bUseMallocProfiler = false;
+ /// <summary>
+ /// If true, then enable loli memory profiling in the build (defines USE_LOLI_PROFILER=1).
+ /// </summary>
+ [XmlConfigFile(Category = "BuildConfiguration")]
+ public bool bUseLoliProfiler = false;
@@ -2373,6 +2379,11 @@ namespace UnrealBuildTool
get { return Inner.bUseMallocProfiler; }
}
+ public bool bUseLoliProfiler
+ {
+ get { return Inner.bUseLoliProfiler; }
+ }
+
public bool bUseSharedPCHs
{
+++ b/Engine/Source/Programs/UnrealBuildTool/System/RulesAssembly.cs
@@ -572,6 +572,13 @@ namespace UnrealBuildTool
UEBuildPlatform Platform = UEBuildPlatform.GetBuildPlatform(Rules.Platform);
Platform.ValidateTarget(Rules);
+ // Setup the loli profiler
+ if (Platform.Platform == UnrealTargetPlatform.Android && Rules.bUseLoliProfiler)
+ {
+ Rules.GlobalDefinitions.Add("USE_LOLI_PROFILER=1");
+ Rules.AdditionalCompilerArguments += " -finstrument-functions-after-inlining";
+ }
当你使用的clang是7.0.0以下版本,或你使用的是gcc时,可尝试 -finstrument-functions选项。
此选项会导致比 after-inline 模式高很多的代码膨胀的副作用
接下来我们实现自己的堆栈回溯函数:
新建 AndroidFunctionStub.cpp 在 Engine\Source\Runtime\Core\Private\Android 文件夹中:
#include <pthread.h>
#include <android/log.h>
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
#define MAX_TRACE_DEEP 128
typedef struct {
void* stack[MAX_TRACE_DEEP];
int current = MAX_TRACE_DEEP - 1;
} thread_stack_t;
static pthread_once_t sBackTraceOnce = PTHREAD_ONCE_INIT;
static pthread_key_t sBackTraceKey;
void (*loli_ignore)(bool) = nullptr;
void __attribute__((no_instrument_function)) destructor(void* ptr) {
if (ptr) {
free(ptr);
ptr = nullptr;
}
}
void __attribute__((no_instrument_function)) init_once(void) {
pthread_key_create(&sBackTraceKey, destructor);
}
thread_stack_t* __attribute__((no_instrument_function)) get_backtrace_info() {
thread_stack_t* ptr = (thread_stack_t*)pthread_getspecific(sBackTraceKey);
if (ptr) {
return ptr;
}
ptr = (thread_stack_t*)malloc(sizeof(thread_stack_t));
ptr->current = MAX_TRACE_DEEP - 1;
pthread_setspecific(sBackTraceKey, ptr);
return ptr;
}
void __attribute__((no_instrument_function)) __cyg_profile_func_enter(void* this_func, void* call_site) {
(void)call_site;
if (loli_ignore)
loli_ignore(true);
pthread_once(&sBackTraceOnce, init_once);
thread_stack_t* ptr = get_backtrace_info();
if (ptr->current > 0) {
ptr->stack[ptr->current--] = this_func;
}
if (loli_ignore)
loli_ignore(false);
//__android_log_print(ANDROID_LOG_INFO, "TEMP", "__cyg_profile_func_enter call");
}
void __attribute__((no_instrument_function)) __cyg_profile_func_exit(void* this_func, void* call_site) {
(void)this_func;
(void)call_site;
if (loli_ignore)
loli_ignore(true);
pthread_once(&sBackTraceOnce, init_once);
thread_stack_t* ptr = get_backtrace_info();
if (++ptr->current >= MAX_TRACE_DEEP) {
ptr->current = MAX_TRACE_DEEP - 1;
}
if (loli_ignore)
loli_ignore(false);
//__android_log_print(ANDROID_LOG_INFO, "TEMP", "__cyg_profile_func_exit call");
}
__attribute__((visibility("default"))) __attribute__((no_instrument_function)) int get_stack_backtrace(void** backtrace, int max)
{
if (loli_ignore)
loli_ignore(true);
pthread_once(&sBackTraceOnce, init_once);
thread_stack_t* ptr = get_backtrace_info();
int count = max;
if (MAX_TRACE_DEEP - 1 - ptr->current < count)
{
count = MAX_TRACE_DEEP - 1 - ptr->current;
}
if (count > 0)
{
memcpy(backtrace, &ptr->stack[ptr->current + 1], sizeof(void*) * (count));
}
if (loli_ignore)
loli_ignore(false);
return count;
}
__attribute__((visibility("default"))) __attribute__((no_instrument_function)) void set_loli_ignore_func(void (*funcPtr)(bool)) {
loli_ignore = funcPtr;
}
#ifdef __cplusplus
}
#endif // __cplusplus
测试版本: Unity5.6.6f2、Unity2017.4.35f1、Unity2018.4.5f1
需要在UnityPlayerActivity.java中添加一个启动参数:
protected String updateUnityCommandLineArguments(String cmdLine) {
return "-systemallocator";
}
此版本的Unity使用新的Bee构建系统,当你的目标架构是arm64-v8a时,可尝试按下面的代码修改:
// Tools/Bee/Bee.Toolchain.Android/AndroidNdkCompiler.cs
// if (Optimization != OptimizationLevel.None)
// {
// // important for performance. Frame pointer is only useful for profiling, but
// // introduces additional instructions into the prologue and epilogue of each function
// // and leaves one less usable register.
// yield return "-fomit-frame-pointer";
// }
yield return "-fno-omit-frame-pointer"; // <- 打开Framepointer选项
对于 armeabi-v7a 架构,你只能使用 -finstrument-functions 模式。
可使用 -finstrument-functions 模式来加速堆栈回溯。
此方案单线程下比 libunwind 快 10倍,多线程下快 50倍。
缺点是其会导致包体膨胀,并增加编译时间。
PlatformDependent/AndroidPlayer/Jam/Android_NDK.jam.cs
// 2017
Vars.Android_CFLAGS_debug.Assign(
"-O0",
"-D_DEBUG",
"-fno-omit-frame-pointer",
"-finstrument-functions", // <-- add
"-fno-strict-aliasing");
Vars.Android_CFLAGS_release.Assign(
"-Os",
"-DNDEBUG",
"-fomit-frame-pointer",
"-finstrument-functions", // <-- add
"-fstrict-aliasing");
// 5.6
// ######## Common flags for all toolchains ########
Android.CFLAGS.release = ... -finstrument-functions ;
Android.CFLAGS.debug = ... -finstrument-functions ;
Unity 2017 使用 NDK r13b,不支持 -finstrument-functions-after-inline。如果你的NDK是clang 7.0.0及以上则可使用 after-inline 优化。此选项比不inline更快,包体膨胀也小很多。
我们需要把桩函数的定义放到一个所有代码都能看到的地方。
这对于虚幻引擎来说不是问题,因为它使用了Unity Build系统。
在此文件夹下选择正确的架构:
然后把这些代码放到架构文件夹下的 /usr/include 目录下的 jni.h 文件中:
#ifndef JNI_H_
#define JNI_H_
#include <sys/cdefs.h>
#include <stdarg.h>
extern void __attribute__((no_instrument_function)) __cyg_profile_func_enter(void* this_func, void* call_site);
extern void __attribute__((no_instrument_function)) __cyg_profile_func_exit(void* this_func, void* call_site);
接下来我们就可以实现此函数:
#include <pthread.h>
#include <android/log.h>
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
#define MAX_TRACE_DEEP 128
typedef struct {
void* stack[MAX_TRACE_DEEP];
int current = MAX_TRACE_DEEP - 1;
} thread_stack_t;
static pthread_once_t sBackTraceOnce = PTHREAD_ONCE_INIT;
static pthread_key_t sBackTraceKey;
void (*loli_ignore)(bool) = nullptr;
void __attribute__((no_instrument_function)) destructor(void* ptr) {
if (ptr) {
free(ptr);
ptr = nullptr;
}
}
void __attribute__((no_instrument_function)) init_once(void) {
pthread_key_create(&sBackTraceKey, destructor);
}
thread_stack_t* __attribute__((no_instrument_function)) get_backtrace_info() {
thread_stack_t* ptr = (thread_stack_t*)pthread_getspecific(sBackTraceKey);
if (ptr) {
return ptr;
}
ptr = (thread_stack_t*)malloc(sizeof(thread_stack_t));
ptr->current = MAX_TRACE_DEEP - 1;
pthread_setspecific(sBackTraceKey, ptr);
return ptr;
}
void __attribute__((no_instrument_function)) __cyg_profile_func_enter(void* this_func, void* call_site) {
(void)call_site;
if (loli_ignore)
loli_ignore(true);
pthread_once(&sBackTraceOnce, init_once);
thread_stack_t* ptr = get_backtrace_info();
if (ptr->current > 0) {
ptr->stack[ptr->current--] = this_func;
}
if (loli_ignore)
loli_ignore(false);
//__android_log_print(ANDROID_LOG_INFO, "TEMP", "__cyg_profile_func_enter call");
}
void __attribute__((no_instrument_function)) __cyg_profile_func_exit(void* this_func, void* call_site) {
(void)this_func;
(void)call_site;
if (loli_ignore)
loli_ignore(true);
pthread_once(&sBackTraceOnce, init_once);
thread_stack_t* ptr = get_backtrace_info();
if (++ptr->current >= MAX_TRACE_DEEP) {
ptr->current = MAX_TRACE_DEEP - 1;
}
if (loli_ignore)
loli_ignore(false);
//__android_log_print(ANDROID_LOG_INFO, "TEMP", "__cyg_profile_func_exit call");
}
__attribute__((visibility("default"))) __attribute__((no_instrument_function)) int get_stack_backtrace(void** backtrace, int max)
{
if (loli_ignore)
loli_ignore(true);
pthread_once(&sBackTraceOnce, init_once);
thread_stack_t* ptr = get_backtrace_info();
int count = max;
if (MAX_TRACE_DEEP - 1 - ptr->current < count)
{
count = MAX_TRACE_DEEP - 1 - ptr->current;
}
if (count > 0)
{
memcpy(backtrace, &ptr->stack[ptr->current + 1], sizeof(void*) * (count));
}
if (loli_ignore)
loli_ignore(false);
return count;
}
__attribute__((visibility("default"))) __attribute__((no_instrument_function)) void set_loli_ignore_func(void (*funcPtr)(bool)) {
loli_ignore = funcPtr;
}
#ifdef __cplusplus
}
#endif // __cplusplus
将函数实现代码重命名并放在下面的文件夹中:
PlatformDependent/AndroidPlayer/Source/main/AndroidLoli.cpp
PlatformDependent/AndroidPlayer/Source/AndroidLoli.cpp
至此你的 libunity&libmain 代码库就会包含我们的插桩优化了。
使用 perl build.pl 打出正确的 libunity.so。
记得把Engine code stripping选项关掉:
可使用 IDA 或 NDK中的工具来检查我们的函数是否正确导出:
使用 adb logcat -s Loli 在运行时检查桩函数是否被正确使用:
set_loli_ignore_func found at 0xc4c8a368
get_stack_backtrace found at 0xc4c8a238