UnrealEngine4被忽略空指针的忧伤 - 纳金网
联系我们

给我们留言

联系我们

地址:福建省晋江市青阳街道洪山路国际工业设计园纳金网

邮箱:info@narkii.com

电话:0595-82682267

(周一到周五, 周六周日休息)

当前位置:主页 > 3D教程 > 图文教程

UnrealEngine4被忽略空指针的忧伤

来源: 纳金网 | 责任编辑:传说的落叶 | 发布时间: 2019-06-13 08:35 | 浏览量:

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()函数一度在我们项目的性能分析中占了一块比较醒目的位置)…看看你能发现什么可疑的地方吗?
 

[代码]:

1 FORCEINLINE bool IsPendingKill() const
2 {
3     return GUObjectArray.IndexToObject(InternalIndex)->IsPendingKill();
4 }
 
然后我们来看一看上面代码中使用到的IndexToObject()函数:
 

[代码]:

1 FORCEINLINE FUObjectItem* IndexToObject(int32 Index)
2 {
3     check(Index >= 0);
4     if (Index < ObjObjects.Num())
5     {
6         return const_cast<fuobjectitem*>(&ObjObjects[Index]);
7     }
8     return nullptr;
9 }</fuobjectitem*>
 
看到这里读者应该可以找到问题了。
 
代码分析
 
没错,如果变量Index太大,则整个函数将返回空指针nullptr。但是函数IsPendingKill并未对空指针进行处理,而是直接调用了FUObjectItem::IsPendingKill()函数,容易引起access violation的问题。可以从对应的汇编代码来进行判断:
 

[代码]:

01 0x1413738e6 movsxd rax, dword ptr [rbx+0xc]
02 0x1413738ea cmp eax, dword ptr [rip+0x19c2334]
03 0x1413738f0 jnl 0x1413738ff
04 0x1413738f2 shl rax, 0x4
05 0x1413738f6 add rax, qword ptr [rip+0x19c231b]
06 0x1413738fd jmp 0x141373901
07 0x1413738ff xor eax, eax
08 0x141373901 mov eax, dword ptr [rax+0x8]
09 0x141373904 shr eax, 0x1d
10 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()类似:
 

[代码]:

1 FORCEINLINE FUObjectItem* IndexToObjectUnsafeForGC(int32 Index)
2 {
3     return const_cast<fuobjectitem*>(&ObjObjects[Index]);
4 }</fuobjectitem*>
 
这个函数正是我们想要的,它把变量Index的测试逻辑删掉了,因此我们可以直接将IsPendingKill()函数替换为这个:
 

[代码]:

1 FORCEINLINE bool IsPendingKill() const
2 {
3     return GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->IsPendingKill();
4 }
 
同样的,我们也可以在其他的几个函数中做同样的处理…这些函数都在Engine\Source\Runtime\CoreUObject\Public\UObject\UObjectBaseUtility.h路径中:
 

[代码]:

1 FORCEINLINE void MarkPendingKill()
2 {
3   check(!IsRooted());
4   GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->SetPendingKill();
5 }
 

[代码]:

1 FORCEINLINE void ClearPendingKill()
2 {
3   GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->ClearPendingKill();
4 }
 

[代码]:

1 FORCEINLINE void AddToRoot()
2 {
3   GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->SetRootSet();
4 }
 

[代码]:

1 FORCEINLINE void RemoveFromRoot()
2 {
3   GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->ClearRootSet();
4 }
 

[代码]:

1 FORCEINLINE bool IsRooted()
2 {
3   return GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->IsRootSet();
4 }
 

[代码]:

1 FORCEINLINE bool ThisThreadAtomicallyClearedRFUnreachable()
2 {
3   return GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->ThisThreadAtomicallyClearedRFUnreachable();
4 }
 

[代码]:

1 FORCEINLINE bool IsUnreachable() const
2 {
3   return GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->IsUnreachable();
4 }
 

[代码]:

1 FORCEINLINE bool IsPendingKillOrUnreachable() const
2 {
3   return GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->HasAnyFlags(EInternalObjectFlags::PendingKill | EInternalObjectFlags::Unreachable);
4 }
 
 

[代码]:

1 FORCEINLINE bool IsNative() const
2  {
3    return GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->HasAnyFlags(EInternalObjectFlags::Native);
4  }

 

[代码]:

1 FORCEINLINE void SetInternalFlags(EInternalObjectFlags FlagsToSet) const
2 {
3   GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->SetFlags(FlagsToSet);
4 }
 
   

[代码]:

1 FORCEINLINE EInternalObjectFlags GetInternalFlags() const
2    {
3      return GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->GetFlags();
4    }

 
  

[代码]:

1 FORCEINLINE bool HasAnyInternalFlags(EInternalObjectFlags FlagsToCheck) const
2   {
3     return GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->HasAnyFlags(FlagsToCheck);
4   }

 
 

[代码]:

1 FORCEINLINE void ClearInternalFlags(EInternalObjectFlags FlagsToClear) const
2  {
3    GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->ClearFlags(FlagsToClear);
4  }

 
 

[代码]:

1 FORCEINLINE bool AtomicallyClearInternalFlags(EInternalObjectFlags FlagsToClear) const
2  {
3    return GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->ThisThreadAtomicallyClearedFlag(FlagsToClear);
4  }

 
好吧抱歉,貌似代码太多了……
 
此外,在Engine\Source\Runtime\CoreUObject\Private\UObject\UObjectBase.cpp中,还有两处可以进行修改的。第一处在UObjectBase::DeferredRegister()函数后面:
 

[代码]:

1 check(!GUObjectArray.IsDisregardForGC(this) || GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->IsRootSet());
 
还有就是在UObjectBase::AddObject()函数中:
 

[代码]:

1 if (InternalFlagsToSet != EInternalObjectFlags::None)
2 {
3     GUObjectArray.IndexToObjectUnsafeForGC(InternalIndex)->SetFlags(InternalFlagsToSet);
4 }
 
这些大概就是全部了,最终的汇编代码如下:
 

[代码]:

1 00007FF6A79A28F6  movsxd      rax,dword ptr [rbp+0Ch] 
2 00007FF6A79A28FA  mov         rdx,qword ptr [GUObjectArray+10h (07FF6A9684288h)] 
3 00007FF6A79A2901  add         rax,rax 
4 00007FF6A79A2904  mov         ecx,dword ptr [rdx+rax*8+8] 
5 00007FF6A79A2908  shr         ecx,1Dh 
6 00007FF6A79A290B  test        cl,1
 
我们将其降到了6行汇编代码,而且去掉了分支判断。这无疑是一个很大的改进(译者按:IsPendingKill函数在游戏中会被很频繁的调用,因此这个改进很可能比起读者直观的感受更大)。
 
后记
 
正如我上面所说的,我并不百分百确定这个改动是“正确的”。如果能够从Epic那里得到一些官方的回复就更好了。但是如果这个改动真的会导致bug,那么应该也是之前的代码的问题……不过这种可能也不大,因为到现在为止也没出现过这段代码所导致的bug。
 
但是如论如何,这次处理过后我们能看到在IsPendingKill函数的性能有了很大的提升,当然也包括了其他的函数……这非常酷,不是吗?

相关文章
网友评论

您需要登录后才可以发帖 登录 | 立即注册

关闭

全部评论:0条

推荐
热门