유니티 가비지 컬렉션 최적화하기 4 – 번역

유니티에서 가비지 컬렉션 최적화하기 4

원문 링크

 

이전글 – 가비지 컬렉션에 의한 피해 줄이기

불필요한 힙 할당을 발생시키는 주요 원인 (Common causes of unnecessary heap allocations)

값 타입의 로컬 변수는 스택에 할당되고 이외의 모든 것은 힙에 할당된다는 것을 살펴봤습니다.
그러나, 힙 할당이 우리를 놀라게할만한 상황이 많이 있습니다.
불필요하게 힙 할당이 발생하는 몇가지 주요 원인을 살펴보고, 이를 줄이는 가장 좋은 방법에 대해서 살펴보겠습니다.

 

문자열 (Strings)

C#에서 문자열은 참조 타입입니다. 문자열의 “값”을 저장하고 있는 것처럼 보이지만, 값 타입(Value Type)이 아닌 참조 타입(Reference type)입니다.
즉, 문자열을 생성하고 삭제하면 가비지가 생성됩니다. 문자열은 일반적으로 많은 코드에서 사용되기 때문에, 이로 인해서 가비지가 많이 쌓일 수 있습니다.

C#의 문자열은 변경이 불가능하기 때문에, 처음 생성한 후에는 값을 변경할 수 없습니다.
따라서 문자열을 변경할때마다(예: +연산자를 이용해서 두 개의 문자열을 더하는 경우), 유니티는 새로운 문자열을 생성하고 업데이트된 값을 저장한다음, 기존의 문자열을 삭제합니다. 이 과정에서 가비지가 생성됩니다.

몇가지 간단한 규칙을 따르면, 문자열로 인해서 생성되는 가비지의 양을 최소화할 수 있습니다. 이 규칙들을 고려해서, 적용하는 방법에 대해서 살펴보겠습니다.

  • 불필요한 문자열 생성을 줄여야 합니다. 동일한 문자열 값을 두번 이상 사용하는 경우, 해당 문자열을 한번만 생성하고 캐시에 저장해서 사용해야합니다.
  • 불필요한 문자열 변경을 줄여야 합니다. 예를 들어, 자주 업데이트되는 텍스트 컴포넌트와 연결된 문자열이 있는 경우, 두 개의 텍스트 컴포넌트로 분리하는 것을 고려해보는 것이 좋습니다.
  • 런타임에 문자열을 생성해야하는 경우, StringBuilder 클래스를 사용할 수 있습니다. StringBuilder 클래스는 할당을 발생시키지 않고 문자열을 생성하도록 설계되었기 때문에, 복잡한 문자열을 연결할때 생성되는 가비지의 양을 줄일 수 있습니다.
  • 디버깅을 위한 목적으로 더 이상 필요하지 않게되면, Debug.Log()의 호출을 제거해야 합니다. Debug.Log()의 호출은 화면에 아무것도 출력하지 않더라도, 게임의 모든 빌드에서 실행됩니다. Debug.Log()는 최소 하나의 문자열을 생성하고 이를 처리하기 때문에, 게임에서 Debug.Log()를 많이 호출하면, 이로 인해서 가비지가 쌓일 수 있습니다.

비효율적인 문자열의 사용으로 인해서 불필요한 가비지가 생성되는 코드의 예제를 살펴보겠습니다.
다음의 코드에서는, Update()함수에서 “TIME :” 문자열과 float 타이머 값을 합쳐서 점수를 표시하기위한 문자열을 생성합니다. 이 코드는 불필요한 가비지를 생성합니다.

public Text timerText;
private float timer;

void Update()
{
    timer += Time.deltaTime;
    timerText.text = "TIME:" + timer.ToString();
}

다음 예제는 상당히 개선된 예입니다.
별도의 텍스트 컴포넌트를 추가하고, Start()함수에서 “TIME:”이라는 단어를 설정했습니다. 즉, Update()함수에서 더이상 문자열을 결합하는 작업이 필요하지 않게 되었습니다.
이렇게하면, 생성되는 가바지의 양이 상당히 줄어듭니다.

public Text timerHeaderText;
public Text timerValueText;
private float timer;

void Start()
{
    timerHeaderText.text = "TIME:";
}

void Update()
{
    timerValueText.text = timer.toString();
}

 

 

유니티 함수 호출 (Unity function calls)

유니티 자체 코드 또는 플러그인 여부에 관계없이, 직접 작성하지 않은 코드를 호출할때마다 가비지가 생성될 수 있다는 것을 인식하는 것이 중요합니다.
일부 유니티 함수 호출은 힙 할당을 발생시키기 때문에 불필요한 가비지가 생성되지 않도록 주의해서 사용해야합니다.

호출을 피해야하는 함수에 대한 목록은 없습니다. 모든 함수는 일부 상황에서는 유용한 동시에 일부 다른 상황에서는 유용하지 않을 수 있습니다.
이에 관계없이, 게임을 프로파일링해서 가비지를 생성시키는 원인을 파악하고, 어떻게 처리할 지를 고민하는 것이 가장 좋은 방법입니다.
경우에 따라, 함수의 결과를 캐시에 저장하는 것이 좋은 방법이 될 수 있습니다; 이와 다른 경우로, 다른 함수를 사용하도록 코드를 리팩토링하는 것이 좋은 방법이 될 수 있습니다. 그렇긴 해도, 힙 할당을 발생시키는 유니티 함수의 주요 예제를 몇개 살펴보고, 이를 처리하는 최선의 방법에 대해서 살펴보겠습니다.

