본문 바로가기

기술면접/정리하기

힙 메모리 최적화와 오브젝트 풀링

메모리 힙영역

: 프로그램이 돌아가면서 생서오디는 값들은 메모리의 힙영역에 할당된다. == 동적 메모리 할당

 

ex) 게임 내 새로운 캐릭터 생성으로 인한 값, 네트워크 작업을 통해 일정한 데이터를 받아 배열을 생성

 

이러한 메모리 할당 요청이 발생하면 힙 메모리를 관리하는 관리자가 필요한 만큼의 영역을 힙에 예약한다.

예약된 영역은 다른 값이 할당 될 수 없는 곳이 된다.

예약된 영역은 핸들러나 포인터를 반환하여 값을 할당하는 등의 접근이 가능하게 된다.

 

 

누수현상 (Memory Leak)

: 오류로 인해 영역을 가리키는 포인터 반환이 안된경우, 또는 영역의 예약이 잘못된 경우 등으로 해당 힙 영역을 사용하지 못하게 되는것을 메모리 누수(Memory Leak)라고 한다.

문제가 되는 이 영역은 다시 사용할 수 있도록 예약 해제가 되어야 하는데, 그래야 다시 사용할 수 있는 상태가 되기 때문이다.

사용되지 않는 영역이 쌓이다보면은 언젠가 프로그램이 멈추게 된다.

 

 

가지비 컬렉션 (Garbage Collection)

: 누수현상으로 인한 영역의 값들을 쓰레기(Garbage)라고 부른다. 이 값을 모는 행위를 쓰레기 수집(Gabage Collection)이라 한다.

쓰레기값을 모아서 해제하면 다시 상사용할 수 있는 영역이 되기 때문에 이것들을 모아둔다.

Java나 C# 언어에서는 애초부터 쓰레기 수집을 염두해두었기 때문에 프로그래는 쓰레기 수집에 신경을 덜 쓸 수 있지만, C언어의 경우 프로그래머가 직접 해제해야하기 때문에 불편함이 있다.

 

쓰레기 수집은 만능이 아니다.

가비지 콜렉터가 쓰레기 수집을 알아서 해주는 것에 100% 의지하면 안된다.

Java나 C#은 알아서 쓰레기 수집을 해주지만 직접적으로 필요한 시점에 쓰레기 수집을 해야 할 때도 있다.

하지만 이 작업은 외외로 CPU 사용이 많을 수 있으므로 짧은 시간에 자주 사용는 것은 프로그램 퍼포먼스 저하를 발생할 수 있다.

언제 얼마나 해야할지 혹은 해도 될지 어떻게 판단해야 할지 알아내는 것도 쉽지 않다.

 

Unity의 경우 프로파일링을 통해 어느 시점에 힙 메모리의 변화가 큰지를 감지할 수 있다.

프로파일링을 통해 갑작스런 힙 메모리의 증가를 발견하였다면 문제가 되는 코드를 추적 할 수 있다.

 

ex1)

string ConCatExample(int[] intArray)
{
   string line = intArray[0].ToString();
 
   for(int i = 1; i < intArray.Length; i++)
   {
       line += ", " + intArray[i].ToString();
   }
 
   return line;
}

파라미터로 받은 정수형 배열을 루프를 돌며 콤마를 추가하는 스트링을 만들고 있다.

이 코드는 매우 간결해 보이지만 가비지 콜렉터에게는 악몽같은 코드이다.

line 변수는 매 루프마다 내용이 모두 삭제되고 다시 할당된다.

문자열이 증가할 때마다 힙의 크기도 증가한다.

이 함수호출이 일어날 때마다 수백바이트의 힙 공간을 차지한다.

 

해결책 : 퍼포먼스를 위하여 힙영역에 메리를 불필요하게 차지하는 행위를 하지말자.

위의 예제에서는 StringBuilder를 사용하는 것이 좋다.

 

※String

: 문자열을 담을 수 있는 읽기전용 참조 타입이다.

※StringBuilder

: String이 문자열을 표현하는 클래스라면 StringBuilder는 문자열을 조작하는 클래스다.

 미리 할당해놓은 메모리에서 객체 자체를 조작하므로 string의 메서드보가 효율이 좋다.

 주로 추가, 삽입, 삭제, 대체 역할을 한다.

 

ex2)

public Vector3 targetPos;
 
void Update()
{
   Vector3 movePosition = targetPos + Vector3.right;
   transform.position = movePosition;
}

매 프임마다 오브젝트를 targetPos의 수치에 따라 움직이게 하는 코드이다.

이러한 방식은 값이 변하지 않음에도 불구하고 새로운 값을 생성하고 비효율적이며 불필요한 쓰레기 조각도 조금씩 생성된다.

 

