유니티 메모리 최적화 - yuniti memoli choejeoghwa

힙 메모리 영역

프로그램을 만들 때 힙 메모리 영역은 프로그래머가 항상 신경 써야 되는 중요한 메모리 영역이다.

유니티에서 힙 변수가 생성될 때 여유 메모리가 충분하지 않다면 가비지 컬렉션작업을 수행한다.

하지만 가비지 컬렉션 작업은 꽤나 무거운 작업이기 때문에 이 순간 프레임 드랍이 발생한다.

이를 개선하기 위해 최근에는 Inscremental GC 기능이 업데이트 되긴하였지만,

그래도 가비지 컬렉션의 대상이 되는 힙 할당을 최대한 만들지 않는 것이 좋다.

1. 자주 사용되는 참조 변수는 캐싱하기

다음의 코드는 함수를 호출할 때마다 새로운 힙 메모리 영역에 할당하게 되어 가비지가 발생한다. 

public void MyMethod()
{
    Button btn = GetComponent<Button>(); // 함수 호출시 마다 힙 메모리를 할당한다.
    SomeMethod(btn);
}

다음과 같이 참조를 멤버 변수로 선언하여 가비지를 줄일 수 있다.

private Button btn;

void Start()
{
    btn = GetComponent<Button>();
}

public void MyMethod()
{
    SomeMethod(btn);
}

2. 컬렉션을 새로 만들 때 발생하는 가비지

다음의 코드는 매 프레임 새 컬렉션을 생성하고 있다.

public void Update()
{
    List<int> myList = new List<int>();
    SomeMethod(myList);
}

컬렉션을 Clear()로 내용을 비워서 사용하면 가비지를 줄일 수 있다.

private List<int> myList = new List<int>();

public void Update()
{
    myList.Clear();
    SomeMethod(myList);
}

3. 문자열 연결 시 발생하는 가비지

문자열은 값처럼 보이지만 사실 참조 유형이다. 이를 연결하는+ 연산자는 대표적인 가비지를 생산하는 주범이다.

아래의 코드에서는 기존의 문자열이 존재하는 메모리 공간에 연결시킨 것이라 착각할 수 있다.

하지만 실제로는 새 문자열을 만들고 이전 문자열을 가비지로 버린다.

string name = "Mark";
string str = "My Name : ";
str += name;

 문자열 여러 개를 연결할 때는 가급적 stringBuilder 클래스를 사용하도록 하자.

4. Unity 함수 호출에서 발생하는 가비지

직접 작성하지 않은 코드는 가비지가 발생할 수 있다는 점을 염두에 두도록 하자.

배열을 반환하는 Unity 함수에 액세스 할 때마다 새 배열이 생성되어 반환된 값으로 전달된다.

다음의 코드는 반복문 안에서 매번 Mesh.normals를 호출할 때마다 배열이 생성된다.

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

다음과 같이 캐싱을 해서 가비지를 방지할 수 있다.

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

GameObject.tag이나 GameObject.name 접근 시에도 매번 문자열을 생성해서 반환한다. 

void OnTriggerEnter(Collider other)
{
    if(other.gameObject.tag == "Player")
    {
        //...
    }
}

태그 비교는 GameObject.CompareTag()를 사용하면 힙 할당을 하지 않는다.

string playerTag = "Player";

void OnTriggerEnter(Collider other)
{
    if(other.gameObject.CompareTag(playerTag))
    {
        //...
    }
}

이외에도 힙 할당을 발생하는 유니티 함수들과 힙 할당을 하지않은 대체용 함수들이 존재한다.

Input.GetTouch()와 Input.touchCount는 힙 할당을 일으키니 Input.touches를.

Physics.SphereCastNonAlloc()대신 Physics.SphereCastAll()를 사용하도록 하자.

5. 코루틴에서 발생하는 가비지

yield 자체는 힙 할당을 하지 않지만 전달하는 값은 박싱이 되기 때문에 힙 할당이 발생할 수 있다.

yield return 0; // 0인 int 값 변수가 boxing되기 때문에 가비지를 발생시킨다.

new 를 통해 생성하는 객체는 가비지를 만든다.

While(true)
{
    yield return new WaitForSeconds(1f); // 매번 새로운 객체가 생성된다.
}

