UnrealEngine4被忽略空指针的忧伤
来源:
纳金网 |
责任编辑:传说的落叶 |
发布时间: 2019-06-13 08:35 | 浏览量:
这篇文章介绍了在某些情况下,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条
推荐
热门