해결책 :

public Vector3 targetPos;
public Vector3 oldPos;
 
void Update()
{
   if (targetPos != oldPos)
   {
      Vector3 movePosition = targerPos + Vector3.right;
      transform.position = movePosition;
      targetPos = oldPos;
   }
}

오브젝트의 position이동은 targetPos에 의존적이기 때문에 targetPos의 값에 변함이 없다면 굳이 이동하는 로직을 수행할 필요가 없다. 그러므로 변함이 있을 경우에만 해당 로직을 수행하여 오브젝트의 포지션을 변경한다.

 

 

 

ex3)

float[] RandomList(int numElenments)
{
   var result = new float[numElements];
 
   for (int i = 0; i < numElements; i++)
   {
      result[i] = Random.value;
   }
 
   result result;
}
 

배열을 생성하고 루프에서 배열에 값을 채운다음 결과를 반환한다.

전혀 이상할 것 없는 코드이지만 이 함수가 호출될 때마다 배열의 크기만큼 새로운 힙 영역을 할당하게 된다.

 

해결책

void RandomList(float[] arrayToFill)
{
   var result = new float[numElements];
 
   for(int i = 0; i < arrayToFill.Length; i++)
   {
      arrayToFill[i] = Random.value;
   }
}
 

반환하지 않는 함수 방식이다.

참조형으로 받은 배열을 그대로 값만 바꾼다.

배열을 new 키워드로 새로 생성할 필요가 없으므로 추가적인 힙 메모리 할당이 없다.

 

 

 

오브젝트 생성과 파괴

: 유니티에서 게임을 동적으로 오브젝트를 생성하고 파괴하는 것은 부담이 크고 그 양이 많을 수록 부담도 커진다.

ex) 총알을 발사할때마다 총알 오브젝트를 생성하고 부딪히면 파괴하는 것,

 디펜스 게임에서 수많은 적들을 생성하고 파괴시키는 것 등

양이 많지 않으면 큰 문제가 되지 않지만 짧은 시간에 많은 오브젝트가 생성되고 파괴되는 게임이 길어질수록 많은 자원을 사용하여 배터리 소모나 발열의 원인이 된다.

또한 힙 메모리를 짧은 시간에 자주 할당함으로써 가비지(쓰레기값)이 쌓이게 된다.

 

 

 

오브젝트 풀링

: 오브젝트의 생성과 파괴가 부담이 된다면 생성과 파괴를 최소한으로 줄이면 되는데, 좋은 방법으로는 생성된 오브젝트를 파괴하지않고 재사용하는 것이다.

오브젝트를 필요한 만큼 만들어 둔 뒤, 사용해야할 때 사용하고 사용이 끝나면 파괴하지 않고 다시 사용가능하도록 하는 것이다. 이것을 오브젝트 풀링이라고 한다.

위의 사진과 같이 간단한 순서에 으의해 오브젝트 풀을 사용한다.

오브젝트 생성 → 오브젝트 풀에 넣음 → 꺼냄 → 사용전 초기화 → 사용 → 사용종료 → 다시 오브젝트 풀에 넣기

→다시 사용이 필요할 때 오브젝트를 생성하는 것이 아닌 Pool에서 꺼내기

이렇게 하면 한 번의 생성으로 오브젝트를 재사용함으로 추가 생성과 파괴에 대한 부담이 없어진다. 

 

 

 

가변적인 풀링

: 게임 중 화면에 보이게 될 총알 오브젝트가 10개라서 10개를 풀처리 했다. 그런데 무기를 업그레이드 하면서 총알 속도가 빨라져 10개 이상의 총알이 필요하게 된 경우가 발생했다. 이 경우를 대비하여 풀에 오브젝트의 수가 0일 때 pop 요청이 발생하면면 오브젝트를 생성하여 반환하는 로직이 필요하다. 또한 어느 순간, 오브젝트의 요청이 많아져 풀의 크기가 급격히 증가하였다. 하지만 이후에 사용률이 낮아져 많은 양의 오브젝트가 사용되지 않았다. 이 경우 풀에 불필요한 오브젝트를 쌓아 둘 필요가 없어 제거하는 로직이 필요하다.

 

※ 제거하는 로직에 대해서는 의견이 나뉜다.

→ 메모리의 효율적인 관리를 위해 오랫동안 사용하지 않은 오브젝트는 제거해야한다는 의견.

→ 오브젝트가 pool에 있을 경우 오브젝트를 비활성화 한다면 해당 오브젝트에 붙은 컴포넌트를 실행할 일이 없고 보이지 않으므로 렌더링에도 포함되지 않기고, 제거할 깨 부담만 생기기에 그대로 둬야한다는 의견.