WaitForSecond 또한 객체이기 때문에 멤버 변수나 지역변수로 캐싱할 수 있다.

WaitForSeconds delay = new WaitForSeconds(1f);

While(true)
{
    yield return delay;
}

6. Foreach 루프 (유니티 5.5 이하 버전)

유니티 5.5 이하 버전에서는 foreach를 실행하면 루프가 종료될 때마다 박싱이 일어났지만,

최근 버전에서는 수정되었다.

행복한 연어의 이야기

IT/Unity

(Unity) 유니티 최적화 방법들

해피살몬 2020. 1. 30. 20:22

스크립팅 최적화

  • SendMessage 와 BroadcastMessage 함수 사용자제
  • Find 관련 함수 사용자제
  • 자식이 많은 오브젝트 transform 변경시 많은 비용 발생
  • Update , LateUpdate 등 함수가 비어있으면 지워라 , 빈 Update 종류 함수는 비용발생
  • Vector2 및 Vector3 연산을 줄여라
  • Camera.main 의 결과를 캐싱 하거나 사용하지 않고 수동으로 카메라에 대한 참조를 관리해라

가비지 컬렉터(힙 메모리) 최적화

  • 캐싱 사용
    우리 코드가 힙 할당으로 이어지는 함수를 반복적으로 호출하고 결과를 폐기하면 불필요한 쓰레기가 생성됩니다.
    대신, 우리는 이러한 객체에 대한 참조를 저장하고 재사용하는 기술을 캐싱이라고 합니다.
  • 자주 호출되는 함수에는 힙메모리 할당을 하지 않는다.
  • 오브젝트 풀링 사용
  • 문자열 관련
    1) 필요한 문자열 생성을 줄여야합니다. 동일한 문자열 값을 두 번 이상 사용하는 경우 문자열을 한 번 만들고 값을 캐시해야합니다.
    2) 우리는 불필요한 문자열 조작을 줄여야합니다. 예를 들어 자주 업데이트되고 연결된 문자열이 포함 된 텍스트 구성 요소가있는 경우 두 개의 텍스트 구성 요소로 구분할 수 있습니다.
    3) 런타임에 문자열을 작성해야 한다면 StringBuilder 클래스를 사용해야 합니다 .
    4) 디버깅을 위해 더 이상 필요하지 않게 되면 Debug.Log ()에 대한 호출을 제거해야 합니다.
  • 함수 관련
    1) GameObject.tag 대신 GameObject.CompareTag 사용하기
    2) Input.GetTouch 와 Input.touchCount 대신 Input.touches 사용하기
    3) Physics.SphereCastNonAlloc 대신 Physics.SphereCastAll 사용하기
  • 박싱으로 이어지는 함수 호출 제거
  • 코루틴 함수 관련
    1) StartCoroutine 을 사용할때 약간의 가비지가 발생한다.
    2) yield new return 에서 new 를 할때마다 가비지가 생성 된다.
  • foreach 문 자제
  • 수동으로 가비지컬렉터 실행 자제

그래픽 관련 최적화

  1. CPU 의 작업량을 줄이는 방법
    1) 가까이에 있는 오브젝트를 수동이나 Unity 의 드로우 콜 배칭을 활용해 결합합니다.
    2) 큰 텍스처 아틀라스에 개별적 텍스처를 넣는 등의 방법으로 오브젝트의 머티리얼 수를 줄입니다.
    3) 오브젝트가 여러 번 렌더링되는 요소(반사, 그림자, 픽셀 별 광원 등)를 덜 사용합니다.
  2. GPU 모델 지오메트리 최적화
    1) 필요 이상으로 삼각형을 사용하지 않습니다.
    2) UV 매핑의 경계 부분과 하드 에지(버텍스가 두 배)의 수를 가능한 적게 유지합니다.
  3. 조명(Lighting) 퍼포먼스
    1) 전역 조명을 베이크하고 라이트 매퍼를 사용
  4. GPU: 텍스처(Texture) 압축과 밉맵
    1) 3D 씬에서 사용되는 텍스처에 항상 밉맵 생성을 활성화
  5. 실시간 섀도우
    1) 실시간 섀도우 사용을 줄인다.

기타 최적화 방법

