Unreal Engine 4 —— 被忽略空指针的忧伤

这篇博客介绍了在某些情况下,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

以上的代码大致内容为:

  • 第1~3行处理if (Index < ObjObjects.Num())函数,如果变量Index太大,则跳转到第7行。
  • 第4~5行处理FUobjectItem指针。
  • 第6行的目的是将跳过第7行直达第8行。
  • 第7行将eax设定为0(将一个寄存器对自己进行xor操作相对于直接的赋值操作来说效率更高)。
  • 第8~9行用于获得IsPendingKill函数的值。
  • 第10行进行test。

综合上面的分析,我们可以得到以下两个猜想中有一个是真的:

  • 要么函数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函数的性能有了很大的提升,当然也包括了其他的函数……这非常酷,不是吗?

<全文完>

你可能感兴趣的:(UE4)