글 작성자: Doublsb

Null 체크로 해당 객체가 실제로 존재하는지 확인하는 일은 매우 자주 발생하곤 한다.

그런데 최근 IDE를 VS에서 Rider로 갈아탔더니 null 체크를 할 때마다 이것저것 알려주었고, 충격적인 사실을 여러가지 알게 되었다.

 

UnityEngine.Objects

UnityEngine.Object는 유니티 엔진에서 관리하는 오브젝트이다. 그리고 유니티는 실제로는 C++로 작성되어 있다.

그리고 C++은 가비지 콜렉터가 존재하지 않는 언어이다. 즉, 언어가 아닌 유니티가 가비지 컬렉터를 관리하고 있다는 뜻이다.

UnityObjects를 Destory를 한 뒤 null 체크를 했을 때, 로그에서는 null이 맞다고 뜨니 의심을 품지 않게 되기 마련이다. 그러나 실제로는 null로 객체를 초기화해주지 않았기 때문에, 가비지 컬렉팅이 일어나기 전까지 메모리에 남아 있게 된다.

그래서 유니티 오브젝트를 UnityEngine의 Object와 .net의 Object로 받아 null check를 해보면 다음과 같은 현상이 일어난다.

var obj = new GameObject();
yield return null;

Destroy(obj);
yield return null;

print($".Net Object : {(obj as System.Object) == null}");
print($"Unity Object : {(obj as UnityEngine.Object) == null}");


//결과
//.Net Object : False
//Unity Object : True

Unity의 Object가 ==, != 연산자를 오버로딩하여 네이티브 객체가 사라졌다면 null이라는 정보를 전달하고 있는 것이다.

이렇게 실제 메모리와 엔진의 판단이 다른 경우를 fake null이라고 한다.

 

Unity Objects를 null check 할 때의 퍼포먼스 이슈

위에서 ==, != 연산자를 오버로딩하여, 네이티브 객체가 사라졌는지 유무를 판단하여 전달한다고 했다.

그리고 이는 자연스럽게 코드 실행을 느리게 만든다. 이를 피하기 위해 ReferenceEquals로 비교하면 연산이 굉장히 빨라진다.

 

private int count = 100000000;

    void Start()
    {
        var tf = transform;
        var stopwatch = new Stopwatch();

        // Do == null
        stopwatch.Start();
        NullCheck_NotNull(tf);
        stopwatch.Stop();
        print($"== : {stopwatch.ElapsedMilliseconds}");

        // Reset StopWatch
        stopwatch.Reset();

        // Do ReferenceEquals
        stopwatch.Start();
        NullCheck_ReferenceEquals(tf);
        stopwatch.Stop();
        print($"referenceEquals : {stopwatch.ElapsedMilliseconds}");
    }

    private void NullCheck_NotNull(Object obj)
    {
        for (int i = 0; i < count; i++)
        {
            if (obj == null) { }
        }
    }

    private void NullCheck_ReferenceEquals(object obj)
    {
        for (int i = 0; i < count; i++)
        {
            if (ReferenceEquals(obj, null) == true) { }
        }
    }

 

30배 빨라졌다

1억 번 실행한 결과, ReferenceEquals가 30배정도 빠름을 알 수 있다.

 

마냥 NullCheck를 위해 ReferenceEquals를 쓰는 건 안 된다

그러나, 성능 개선을 위해 ReferenceEquals를 남발하는 것은 코드를 꼼꼼히 체크한 뒤에 해야 한다.

public이나 serializeField로 선언한 UnityObject들은 아무것도 넣어놓지 않으면 Fake Null 상태가 되기 때문이다.

 

public           Camera cam_public;
[SerializeField] Camera cam_serialize;
private          Camera cam_private;

private void Start()
{
	print($"cam_public is {ReferenceEquals(cam_public, null)}");
	print($"cam_serialize is {ReferenceEquals(cam_serialize, null)}");
	print($"cam_private is {ReferenceEquals(cam_private, null)}");
}

inspector에 보여질 수 있는 값은 Fake Null이다

이 점을 주의하고 ReferenceEquals를 사용한다면, 성능 개선에 도움이 될 것이다.

반응형