FishPlayer

一个喜欢摸鱼的废物

0%

UnityObject 空引用检查小测试

Classes deriving from Unity. Object inherit equality operators that change the behaviour of the == and != operators.
While these operators will perform standard . NET reference equality, if comparing one side to null, these operators will call native code to check if the underlying native engine object is still alive.

这段解释来自Rider的提示:
https://github.com/JetBrains/resharper-unity/wiki/Avoid-null-comparisons-against-UnityEngine.Object-subclasses

UnityEngine 底层是C++写的,所以说,其实我们对UnityObject的空引用检查都会再深入到里面C++那一层去再检查,这个检查是比较消耗的。
之前曾经遇到因为使用了 ?. 语法糖而导致的 this 指针为null的奇妙bug,大概也是因为这个原因。

现在的公司是做手游的,大家对性能都比较看重,不过我也真的好奇这个检查到底有多消耗,决定跑一跑试试。

Test Case

因为是做手游,随意干脆就在手机上测试好了。
大概是这样的环境:
Unity 2019.4 LTS

移动设备这边是
Google Pixel Verzion(俺穷买美版)
Android 9 (已获取Root权限)

TestCase 这边其实我都是瞎弄的。

分别进行三种测试:
Unity Object 的空引用检查;
POCO Object 的空引用检查;
bool 的直接检查;(因为我觉得这是一个代替Unity Object空引用检查的一个方法

每一种检查都有检查到true和false的两种情况,并循环跑多次。

然后,上代码!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147

namespace TempCheckTest
{

public class TempObjA
{
private int m_tempNum = 56;
}

public class NullCheckTester : MonoBehaviour
{
// unity obj null check
private Transform m_emptyTransform = null;
private Transform m_selfTransform = null;

// poco obj null check
private TempObjA m_emptyObjA = null;
private TempObjA m_objA = null;

// bool check
private bool m_boolTrue = true;
private bool m_boolFalse = true;

[Header("test")]
public int m_checkTimes = 100000;

[Header("UI side")]
public UnityEngine.UI.Text m_outPutText = null;
public UnityEngine.UI.Button m_doUnityObjTestButton = null;
public UnityEngine.UI.Button m_doPocoObjTestButton = null;
public UnityEngine.UI.Button m_doBoolTestButton = null;

public UnityEngine.UI.InputField m_inputField = null;

[Button("init awake")]
public void InitTester()
{
m_emptyTransform = null;
m_selfTransform = this.transform;

m_emptyObjA = null;
m_objA = new TempObjA();

m_boolTrue = true;
m_boolFalse = false;

m_outPutText.text = string.Empty;

m_doUnityObjTestButton.onClick.RemoveAllListeners();
m_doPocoObjTestButton.onClick.RemoveAllListeners();
m_doBoolTestButton.onClick.RemoveAllListeners();

m_doUnityObjTestButton.onClick.AddListener(DoUnityObjNullCheckTest);
m_doPocoObjTestButton.onClick.AddListener(DoPOCOObjNullCheckTest);
m_doBoolTestButton.onClick.AddListener(DoBoolObjNullCheckTest);

m_inputField.onValueChanged.RemoveAllListeners();
m_inputField.onValueChanged.AddListener(OnInputNumberChanged);
m_checkTimes = 100000;
m_inputField.text = m_checkTimes.ToString();
}

[Button("do unity.object null check test")]
public void DoUnityObjNullCheckTest()
{
System.Diagnostics.Stopwatch watcher = new System.Diagnostics.Stopwatch();
watcher.Start();
for (int i = 0; i < m_checkTimes; i++)
{
if (null == m_emptyTransform) {}

if (null == m_selfTransform) {}
}
watcher.Stop();

string outPut = $"do unity.object null check test 2*{m_checkTimes} cost {watcher.ElapsedMilliseconds}ms";
m_outPutText.text = outPut;
Debug.Log(outPut);
}

[Button("do poco object null check test")]
public void DoPOCOObjNullCheckTest()
{
System.Diagnostics.Stopwatch watcher = new System.Diagnostics.Stopwatch();
watcher.Start();
for (int i = 0; i < m_checkTimes; i++)
{
if (null == m_emptyObjA) {}

if (null == m_objA) {}
}
watcher.Stop();

string outPut = $"do poco object null check test 2*{m_checkTimes} cost {watcher.ElapsedMilliseconds}ms";
m_outPutText.text = outPut;
Debug.Log(outPut);
}

[Button("do bool check test")]
public void DoBoolObjNullCheckTest()
{
System.Diagnostics.Stopwatch watcher = new System.Diagnostics.Stopwatch();
watcher.Start();
for (int i = 0; i < m_checkTimes; i++)
{
if (m_boolFalse) {}

if (m_boolTrue) {}
}
watcher.Stop();

string outPut = $"do bool check test 2*{m_checkTimes} cost {watcher.ElapsedMilliseconds}ms";
m_outPutText.text = outPut;
Debug.Log(outPut);
}

public void OnInputNumberChanged(string arg0)
{
int result = 0;
if (!Int32.TryParse(arg0, out result))
result = Int32.MaxValue;

m_checkTimes = result;
}

#region mono method

private void Reset()
{
InitTester();
}

private void Awake()
{
InitTester();
}

private void OnEnable()
{
this.enabled = false;
}

#endregion

}

}

Test Result

分别进行10w次和100w次测试看看结果。

10w次:
Unity Object null check 4~7ms
POCO Object null check 0ms
bool check 0ms

100w次:
Unity Object null check 45~60ms
POCO Object null check 3~4ms
bool check 2ms

当检测数量非常多的时候,Unity Object 空引用检查的性能消耗比其他两个大了很多很多。现在的项目里大部分东西其实都是POCO的。
似乎把UnityObject Destroy之后再手动把变量置空是个好习惯(