这篇博客介绍了在某些情况下,UE4中IndexToObject()
函数中空指针带来的性能消耗分析以及可以采取的优化项。
本博客翻译自Robert Troughton的博客UE4: The Sadness of the Ignored Null Pointer – Coconut Lizard,传送门,翻译工作已征得原作者同意。
This post is translated from English. You can find the original English language version here: http://coconutlizard.co.uk/blog/ue4/the-ignored-null/
看下面的代码(一如既往的,我注意到这一块的问题是因为IsPendingKill()
函数一度在我们项目的性能分析中占了一块比较醒目的位置)…看看你能发现什么可疑的地方吗?
FORCEINLINE bool IsPendingKill() const
{
return GUObjectArray.IndexToObject(InternalIndex)->IsPendingKill();
}
然后我们来看一看上面代码中使用到的IndexToObject()
函数:
FORCEINLINE FUObjectItem* IndexToObject(int32 Index)
{
check(Index >= 0);
if (Index < ObjObjects.Num())
{
return const_cast(&ObjObjects[Index]);
}
return nullptr;
}
看到这里读者应该可以找到问题了。
没错,如果变量Index
太大,则整个函数将返回空指针nullptr
。但是函数IsPendingKill
并未对空指针进行处理,而是直接调用了FUObjectItem::IsPendingKill()
函数,容易引起access violation的问题。可以从对应的汇编代码来进行判断:
0x1413738e6 movsxd rax, dword ptr [rbx+0xc]
0x1413738ea cmp eax, dword ptr [rip+0x19c2334]
0x1413738f0 jnl 0x1413738ff
0x1413738f2 shl rax, 0x4
0x1413738f6 add rax, qword ptr [rip+0x19c231b]
0x1413738fd jmp 0x141373901
0x1413738ff xor eax, eax
0x141373901 mov eax, dword ptr [rax+0x8]
0x141373904 shr eax, 0x1d
0x141373907 test al, 0x1
以上的代码大致内容为:
if (Index < ObjObjects.Num())
函数,如果变量Index
太大,则跳转到第7行。FUobjectItem
指针。xor
操作相对于直接的赋值操作来说效率更高)。IsPendingKill
函数的值。综合上面的分析,我们可以得到以下两个猜想中有一个是真的:
IsPendingKill()
函数里有bug… 这个函数本应该检查空指针但是没有。Index
变量永远不会太大 - 所以我们或许可以针对这个进行一些优化?到目前为止(2016/07/15)来说,这一段代码已经超过15个月没有进行修改了,而且就我来说也没看到过因为这段代码而引起的崩溃。因此我便假设上面的第二个猜想是真的。而且我认为就算我们针对于这个做了改动,应该也不会比以前更差(如果变量Index
真的太大的话按照以前的逻辑可是会引起崩溃的)。
在我们进行处理的时候,我找到了以下的代码,它的功能和IndexToObject()
类似:
FORCEINLINE FUObjectItem* IndexToObjectUnsafeForGC(int32 Index)
{
return const_cast(&ObjObjects[Index]);
}
这个函数正是我们想要的,它把变量Index
的测试逻辑删掉了,因此我们可以直接将IsPendingKill()
函数替换为这个:
FORCEINLINE bool IsPendingKill() const
{
return GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->IsPendingKill();
}
同样的,我们也可以在其他的几个函数中做同样的处理…这些函数都在Engine\Source\Runtime\CoreUObject\Public\UObject\UObjectBaseUtility.h
路径中:
FORCEINLINE void MarkPendingKill()
{
check(!IsRooted());
GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->SetPendingKill();
}
FORCEINLINE void ClearPendingKill()
{
GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->ClearPendingKill();
}
FORCEINLINE void AddToRoot()
{
GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->SetRootSet();
}
FORCEINLINE void RemoveFromRoot()
{
GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->ClearRootSet();
}
FORCEINLINE bool IsRooted()
{
return GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->IsRootSet();
}
FORCEINLINE bool ThisThreadAtomicallyClearedRFUnreachable()
{
return GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->ThisThreadAtomicallyClearedRFUnreachable();
}
FORCEINLINE bool IsUnreachable() const
{
return GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->IsUnreachable();
}
FORCEINLINE bool IsPendingKillOrUnreachable() const
{
return GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->HasAnyFlags(EInternalObjectFlags::PendingKill | EInternalObjectFlags::Unreachable);
}
FORCEINLINE bool IsNative() const
{
return GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->HasAnyFlags(EInternalObjectFlags::Native);
}
FORCEINLINE void SetInternalFlags(EInternalObjectFlags FlagsToSet) const
{
GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->SetFlags(FlagsToSet);
}
FORCEINLINE EInternalObjectFlags GetInternalFlags() const
{
return GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->GetFlags();
}
FORCEINLINE bool HasAnyInternalFlags(EInternalObjectFlags FlagsToCheck) const
{
return GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->HasAnyFlags(FlagsToCheck);
}
FORCEINLINE void ClearInternalFlags(EInternalObjectFlags FlagsToClear) const
{
GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->ClearFlags(FlagsToClear);
}
FORCEINLINE bool AtomicallyClearInternalFlags(EInternalObjectFlags FlagsToClear) const
{
return GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->ThisThreadAtomicallyClearedFlag(FlagsToClear);
}
好吧抱歉,貌似代码太多了……
此外,在Engine\Source\Runtime\CoreUObject\Private\UObject\UObjectBase.cpp
中,还有两处可以进行修改的。第一处在UObjectBase::DeferredRegister()
函数后面:
check(!GUObjectArray.IsDisregardForGC(this) || GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->IsRootSet());
还有就是在UObjectBase::AddObject()
函数中:
if (InternalFlagsToSet != EInternalObjectFlags::None)
{
GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->SetFlags(InternalFlagsToSet);
}
这些大概就是全部了,最终的汇编代码如下:
00007FF6A79A28F6 movsxd rax,dword ptr [rbp+0Ch]
00007FF6A79A28FA mov rdx,qword ptr [GUObjectArray+10h (07FF6A9684288h)]
00007FF6A79A2901 add rax,rax
00007FF6A79A2904 mov ecx,dword ptr [rdx+rax*8+8]
00007FF6A79A2908 shr ecx,1Dh
00007FF6A79A290B test cl,1
我们将其降到了6行汇编代码,而且去掉了分支判断。这无疑是一个很大的改进(译者按:IsPendingKill函数在游戏中会被很频繁的调用,因此这个改进很可能比起读者直观的感受更大)。
正如我上面所说的,我并不百分百确定这个改动是“正确的”。如果能够从Epic那里得到一些官方的回复就更好了。但是如果这个改动真的会导致bug,那么应该也是之前的代码的问题……不过这种可能也不大,因为到现在为止也没出现过这段代码所导致的bug。
但是如论如何,这次处理过后我们能看到在IsPendingKill
函数的性能有了很大的提升,当然也包括了其他的函数……这非常酷,不是吗?
<全文完>