Humility

아무리 노력해도 최고가 되지 못할 수 있다⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀그럼에도 노력하는자가 가장 겸손한 것 아닌가

공부하는 블로그

유니티/문법

[Unity] 유니티 C#) Garbage Collector ( 가비지 컬렉터 )

새벽_글쓴이 2025. 1. 6. 17:21
반응형

가비지 컬렉터(GC)란?

C#은 자동 메모리 관리를 위해 가비지 컬렉터를 사용한다

더 이상 참조되지 않는 객체를 자동으로 감지하고 제거하여 메모리를 회수한다

이 과정에서 게임성능에 영향을 줄 수 있다.


가비지 컬렉터 (Garbage Collector)

시스템에서 메모리 관리를 담당하는 실제 프로그램 구성요소
  • 메모리 관리를 수행하는 프로그램 또는 프로그램의 한 부분
  • 가비지 컬렉션을 수행하는 주체
  • 시스템의 구성 요소

 

가비지 컬렉션 (Garbage Collection)

프로그램이 수행하는 메모리 정리 작업
  • 가비지 컬렉터가 수행하는 작업/프로세스 자체
  • 불필요한 메모리를 찾고 해제하는 행위
  • 작업/프로세스의 개념
쉽게 비유하자면
가비지 컬렉터 = 청소부
가비지 컬렉션 = 청소하는 행위

C#의 GC 작동 방식

1. 세대별 가비지 컬렉션 (Generational Garbage Collection)

  • Generation 0 (Gen 0)
새로 생성된 객체들이 위치
가장 자주 GC가 발생
수명이 짧은 객체들이 주로 있음

 

  • Generation 1 (Gen 1)
Gen 0에서 살아남은 객체들이 승격
Gen 0보다 덜 빈번하게 GC 발생
중간 수명의 객체들이 위치

 

  • Generation 2 (Gen 2)
Gen 1에서 살아남은 객체들이 승격
가장 드물게 GC 발생
수명이 긴 객체들이 저장

 

 

2. Mark and Sweep 알고리즘

// Mark 단계 예시
class GC {
    void Mark() {
        // 1. GC Root에서 시작
        // - 정적 필드
        // - 스레드 스택의 로컬 변수
        // - CPU 레지스터
        
        // 2. 도달 가능한 모든 객체를 재귀적으로 마킹
        foreach (object obj in roots) {
            MarkAsReachable(obj);
        }
    }
    
    // Sweep 단계
    void Sweep() {
        // 마킹되지 않은 객체들을 메모리에서 제거
        // 연속된 빈 공간을 만들기 위해 압축
    }
}

 

3. GC 작동 트리거 조건

  • Gen 0 임계값 도달 시
  • Gen 1 또는 Gen 2 임계값 도달 시
  • 시스템 메모리 부족 시
  • GC.Collect() 메서드 명시적 호출 시

 

4. Large Object Heap (LOH)

  • 85KB 이상의 큰 객체들은 별도의 힙에 할당
  • 세대별 GC와 다르게 관리됨
  • 메모리 파편화 위험이 높음
// LOH 예시
byte[] largeArray = new byte[86000]; // LOH에 할당됨

 

5. GC 최적화 기법

public class GCOptimization
{
    // 1. 메모리 압축 회피
    private byte[] buffer = new byte[85 * 1024]; // LOH 회피

    // 2. 재사용 가능한 객체 풀
    private Stack<object> objectPool = new Stack<object>();

    // 3. 가비지 생성 모니터링
    public void MonitorGC()
    {
        long gen0Before = GC.CollectionCount(0);
        long gen1Before = GC.CollectionCount(1);
        long gen2Before = GC.CollectionCount(2);

        // 작업 수행

        Debug.Log($"Gen0 Collections: {GC.CollectionCount(0) - gen0Before}");
        Debug.Log($"Gen1 Collections: {GC.CollectionCount(1) - gen1Before}");
        Debug.Log($"Gen2 Collections: {GC.CollectionCount(2) - gen2Before}");
    }
}

 

6. 백그라운드 GC

  • 별도의 스레드에서 GC 작업 수행
  • 애플리케이션 일시 정지 최소화
  • Server GC와 Workstation GC로 구분

 

7. GC 모드

  • Concurrent GC: 애플리케이션 실행과 동시에 GC 수행
  • Background GC: 별도 스레드에서 GC 수행
  • Foreground GC: 애플리케이션을 일시 정지하고 GC 수행

 

유니티 개발 시에는 특히 Gen 0 GC를 최소화하는 것이 중요하다

유니티 GC 특징

  • Unity는 Boehm GC를 사용 (표준 .NET의 세대별 GC와 다름)
  • Mark & Sweep 방식은 동일하지만 세대 관리 방식이 다름
  • Unity 2019부터 Incremental GC 도입
// Unity의 GC 특성을 고려한 최적화 예시
public class UnityGCExample : MonoBehaviour 
{
    private void Start()
    {
        // Unity에서는 이런 방식의 할당이 특히 비효율적
        for (int i = 0; i < 1000; i++) 
        {
            var temp = new Vector3(i, i, i);  // 매 프레임 GC 발생
        }
    }