프로파일링을(기본 or 툴 사용) 해서 어디서 과부화가 걸리는지 확인

  1. 에셋 관련

    1) 텍스처

    • Read/Write enabled 플래그 비활성화 (기본 비활성화)
    • 밉맵(하지만 그래픽적인 차이가 좀 있음 그러므로 상황봐서)
    • 모든 텍스처를 압축

    2) 모델

    • Read/Write enabled 플래그 비활성화(기본 활성화)
    • 애니메이션화되지 않는 모델의 리그를 비활성화
    • 애니메이션화되는 모델에 게임 오브젝트 최적화 옵션 활성화
    • Optimize Game Objects 활성화
    • 가능한 한 메시 압축 사용
  2. 힙 메모리 관련

    • 오브젝트 풀링 사용
    • 박싱을 최대한 피해야 한다.
      ex) - enum 타입을 Dictionary 의 키로 사용하면 박싱이 발생한다.
    • Foreach 루프문 사용시 루프가 종료되는 시점마다 박싱이 발생했었다.(몇번 반복하든 foreach 문 하나당 한번씩)
      버전 업그레이드를 하면서 Foreach문의 박싱은 사라졌지만 CPU 성능에 차이가 있다.(결론은 아무튼 자제 하는게 좋다.)
    • 배열 기반 Unity API
      링크 : https://docs.unity3d.com/kr/2018.2/Manual/BestPracticeUnderstandingPerformanceInUnity4-1.html
  3. Resources 폴더사용 하지 말고 AssetBundle 을 사용하자.

  4. 기타

    • 부동 소수점보다 정수 연산의 속도가 빠르며, 부동 소수점 연산의 속도가 벡터, 매트릭스 또는 쿼터니언 연산보다 빠르다
    • Object.Find와 Object.FindObjectOfType의 사용을 모두 제거하는 것이 일반적으로 가장 좋다.(싱글톤은 예외)
    • 일차원 배열 사용, 다차원 배열이 필요한 경우 다차원 배열 말고 가변 배열을 사용
    • 적은 콜백 함수 사용 (Update, FixedUpdate, LateUpdate 같은)
    • 델리게이트 사용시 추가 제거가 자주 일어난다면 다른 데이터 구조 사용을 고려

게임을 더 빠르게 만드는 간단한 체크리스트

  • PC 용으로 빌드할 때(타겟 GPU 에 따라) 버텍스 수를 프레임당 200K 및 3M 아래로 유지합니다.
  • 빌트인 셰이더를 사용하는 경우 Mobile 이나 Unlit 의 카테고리에서 선택합니다.
    비모바일 플랫폼에서도 작동은 하지만 복잡한 셰이더를 단순화시키고 비슷하게 만듭니다.
  • 씬 별로 서로 다른 머티리얼의 수를 적게 유지하고 다른 오브젝트 간에 최대한 많은 머티리얼을 공유합니다.
  • 움직이지 않는 오브젝트에는 Static 프로퍼티를 설정하여, 정적 배칭과 같은 내부 최적화가 가능하도록 해야 합니다.
  • 다수 보다는 단일(가능하다면 방향성) pixel light 만 지오메트리에 영향을 주도록 해야 합니다.
  • 동적 조명을 사용하기보다 조명을 베이크해야 합니다.
  • 가능하면 압축 텍스처 사용하고 32 비트보다는 16 비트를 사용해야 합니다.
  • 가능하면 안개의 사용을 피해야 합니다.
  • 오클루전 컬링을 사용하여 오클루전이 많아 복잡한 정적 씬에서 드로우 콜과 가시적 지오메트리를 줄이는 데 활용합니다.
    오클루전 컬링을 염두에 두고 레벨을 디자인합니다.
  • 스카이박스를 써서 멀리 있는 지오메트리를 “진짜처럼 보이게” 합니다.
  • 멀티 패스 접근 방식 대신 픽셀 셰이더나 텍스처 컴바이너를 활용해 여러 텍스처를 혼합합니다.
  • 가능하면 half 정밀도 변수를 사용합니다.
  • 픽셀 셰이더에서 pow, sin, cos 등 복잡한 수학 연산의 사용을 최소화합니다.
  • 프래그먼트 별 텍스처 사용을 줄입니다.