- 서론
유니티의 null 체크는 무겁다고들 한다
왜일까? 구현을 보자
public static bool operator ==(Object x, Object y) => Object.CompareBaseObjects(x, y);
private static bool CompareBaseObjects(Object lhs, Object rhs) { bool flag1 = (object) lhs == null; bool flag2 = (object) rhs == null; if (flag2 & flag1) return true; if (flag2) return !Object.IsNativeObjectAlive(lhs); return flag1 ? !Object.IsNativeObjectAlive(rhs) : lhs.m_InstanceID == rhs.m_InstanceID; }
임의의 UnityEngine.Object인 obj을 obj == null와 같이 검사하면 다음과 같이 작동할것이다
private static bool CompareBaseObjects(Object lhs, Object _) { if (lhs is null) return true; return !Object.IsNativeObjectAlive(lhs); }
그냥 사용하면, 추가적인 캐스팅비용이 발생할것만 같다(아마도?)
그러니, null check와 함께 Object.IsNativeObjectAlive를 직접 호출해보자
* Object.IsNativeObjectAlive의 코드는 아래와 같다
private static bool IsNativeObjectAlive(Object o) { if (o.GetCachedPtr() != IntPtr.Zero) return true; return !(o is MonoBehaviour) && !(o is ScriptableObject) && Object.DoesObjectWithInstanceIDExist(o.GetInstanceID()); }
- 테스트 코드
최종적으로 코드는 다음과 같을 것이다
public static partial class ObjectUtility { private delegate bool ObjectAliveChecker(Object obj); private static ObjectAliveChecker _checker; private static ObjectAliveChecker Checker => _checker ??= typeof(Object) .GetMethod("IsNativeObjectAlive", BindingFlags.Static | BindingFlags.NonPublic) ?.CreateDelegate(typeof(ObjectAliveChecker)) is ObjectAliveChecker method ? method : obj => obj == null; [method: MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool IsNativeObjectAlive(this Object obj) => obj is not null && Checker(obj); }
public class NullCheckPerformanceTester : MonoBehaviour { public Object obj; [method: MethodImpl(MethodImplOptions.AggressiveInlining)] private bool SafeCompare() => obj == null; [method: MethodImpl(MethodImplOptions.AggressiveInlining)] private bool UnsafeCompare() => obj.IsNativeObjectAlive(); private void Benchmark(Func<bool> act) { var watch = new Stopwatch(); watch.Start(); for (var i = 0; i < 10000000; ++i) _ = act(); watch.Stop(); Debug.Log(watch.ElapsedMilliseconds); watch.Reset(); } public void Start() { Debug.Log("for jit"); Benchmark(SafeCompare); Benchmark(UnsafeCompare); Debug.Log("test"); Benchmark(SafeCompare); Benchmark(UnsafeCompare); } }
(유니티엔진이 드디어 일부 C#9.0을 지원한다; 사용중인 버전은 2021.2.0b2)
obj에 null값을 넣어 테스트 해보자
- 결과
Debug모드 시 리플렉션을 이용한 호출이 조금 빠른(약 0.87배) 모습을 볼수 있다
Release모드에서는 정말 아주 조금 빠른(약 0.95배) 모습을 볼수 있다
- 오늘의 교훈
그냥 주는대로 쓰자