Unity 분석

메모리 구조와 유니티 - GC에 대한 심층적 이해

지영7130 2023. 1. 5. 08:00

개발을 하다보면 "메모리가 낭비된다", "메모리 누수" 등 메모리를 효율적으로 사용해야 한다는 내용들이 많이 나온다. 그렇다면 어떻게 하면 우리가 게임을 만들 때 메모리를 아낄 수 있을까? 내가 쓴 코드에서 어느 부분이 저장될까? 유니티에서 C# 스크립트를 할 때 메모리가 어떻게 할당되고 사용되는지 알아보자.


위 사진은 컴퓨터 메모리 구조이다. 프로그래밍에 대해 배우다 보면 한두번쯤은 봤을 것이다. 메모리는 크게 코드, 데이터, 힙, 스택 영역으로 나뉘어 저장된다. 아래 예제를 통해 어느 부분이 어디에 저장되는지 알아보자.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


[System.Serializable]
public class VRMap
{
    public Transform vrTarget;
    public Transform rigTarget;
    public Vector3 trackingPositionOffset;
    public Vector3 trackingRotationOffset;

    public void Map()
    {
        rigTarget.position = vrTarget.TransformPoint(trackingPositionOffset);
        rigTarget.rotation = vrTarget.rotation * Quaternion.Euler(trackingRotationOffset);
    }
}

public class VRRig : MonoBehaviour
{
    public Transform vr;
    public Transform rig;

    public Transform headConstraint;
    public Vector3 headBodyOffset;

    private void Update()
    {
        Vector3 newPosi = headConstraint.position + headBodyOffset;
        transform.position = newPosi;
        transform.forward = Vector3.ProjectOnPlane(headConstraint.up, Vector3.up).normalized;

        VRMap head = new VRMap();
        head.vrTarget = vr;
        head.rigTarget = rig;

        head.Map();
    }
}


1. 코드

우리가 쓴 스크립트가 저장되는 곳이다. 컴퓨터는 이곳에 저장된 명령어들을 하나씩 가져가서 처리한다. 위 예제의 택스트들이 저장된다고 생각하면 된다.


2. 데이터

전역변수와 정적변수가 저장되는 곳이다. 이곳에 저장된 메모리들은 프로그램이 끝날때까지 없어지지 않고 계속 남아있는다. 따라서 전역변수와 정적변수를 필요 이상으로 많이 사용하게 되면 메모리 낭비가 될 수 있다. 위 예제에서 이부분이 데이터 영역에 값을 저장한다.

    public Transform vr;
    public Transform rig;

    public Transform headConstraint;
    public Vector3 headBodyOffset;



4. 스택 (설명을 위해 힙보다 먼저 설명하겠다.)

스택은 지역변수나 매개변수를 저장한다. 이때 저장된 데이터들은 코드블럭이 끝나면 소멸한다. 아래 코드중에 지역변수는 Vector3 newPosi 와 VRMap head 가 있다. 하지만 이 둘에는 차이점이 있다.

아래 코드에서 newPosi는 두개의 Vector3의 값을 더한 값을 지역변수에 저장을 했다. 이때는 스택에 Vector3 값이 저장된다. 하지만 head에는 new VRMap()로 새로운 객체를 생성하여 지역변수에 저장을 했다. 이 경우에는 스택에 head의 값이 저장되지 않는다. 왜냐하면 C#에서 new로 생성하는 모든 참조형 객체는 힙 영역에 저장되기 때문이다. 그렇다면 스택에는 어떤 값이 저장될까? 바로 new VRMap() 으로 만든 객체가 있는 주소값이 스택영역에 저장된다.

이렇게 스택 영역에 저장된 값들은 한 코드블럭이 끝나면 사라지게 된다. 따라서 newPosi에 저장된 Vector3 값과 head에 저장된 주소값은 Update문이 한번 끝날때 마다 소멸하고 Update문이 다시 시작할때 생성된다. 이 생성하고 소멸하는 과정들을 매 프레임마다 반복하게 된다.

    private void Update()
    {
        Vector3 newPosi = headConstraint.position + headBodyOffset;
        transform.position = newPosi;
        transform.forward = Vector3.ProjectOnPlane(headConstraint.up, Vector3.up).normalized;

        VRMap head = new VRMap();
        head.vrTarget = vr;
        head.rigTarget = rig;

        head.Map();
    }



3. 힙

힙은 프로그래머가 직접 메모리에 할당해주는 공간이다. 앞에서도 말했지만 C#에서 new로 생성하는 모든 참조형 객체는 이 힙 영역에 저장된다. 따라서 위 코드에서 new VRMap()으로 생성한 데이터는 힙 영역에 저장된다. 주의해야 할점은 힙 영역에 저장된 값들은 스택 영역에 저장된 값들과 다르게 코드블럭이 끝나도 사라지지 않는다. 그래서 위 코드대로라면 매 프레임 마다 힙에 new VRMap()으로 생성한 데이터들이 계속해서 쌓이게 된다.

