C# - GC(+LOH, SOH란?), 유니티 예시

Justin·2022년 8월 10일
0

C#

목록 보기
4/7

검색을 하다가 Loh, soh라는 내용을 봤는데 이게 뭘까 궁금해서 찾는김에 정보를 정리하고자 한다.

GC(Garbage Collector)

우선 Loh, soh를 알기 위해서는 GC. 가비지 컬렉터에 대해 살짝 알아야 한다. GC 는 메모리를 관리하기 위한 존재이다.

C나 C++에서는 객체를 위해 메모리 공간을 확보하고, 객체를 할당한 후에 해당 객체의 작업이 종료되면 할당했던 메모리의 힙을 가리키는 포인터를 통해 메모리를 해제하는 작업을 직접 구현해주어야 한다.

하지만 이 과정이 복잡하고, 실수를 자주 발생 시키기에 메모리 할당 및 해제를 알아서 처리해주는 GC가 등장하였다.

GC의 세대 별 관리

GC에서는 0세대, 1세대, 2세대로 메모리를 관리 한다. 처음 생성되어 메모리에 할당 되는 친구들을 0세대에 두고 사용하는지 검증하여 사용하지 않다면 해제를 해버린다.

하지만 여기서 계속 사용되는게 있다면 1세대로 거기서도 계속 사용되면 2세대로 넘겨서 관리를 하는 방식이다.

이때 다음 세대로 넘어갈 수록 관리를 하는 빈도가 줄어든다. 오래 살아남은 개체일수록 해제 하지 않아도 되고, 중요한 개체일 가능성이 높아지기 때문이다.

추가
GC는 세대별로 호출되어지는게 아니고 해당 세대가 호출되면 이전 세대들도 불려진다. 즉 2세대의 경우는 전체 GC를 호출한다고 볼 수 있다.

GC 장단점

  • 💗 포인터에 대한 이해가 없어도 괜찮다.
  • 💗 메모리 할당, 해제에 관해 실수가 적어진다.
  • 💔 GC가 동작 시에는 다른 작업들이 멈춘다.
  • 💔 메모리 관리를 직접할 수 없어 메모리를 더 효율적으로 관리하기는 힘들다

LOH, SOH

LOH, SOH는 위에서 설명한 GC에서 사용하는 방식 중 일부이다. 개체의 크기가 85kb 보다 크거나 같으면 큰 개체(LOH)로 구분 이보다 작으면 작은 개체(SOH)로 구분하여 GC에서 관리를 하는 것이다.

이렇게 구분하는 이유도 용량이 클 수록 중요할 가능성이 높기에 따로 관리하기 위함이다.

SOH(Small of Heap)는 위에서 설명한 세대별 관리 방식으로 일반적이게 관리 되는 방식을 뜻한다.

LOH(Large of Heap)
여기서 LOH에 할당된 개체들은 3세대라 불리는 곳에 배치 된다. 3세대는 물리적 세대로, 논리적으로는 2세대의 일부로 수집된다.(고로 2세대와 LOH는 같이 수집된다.)

코드로 동작

우리도 가비지 컬렉터 기능을 이용 할 수는 있지만 사용자 코드에서는 0세대 또는 LOH만 할당할 수 있으며 1세대, 2세대의 경우는 오직 GC에서만 가능하다.

GC 세대별 동작 그림 설명

첫 번째 그림에서 다음 Gen0에서 살아남은 Obj0, obj2가 Gen1을 만들고, 또 살아남은 obj0이 Gen2를 만든다.

위 그림에서 보면 GC로 인해 사용되지 않는 개체가 메모리 해제되며 Free Space가 발생하게 되는데 이때 저 공간을 낭비 시키지 않기 위해 재배치 하는 과정을 거친다.

단, LOH에서는 용량이 크기 때문에 재배치할 경우 오버헤드 비용이 크기 때문에 발생하지 않는다.

결국 2세대 GC가 동작하게 되면 SOH(0, 1, 2), LOH가 모두 동작하기에 비용이 클 수 밖에 없으니 최대한 적게 동작하게 만들면 좋다.

언제 GC가 호출될까?

  • 할당이 0세대, LOH 임계값 초과
    임계치를 초과할 때 이를 확장 하기 위해 GC가 호출되어 정리하고, 그래도 부족하다면 OS에서 획득하려고 시도하게 된다.

  • GC.Collect 메서드 호출
    직접 가비지 컬렉터를 호출 할 수도 있다. 매개 변수가 없는 GC.Collect() 메서드가 호출되거나 다른 오버로드에서 인수로 GC.MaxGeneration을 전달하면 LOH가 나머지 관리되는 힙과 함께 수집된다.

  • 시스템 메모리 부족
    OS에서 메모리 부족을 알릴 경우 GC가 이를 캐치하여 동작하기도 한다.

유니티에서는 어떻게 활용 할까?

오래 플레이되는 게임에서 부드러운 프레임률을 유지 할 때

작은 영역을 자주 할당하지만, 짧은 기간 동안만 사용할 때에 이렇게 주기적으로 GC를 호출해줄 수 있다.


이렇게 되면 GC가 필요 이상으로 많이 동작하긴 하지만, 한 번 동작할 때 오랜기간 동작하며 유저들에게 고통을 주지 않고 부드럽게 플레이 할 수 있게 된다.

메모리 할당, 가비지 콜렉션이 비교적 자주 발생하지 않아 게임플레이 중간에 처리할 때

위에서 언급한대로 처음 힙에 할당된 애들이 공간을 차지하고, 이게 부족하면 OS에 요청한다고 했다.

그 방법을 이용해서 아래의 코드와 같이 시작 시에 힙 영역의 많은 공간을 할당하게 만들고 비워주어 힙의 크기를 넓혀놓는 방법이다.

이러면 GC에서 공간을 넓히기 위해 할당한 애들을 다 정리하는데 비용이 발생하지만, 미리 공간이 늘어나있기에 다른 메모리들이 할당 되어도 한동안은 GC가 호출되지 않아 안정적으로 플레이가 가능하다.

*주의: 다만 너무 커서 운영체제가 시스템 메모리를 확보하려고 앱을 강제종료하는 경우

점진적 가비지 컬렉션

유니티는 기본적으로 Boehm–Demers–Weiser 라 하여 GC 동작 시에 다른 동작들을 멈추는 방식을 의미힌다.

점진적 가비지 컬렉션은 위와 같은 방식의 문제점을 해결하고자 조금씩 가비지 컬렉션을 실행시키는 방식을 뜻한다.

어떻게 보면 위에서 일정 프레임마다 가비지 컬렉션을 동작 시킨것과 유사한 방법이다.


일반적으로 가비지 컬렉터가 동작할 때 이렇게 스파이크 현상이 발생하는데

점진적 동작을 설정하면 그래프가 튀지 않고 안정적인 모습을 볼 수 있다.

설정은 여기서 확인 할 수 있다.

참고자료
[Unity Docs]
https://docs.unity.cn/kr/2021.1/Manual/UnderstandingAutomaticMemoryManagement.html
[MSDN]
https://docs.microsoft.com/ko-kr/dotnet/standard/garbage-collection/large-object-heap

profile
인디 게임을 만들며 공부하고 있습니다.

0개의 댓글