    // 대신 이렇게 사용
    private Vector3[] vectors = new Vector3[1000];  // 미리 할당
    private void BetterApproach()
    {
        for (int i = 0; i < 1000; i++)
        {
            vectors[i].Set(i, i, i);  // GC 발생 없음
        }
    }
}

 

유니티 특화 GC 최적화

  • IL2CPP 사용 시 GC 성능이 향상
  • Unity Burst 컴파일러 사용으로 GC 회피 가능
  • Unity Job System과 함께 사용 시 효율적

 

유니티 GC와 .NET의 차이점

  • 세대별 수집이 없음
  • 압축(Compaction)이 없음
  • 병렬 GC 지원이 제한적
  • 증분 GC는 선택적 기능

 

유니티 프로젝트 설정에서의 GC 관련 옵션

Project Settings에서 설정 가능

1. Incremental GC Enable/Disable
2. GC 타이밍 조절 (Quality Settings의 Time slice)
3. IL2CPP 사용 시 GC 옵션

 

유니티에서 GC가 발생하는 주요 상황

  • 참조 타입(클래스) 객체 생성
  • 박싱/언박싱 연산
  • 문자열 연결 작업
  • LINQ 사용
  • foreach 루프 사용
  • 람다식과 클로저 사용

가비지 생성 최소화 ( 메모리 최적화 )

1. 객체 풀링 사용

public class ObjectPool<T> where T : Component
{
    private Queue<T> pool = new Queue<T>();
    private T prefab;
    
    public T Get() {
        if (pool.Count == 0)
            return GameObject.Instantiate(prefab);
        return pool.Dequeue();
    }
    
    public void Return(T obj) {
        pool.Enqueue(obj);
        obj.gameObject.SetActive(false);
    }
}

 

2. 문자열 처리 최적화

// 나쁜 예
string result = "";
for (int i = 0; i < 100; i++) {
    result += i.ToString();  // 매번 새로운 문자열 객체 생성
}

// 좋은 예
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
    sb.Append(i);
}
string result = sb.ToString();

 

3. 캐싱 활용

// 나쁜 예
void Update() {
    GameObject.Find("Player");  // 매 프레임마다 검색
}

// 좋은 예
private GameObject player;
void Start() {
    player = GameObject.Find("Player");  // 한 번만 검색하고 저장
}

 

4.구조체 활용

// 클래스 대신 구조체 사용
public struct Position
{
    public float x;
    public float y;
    public float z;
}

 

5. 제네릭 컬렉션 사용

// 나쁜 예
ArrayList list = new ArrayList();  // 박싱/언박싱 발생

// 좋은 예
List<int> list = new List<int>();  // 타입 안전성과 성능 향상

 

6. 값 타입과 참조 타입의 차이점

// 값 타입 (Stack에 직접 저장)
struct ValueData { 
    public int value;
}

// 참조 타입 (Heap에 저장되고 GC 대상)
class ReferenceData { 
    public int value;
}

void Example() {
    ValueData v1 = new ValueData();     // GC 발생 안함
    ReferenceData r1 = new ReferenceData(); // GC 대상
}

 

7. 유니티 이벤트와 델리게이트 최적화

// 나쁜 예 - 매번 새로운 델리게이트 할당
void Update() {
    button.onClick.AddListener(() => DoSomething());
}

// 좋은 예 - 미리 할당된 메서드 사용
void Start() {
    button.onClick.AddListener(DoSomething);
}

 

8. 프로퍼티 vs 필드

// 자동 프로퍼티는 컴파일러가 backing field를 생성
public Vector3 Position { get; set; }  // 약간의 오버헤드 발생

// 직접 필드 사용이 더 효율적
public Vector3 position;

 

9. 유니티 API 사용 시 주의사항

// 피해야 할 메서드들 (GC 부하 발생)
GameObject.FindObjectsOfType<T>();
GameObject.Find();
GetComponents<T>();

 

10. 커스텀 Allocator 사용

public class CustomAllocator<T> where T : new()
{
    private T[] items;
    private Stack<int> freeIndices;

    public CustomAllocator(int capacity)
    {
        items = new T[capacity];
        freeIndices = new Stack<int>(capacity);
        for (int i = capacity - 1; i >= 0; i--)
        {
            freeIndices.Push(i);
        }
    }

    public T Allocate()
    {
        if (freeIndices.Count > 0)
        {
            int index = freeIndices.Pop();
            items[index] = new T();
            return items[index];
        }
        throw new System.OutOfMemoryException();
    }

    public void Free(T item)
    {
        int index = System.Array.IndexOf(items, item);
        if (index != -1)
        {
            items[index] = default(T);
            freeIndices.Push(index);
        }
    }
}

 

11. 메모리 파편화 방지

  • 비슷한 크기의 객체들을 같이 할당하여 메모리 파편화 최소화
  • 객체 풀의 크기를 미리 계산하여 동적 확장 방지
  • 가능한 한 고정 크기의 배열 사용

 