하지만 우리의 램(메모리)은 한정되어 있다. 힙에 사용하지 않는 데이터들이 계속해서 남아있으면 메모리가 꽉 차서 더이상 새로운 값을 할당할 수 없다. C++에서는 프로그래머가 메모리를 직접 관리하기 때문에 이러한 현상이 일어나면 프로그램이 멈춰버린다. 이 현상을 "메모리 누수" 라고 부른다.

그렇지만 우리의 C#에는 GC라는 것이 있다. 가비지 컬랙션 이라고 부르는데 이것이 힙에 안쓰는 데이터들을 자동으로 제거해준다. 그렇기 때문에 위처럼 코드 작성을 해도 프로그램이 꺼지지 않고 돌아간다. 그렇다면 우리는 GC를 믿고 저대로 힙에 데이터가 쌓이는 것을 방치해도 될까? 안타깝게도 GC가 힙에 있는 데이터를 지울 때 매우 많은 성능을 잡아먹는다. 그렇기 때문에 이렇게 Update 문에서 계속해서 new VRMap()을 호출하여 힙에 데이터를 누적시키는 것은 성능에 극도로 나쁜 영향을 끼친다. 그렇기 때문에 매 프레임 마다 객체를 생성하는 것은 지양해야 한다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


[System.Serializable]
public class VRMap
{
    public Transform vrTarget;
    public Transform rigTarget;
    public Vector3 trackingPositionOffset;
    public Vector3 trackingRotationOffset;

    public void Map()
    {
        rigTarget.position = vrTarget.TransformPoint(trackingPositionOffset);
        rigTarget.rotation = vrTarget.rotation * Quaternion.Euler(trackingRotationOffset);
    }
}

public class VRRig : MonoBehaviour
{
    public Transform vr;
    public Transform rig;

    public Transform headConstraint;
    public Vector3 headBodyOffset;

    private VRMap head;

    private void Start()
    {
        head = new VRMap();

        head.vrTarget = vr;
        head.rigTarget = rig;
    }

    private void Update()
    {
        Vector3 newPosi = headConstraint.position + headBodyOffset;
        transform.position = newPosi;
        transform.forward = Vector3.ProjectOnPlane(headConstraint.up, Vector3.up).normalized;

        head.Map();
    }
}


코드를 고쳐보았다. 먼저 head를 전역변수에 선언하여 데이터 영역에 저장을 했다. 매 프레임마다 사용해야 하니 스택 영역에서 계속 생성하고 지우고 하는것보다 훨씬 괜찮을 것이다. 또 Start에서 head = new VRMap(); 를 통해 객체를 생성했다. 이를 통해 힙 영역에 VRMap에 대한 데이터들이 저장되고 그 저장된 값이 있는 주소값이 데이터 영역에 저장될 것이다. 아까와 마찬가지로 힙 영역에 데이터를 저장했지만 이전과 완전히 다르다. 객체를 한번 생성해 두고 전역변수에 그 주소값을 저장함으로써 이제 이 데이터는 "사용하는 데이터" 이다. 따라서 GC가 힙에 저장된 이 값을 지우지 않게된다. Update 문에서는 이렇게 저장된 값을 계속 재사용 하게된다.

이와 관련된 더 자세한 유니티 공식문서이다. https://docs.unity3d.com/kr/530/Manual/UnderstandingAutomaticMemoryManagement.html

자동 메모리 관리 이해 - Unity 매뉴얼

오브젝트나 문자열, 배열을 생성한 이후 저장하려면 메모리 공간이 필요합니다. 필요한 공간은 heap이라고 하는 중심 풀에서 할당됩니다. 메모리 공간을 할당받은 항목이 더 이상 사용되지 않게

docs.unity3d.com


"C++은 컴퓨터에서 빠르고 C#은 프로그래머에게서 빠르다." 이 말을을 들어본 적이 있을 것이다. 이것은 GC 때문에 생겨난 말이다. GC는 양날의 검으로 프로그래머에게 편의를 주지만 컴퓨터 성능에 안좋은 영향을 끼친다. 따라서 항상 이 GC가 너무 자주 호출되지 않도록 주의해야 한다.

'Unity 분석' 카테고리의 다른 글

Enum의 활용  (0) 2023.01.07
코루틴의 작동방식  (0) 2023.01.06
GameObject.Find()  (0) 2023.01.04
물체가 이동하면서 얇은 벽을 통과하는 경우  (0) 2023.01.04
FixedUpdate  (0) 2023.01.04