- 서론


유니티의 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배) 모습을 볼수 있다



- 오늘의 교훈


그냥 주는대로 쓰자