12. 유니티 잡 시스템 활용

using Unity.Jobs;
using Unity.Collections;

public struct DataProcessJob : IJob
{
    public NativeArray<float> data;
    
    public void Execute()
    {
        // 값 타입과 NativeContainer를 사용하여 GC 없이 처리
        for (int i = 0; i < data.Length; i++)
        {
            data[i] = ProcessData(data[i]);
        }
    }
}

 

13. 고급 프로파일링 기법

public class MemoryProfiler : MonoBehaviour
{
    private System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
    
    void Profile(string operation)
    {
        long beforeMem = System.GC.GetTotalMemory(false);
        stopwatch.Reset();
        stopwatch.Start();
        
        // 프로파일링할 작업 수행
        
        stopwatch.Stop();
        long afterMem = System.GC.GetTotalMemory(false);
        
        Debug.Log($"{operation}: Time={stopwatch.ElapsedMilliseconds}ms, " +
                  $"Memory Delta={(afterMem-beforeMem)/1024}KB");
    }
}

 

14. 추가 최적화 팁

  • Unity Profiler를 사용하여 GC 할당을 모니터링
  • Update() 메서드 내에서 new 키워드 사용 최소화
  • GetComponent 호출 결과를 캐싱
  • 코루틴에서 WaitForSeconds 객체 재사용

 

실제 최적화가 필요한 시점

  • 프로파일러에서 GC가 자주 발생하는 것이 확인될 때
  • FPS 모니터링에서 주기적인 프레임 드랍이 발견될 때
  • 메모리 사용량이 지속적으로 증가할 때
  • 모바일 기기에서 발열이나 배터리 소모가 심할 때
  • 로딩이나 씬 전환에서 끊김 현상이 발생할 때
  • 대량의 객체를 생성/삭제하는 시스템
  • 긴 시간 실행되는 게임에서 메모리 누수가 의심될 때

 

디버깅 및 모니터링

// 예시 코드
public class MemoryDebugger : MonoBehaviour
{
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.M))
        {
            Debug.Log($"Total Memory: {System.GC.GetTotalMemory(false) / 1024 / 1024}MB");
            System.GC.Collect();
        }
    }
}

 

과도한 최적화는 코드의 가독성과 유지보수성을 해칠 수 있다
그렇기에 실제 문제가 있는 부분을 찾아내어 선택적으로
적용하는 것이 좋을 수도 있다

 

 


가비지 컬렉션 최소화와 메모리 최적화

 

이 두가지는 상당히 겹치는 부분이 많다

 

 

메모리 최적화 방법

  • 메모리를 효율적으로 사용
  • 전반적인 메모리 사용량 감소
  • 더 나은 성능을 위한 코드 구조 개선

 

가비지 생성 최소화 방법

  • 불필요한 객체 생성을 줄임
  • 재사용 가능한 객체 활용
  • 임시 객체 생성 회피

가비지 컬렉션 최소화

가비지 컬렉터가 청소하는 작업을 최소화

 

1. GC.Collect() 호출 타이밍 조절

public class GCController : MonoBehaviour 
{
    void Start() 
    {
        // 로딩이 끝난 직후나 씬 전환 시점에 GC 실행
        StartCoroutine(CollectGCAtOptimalTime());
    }

    IEnumerator CollectGCAtOptimalTime() 
    {
        // 중요한 작업이 없는 시점까지 대기
        yield return new WaitForSeconds(1f);
        System.GC.Collect();
    }
}

 

2. 증분 가비지 컬렉션(Incremental GC) 설정

  • Project Settings에서 설정 가능
  • GC를 여러 프레임에 걸쳐 분산 실행
  • 한 번에 큰 멈춤 현상 방지

 

3. 가비지 컬렉션이 발생하는 임계값 조정

public class GCThresholdManager 
{
    void AdjustGCThreshold() 
    {
        // GCSettings를 통한 설정
        // Unity에서는 제한적으로만 가능
        System.Runtime.GCSettings.LargeObjectHeapCompactionMode = 
            System.Runtime.GCLargeObjectHeapCompactionMode.CompactOnce;
    }
}

 

4. GC 실행 시점 최적화

public class LoadingManager : MonoBehaviour 
{
    void LoadLevel() 
    {
        // 큰 리소스를 로드하기 전에 GC 실행
        System.GC.Collect();
        Resources.Load("LevelData");
        
        // 로드 완료 후 다시 한번 GC 실행
        System.GC.Collect();
    }
}

 

5. 프로파일링을 통한 GC 타이밍 모니터링

public class GCProfiler : MonoBehaviour 
{
    void Update() 
    {
        // GC 발생 시점과 영향 모니터링
        Debug.Log($"Gen 0 collections: {System.GC.CollectionCount(0)}");
        Debug.Log($"Gen 1 collections: {System.GC.CollectionCount(1)}");
        Debug.Log($"Gen 2 collections: {System.GC.CollectionCount(2)}");
    }
}

 

가비지 생성 최소화 방법과 함께 사용하면
보다 효과적인 메모리 관리가 가능할 것 같다
반응형