배열을 반환하는 유니티 함수에 접근할때마다, 새로운 배열이 생성되고 이 배열이 반환 값으로 전달됩니다.
이 동작은 특히, 해당 함수가 accessor 일때(예: Mesh.normals), 항상 정확하거나 예상할 수 있는 것은 아닙니다.

다음 코드에서는 루프를 반복할때마다 새 배열이 생성됩니다.

void ExampleFunction()
{
    for (int i = 0; i < myMesh.normals.Length; i++)
    {
        Vector3 normal = myMesh.normals[i];
    }
}

이런 경우에는 할당을 줄이는 것이 쉽습니다: 간단히 배열의 참조 값을 캐시에 저장할 수 있습니다. 이렇게하면, 한개의 배열만 생성되고, 이에 따라서 생성되는 가바지의 양이 줄어듭니다.

다음 코드는 이 방법을 보여줍니다. 예제의 경우, 루프를 실행하기 전에 Mesh.normals를 호출해서, 이 참조 값을 캐시에 저장해서 하나의 배열만 생성되도록 했습니다.

void ExampleFunction()
{
    Vector3[] meshNormals = myMesh.normals;

    for (int i = 0; i < meshNormals.Length; i++)
    {
        Vector3 normal = meshNormals[i];
    }
}

예상치 못한 힙 할당을 발생시키는 함수의 다른 예로, GameObject.name 과 GameObject.tag 가 있습니다.
이 둘은 모두 새로운 문자열을 반환하는 접근자(accessor)입니다. 즉, 이러한 함수를 호출하면 가비지가 생성됩니다.
값을 캐싱하는 것이 유용할 수 있지만, 이 경우에는 대신 사용할 수 있는 유니티 함수가 존재합니다.
가비지를 생성하지 않고 게임 오브젝트의 태그(tag)를 값과 비교할때는, GameObject.CompareTag()를 사용할 수 있습니다.

다음 예제 코드에서는, GameObject.tag를 호출해서 가비지를 생성합니다.

 
private string playerTag = "Player"; 
void OnTriggerEnter(Collider other) 
{ 
    bool isPlayer = other.gameObject.tag == playerTag; 
}

GameObject.CompareTag()를 사용하면, 이 함수는 더이상 가비지를 생성시키지 않습니다:

 
private string playerTag = "Player"; 

void OnTriggerEnter(Collider other) 
{ 
    bool isPlayer = other.gameObject.CompareTag(playerTag); 
} 

GameObject.CompareTag()는 교유한 함수가 아닙니다; 많은 유니티 함수 호출에는 힙 할당을 발생시키지 않는 대체 버전이 존재합니다.
예를 들어, Input.GetTouch()와 Input.touchCount를 Input.touches 대신 사용할 수 있습니다.
또는 Physics.SphereCastAll() 대신 Physics.SphereCastNonAlloc()을 사용할 수 있습니다.

 

박싱 (Boxing)

박싱(Boxing)이란 값 타입(Value Type)의 변수가 참조 타입(Reference Type)의 변수로 사용될때 발생하는 상황을 나타내는 용어입니다.
박싱은 주로 int나 float와 같은 값 타입 변수를, Object.Equals()와 같이 object 파라미터를 요구하는 함수에 전달할때 발생합니다.

예를 들어, String.Format()함수는 string과 object 파라미터를 전달받습니다. 이 함수에 string과 int를 전달하면, 전달된 int변수에서 박싱이 발생합니다.
아래 코드는 박싱의 예를 보여줍니다:

void ExampleFunction()
{
    int cost = 5;
    string displayString = String.Format("Price: {0} gold", cost);
}

박싱이 처리되는동안 내부에서 일어나는 과정 때문에, 박싱은 가비지를 생성합니다.
값 타입의 변수가 박싱되면, 유니티는 임시로 힙에 System.Object 변수를 생성해서, 해당 값 타입의 변수를 래핑(Wrapping, 감쌉니다)합니다.
System.Object는 참조 타입의 변수이기 때문에, 이 임시 오브젝트가 처리될때 가비지가 생성됩니다.

박싱은 불필요한 힙 할당의 매우 전형적인 사례입니다.
코드에서 직접 변수를 박싱하지 않더라도, 박싱을 발생시키는 플러그인을 사용하는 경우가 있을 수 있고, 다른 기능의 내부 처리과정 중에서 박싱이 발생할 수도 있습니다. 가능하면 박싱을 피하고, 박싱을 발생시키는 함수 호출을 제거하는 것이 가장 좋은 방법입니다.

 

내용 끝까지 읽어주셔서 감사합니다.
배너 클릭은 저에게 많은 힘이 됩니다.
감사합니다 🙂

 

다음글 – 불필요한 힙 할당을 발생시키는 주요 원인 2

RonnieJ

프리랜서 IT강사로 활동하고 있습니다. 게임 개발, C++/C#, 1인 기업에 관심이 많습니다.

2 Responses

  1. Cargold 댓글:

    최적화 관해서 공부하던 중에 방문하게 됐습니다.
    일부 유니티 함수 중에는 배열을 미리 캐싱하고 반복문에서 처리하는게 좋겠군요.
    좋은 정보 배워갑니다.

    • RonnieJ 댓글:

      자주 사용되는 배열이라면 미리 저장해두는 게 도움이 되죠 ㅎㅎ

      블로그 방문 감사드려요~
      배너 클릭은 큰 도움이 됩니다. 자주 들러주세요~

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

Please turn AdBlock off

Notice for AdBlock users

Please turn AdBlock off