티스토리 뷰

728x90

learn.unity.com/tutorial/fixing-performance-problems-2019-3#

 

Fixing Performance Problems - 2019.3 - Unity Learn

Once you've discovered a performance problem in your game, how should you go about fixing it? This tutorial discusses some common issues and optimization techniques for scripts, garbage collection, and graphics rendering.

learn.unity.com

빌드프로세스

코드가 정상적으로 동작하지 않는것을 알기위해 빌드 프로세스에 대해 알아야합니다.

 

Unity는 먼저 스크립트를 CIL(Common Intermediate Language)로 컴파일합니다. CIL은 다양한 네이티브코드 언어로 쉽게 컴파일 할 수있는 언어입니다. 그런 다음 CIL은 특정 대상장치에 대한 네이티브 코드로 컴파일됩니다. (AOT, Ahead of time compilation)또는 코드가 실행되기 직전에 대상장치 자체에서 컴파일합니다. (JIT, just in time compilation) 이는 대상 디바이스에 따라 다릅니다.

 

아직 컴파일되기 전의 코드를 소스코드라고 합니다.

대부분의 경우 잘 구조화되고 효율적인 소스코드는 잘 구성되고 효율적인 컴파일된 코드를 생성합니다. 

일부 소스코드가 더 효율적인 네이티브 코드로 컴파일되는 이유를 알면 효율적인 소스코드를 작성하는데 도움이 됩니다.

1. 일부 CPU명령은 다른 명령보다 실행하는데 더 많은 시간이 걸립니다.

2. 소스에서 단순한 작업이 실제 네이티브에서는 복잡한 경우가 있습니다.

 

소스와 같이 CIL로 컴파일된 코드를 managed code, 이것이 네이티브로 컴파일되면 managed runtime이라고 합니다.

managed runtime은 자동 메모리 관리 및 안전검사와 같은 작업을 처리하여 코드의 버그로 인해 crash가 아닌 exception을 발생하게 함.

 

cpu가 실행중인 엔진코드(c++/네이티브)와 managed code같에 전환(transition)될때 이러한 안전감사를 설정하기 위한 작업이 수행됨.

관리코드에서 엔진코드로 데이터를 다시 전달할 때 CPU는 관리되는 런타임에서 사용하는 형식의 데이터를 엔진코드에 필요한 형식으로 변환하는 작업을 수행합니다. 이를 마샬링(marshaling)이라고 합니다. 이 오버헤드는 비싸지는 않지만 비용이 존재한다는 것을 이해해야합니다.

낮은성능의 코드원인

코드가 제시간에 수행 되지 않는다는건, 런타임에 CPU에 너무 많은 명령을 생성하기 때문이라는 것.

1. 코드가 단순히 낭비적이거나 구조가 잘못되어있다는것.

2. 코드 구조가 잘 된것으로 보이나 불필요하게 다른 코드를 호출할때. (마샬링이 빈번할때 등)

3. 코드가 너무 까다로울때. (== 코드의 기능이 복잡하고 어려울때)

코드성능을 향상할때

게임성능문제가 코드때문이라고 확인이되면, 이를 해결하는 방법에 대해서는 신중하게 생각해야합니다.

문제의 기능이 이미 최적화한 것이고, 본질적으로 리소스가 많이 들수도 있습니다. 변경사항은 메모리 사용량을 증가시키거나 GPU로 분산할 수도 있습니다. 모든 최적화와 같이 빠르게 적용할수 있는 규칙은 없습니다.

게임을 프로파일링하고, 이슈의 성격을 이해하고, 다양한 솔루션을 실험하고 변경결과를 측정하는 수 밖에 없습니다.

효율적인 코드 작성

가능하면 코드를 루프밖으로 빼기

루프는 특히 중첩된 경우 비효율성이 발생하는 일반적인 장소입니다. 특히 코드가 게임의 많은 게임오브젝트에서 사용되는 경우 비효율성이 증가합니다.

// 간단한예.
void Update(){
 for(int f=0; f<myArray.Length; f++){
  if(exampleBool){
   ExampleAction(myArray[f]);
  }
 }
}

void Update(){
 if(exampleBool) {
  for(int f=0; f<myArray.Length; f++){
    ExampleAction(myArray[f]);
   }
  }
}

코드가 모든 프레임에서 실행하야하는지 고려해보자. Update()의 경우 매프레임 호출되므로 필요한 경우에만 실행되도록 Update()에서 코드를 빼는것은 성능 향상에 도움이 될 수 있습니다.

n프레임마다 코드실행

private int interval = 3;

void update(){
 if(Time.frameCount % interval == 0){
  ExampleExpensiveAction();
 } else if(Time.frameCount % 1 == 0){
  AnotherExampleExpensiveAction();
 }
}

무거운코드를 별도의 프레임에 분산해서 스파이크를 방지할 수 있도록. 매 프레임마다 발생시키기 보다는 분산시켜보자.

캐싱 Caching

코드에 대한 결과를 캐싱하여 재사용하자.

private Renderer m_renderer;

void Start(){
 m_renderer = GetComponent<Renderer>();
}

void Update(){
 Example(m_renderer);
}

올바른 데이터 구조의 사용

데이터구조에 대한 단일선택은 게임에 큰 영향을 미치지는 않지만, 이러한 컬렉션이 많이 포함된 데이터 기반게임에서는 이러한 선택이 성능에 영향을 미칩니다. 데이터의 강/약점을 이해하여 적당한 구조를 사용해야합니다.

MSDN 가이드 (C#의 데이터구조와 컬렉션)

비싼 UnityAPI 호출을 피하기

생각보다 비용이 많이드는 UnitAPI 호출의 몇가지 예.

 

SendMessage()

SendMessage(), BroadcastMessage()는 리플렉션을 사용하고 있어 무거운 함수입니다. 리플렉션은 컴파일타임이 아닌 런타임에 검사하는 방식입니다.

함수를 호출할 위치를 알고 있다만 직접참조하여 호출해야합니다. 그것을 모른다면, Events 또는 Delegates사용을 고려할수 있습니다.

 

Find()

Find()는 강력하지만 비용이 많이 듭니다. Unity가 메모리의 모든 게임오브젝트 및 컴포넌트를 룩업해서 찾습니다.

참조할 객체를 미리 연결하거나 캐싱하는것이 좋습니다.

 

Transfrom

transform의 position이나 scale을 변경하면 OnTransfromChanged 이벤트가 해당 transform의 모든 자식에게 전파됩니다. 즉, 자식이 많은 Transform은 상대적으로 비용이 많이 듭니다. 이러한 내부 이벤트수를 제한하려면 값을 변경할때 한번만 적용하도록 하는것이 바람직합니다. (예를 들어 x값을 계산후 z 값을 도출할때 매번 transform.positon에 넣지 말고 최종값만 position에 적용하는것이 좋습니다.)

 

transform.localpotion은 해당 transform에 저장되만, transform.position은 매번 worldposition을 계산해서 반환합니다.

 

Update()

update(), lateupdate()등의 이벤트함수는 숨겨진 오버헤드가 있습니다. 이러한 함수는 호출될 때마다 엔진코드와 관리코드간의 통신이 필요합니다. 그리고 이를 호출하기 전에 여러가지 안전검사를 수행합니다. 안전검사는 GameObject가 유효한 상태인지, 파괴된건지를 검사합니다. 이 오버헤드는 단일 호출에 대해서는 크지 않지만, 수천개의 monobehaviour에서는 문제가 될수 있습니다.

 

이러한 이유로 빈 이벤트함수는 낭비가 됩니다.

 

게임에 update()를 사용하는 mono가 많은 경우 오버헤드를 줄이기위해 코드를 다르게 구성하는것이 좋습니다.

blogs.unity3d.com/2015/12/23/1k-update-calls/?_ga=2.168008996.489364047.1616387768-1507229146.1546094146

 

Vector2와 Vector3

vector math 연산은 일반적인 float, int math 연산보다 복잡합니다.

update()내 중첩루프에서 vector math연산을 자주 수행하는 경우 cpu에 불필요한 작업을 생성할 수 있습니다. 이런경우 int,float계산을 수행하여 성능을 절약할 수 있습니다.

예를들어 vector3.magnitude와 vector3.distance같이 광범위하고 빈번하게 크기/거리를 사용하는 대신 vector3.sqrMagnitude같이 상대적으로 저렴한 계산을 할 수 있습니다.

 

Camera.main

camera.main은 편리하지만, 씬뒤에서 find()와 유사한 동작을 수행합니다.

실행이 필요할 때만 코드 실행하기

종종 성능문제를 해결하는 방법은 고급기술을 쓰지 않고, 단순히 처음부터 있을 필요가 없는 코드를 제거하는것이 빠릅니다.

Culling

unity에는 객체가 카메라의 프로스텀에 있는지 확인하는 코드가 포함되어 있습니다. 카메라 프로스텀 내에 있지 않으면 렌더링관련 코드가 실행 되지 않습니다. (frustum culling)

 

스크립트의 코드에도 유사한 접근을 취한다고 생각해봅시다. 객체의 시각적 상태와 관련된 코드가 있을때, 플레이어가 객체를 볼수 없을때 코드를 실행할 필요가 없습니다.

void Update(){
 UpdateTransformPosition();
 
 if(m_renderer.isVisible){
  UpdateAnimation();
 }
}

플레이어가 객체를 볼수 없을때 코드를 비활성화하는 방법은 몇가지가 있습니다. 씬의 특정 오브젝트가 게임의 특정지점에서 보이지 않는것을 알고 있다면, 수동으로 비활성화 할 수 있습니다. 활실하지 않고 가시성을 계산해야할 경우에는 대략적인 계산, OnBecameInvisible(), OnBecameVisible()과 같은 함수 또는 레이캐스트를 사용할 수 있습니다.

LOD

LOD의 방식을 소스에도 고려해봅시다. 예를들어 플레이어와의 거리에 따라 값비싼작업의 수준을 조절해볼 수 있습니다.

Unity의 CullingGroupAPI를 사용하면 unity의 LOD시스템과 연결하여 코드를 최적화 할수 있습니다.

가비지컬렉션의 영향

코드가 메모리를 사용하는 방식에 따라 가비지 수집의 빈도와 CPU비용이 결정되므로 가비지수집의 작동방식을 이해하는것은 중요합니다.

Unity의 메모리관리에 대한 간략한 소개

Unity가 자체엔진코드를 실행할때 메모리를 관리하는 방식을 수동메모리관리 라고 합니다. 이는 엔진코드가 메모리 사용방법을 명시적으로 명시해야함을 의미합니다. 수동메모리관리는 가비지콜렉팅을 안하므로 여기서는 다루지 않습니다.

 

Unity가 스크립트의 메모리를 관리하는 방식은 자동메모리관리 라고합니다. 이는 우리 코드가 메모리를 세부적으로 관리하는 방법을 명시적으로 할 필요가 없음을 의미합니다. Unity에서 이를 처리합니다.

 

가장 기본적인 수준에서 unity의 자동메모리관리는 다음과 같이 작동합니다.

  1. Unity는 스택과 힙(managed heap이라고도함)의 두가지 메모리풀에 엑세스 할 수 있습니다. 스택은 작은 데이터조각의 단기저장에 사용되며 힙은 장기저장 및 큰 조각에 사용됩니다.
  2. 변수가 생성되면 Unity는 스택또는 힙에서 메모리 블록을 요청합니다.
  3. 변수가 범위내(access scope)에 있는한 할당된 메모리는 계속 사용됩니다. 우리는 이 메모리가 할당되었다고 말합니다. 스택메모리에 있는 변수를 스택의 객체로, 힙 메모리에 있는 변수를 힙의 객체로 설명합니다.
  4. 변수가 범위를 벗어나면 메모리가 더 이상 필요하지 않으며 원래 풀로 반환될 수 있습니다 .메모리가 풀로 반환되면 메모리가 할당해제 되었다고 말합니다. 스택의 메모리는 참조하는 변수가 범위를 벗어나는 즉시 할당해제됩니다. 그러나 힙메모리를 이 시점에서 할당해제되지 않고,참조하는 변수가 범위를 벗어나는 경우에도 할당된 상태로 유지됩니다.
  5. 가비지 컬렉터는 사용되지 않은 힙메모리를 식별하고 할당을 취소합니다. 그리고 주기적으로 실행되어 힙을 정리합니다.

스택 할당 및 해제시 발생하는 일

스택할당 및 해제는 빠르고 간단합니다. 이는 스택이 짧은 시간 동안 작은 데이터를 저장하는 데만 사용되기 때문입니다. 할당및해제는 항상 예측가능한 순서로 발생하며 예측가능한 크기입니다.

 

element는 엄격한 순서로 추가 및 제거할 수 있습니다. 이 단순함과 엄격함이 이를 매무 빠르게 만듭니다. 변수가 스택에 저장될때 해당 메모리는 단순히 스택의 "끝"에서 할당됩니다. 스택변수가 범위를 벗어나면 해당 변수를 저장하는데 사용된 메모리는 재사용을 위해 즉시 스택으로 반환됩니다.

힙 할당중에 발생하는 일

힙은 스택보다 복잡한데, 이는 힙을 사용하여 장/단기 데이터와 다양한 유형 및 크기의 데이터를 모두 저장할 수 있기 때문입니다. 할당 및 할당 해체가 항상 예측 가능한 순서로 발생하는 것은 아니며 매우 다른 크기의 메모리 블록이 필요할 수 있습니다.

 

힙변수가 생성되면 다음 단계가 수행됩니다.

  1. Unity는 힙에 충분한 여유메모리가 있는지 확인합니다. 힙이 충분하면 변수에 대한 메모리가 할당됩니다.
  2. 힙에 충분하지 않으면, Unity는 사용하지 않는 힙메모리를 제거하기위해 가비지콜렉션을 트리거 합니다. 이것은 느린작업일 수 있습니다. 이제 힙에 여유가 생기면 변수에 대한 메모리가 할당됩니다.
  3. 가비지콜렉션 이후에도 모자르면 Unity는 힙 메모리양을 늘립니다. 이것은 느린작업일 수 있습니다. 그런다음 변수에 대한 메모리가 할당됩니다.

힙 할당은 특히 가비지콜렉션을 수행하고 힙을 확장해야하는 경우 느려질 수 있습니다.

가비지콜렉션때 생기는 일

  1. 가비지콜렉션은 힙의 모든 개체를 검사합니다.
  2. 가비지콜렉션은 모든 현재 개채참조를 검색하여 힙의 개체가 여전히 범위내에 있는지 확인합니다.
  3. 더이상 범위에 없는 개체는 삭제 플레그가 지정됩니다.
  4. 플래그가 지정된 개체가 삭제되고 할당된 메모리가 힙으로 반환됩니다.

가비지콜렉션은 비용이 많이드는 작업일 수 있습니다. 힙에 객체가 많은 수록 더 많은 작업을 수행해야하고, 코드에서 객체참조가 많을수록 더 많은 작업을 수행해야합니다.

가비지콜렉션이 발생하는 때

  1. 힙메모리가 부족할때
  2. 때때로 자동으로 (플랫폼에 따라 빈도가 다름)
  3. 수동으로 실행되었을때

가비지콜렉션 문제

가장 명백한 문제는 가비지콜렉션이 수행시간이 오래걸릴 수 있다는 것입니다.

그리고 가비지콜렉션이 불편한 시간에 실행될 수도 있다는 것입니다. CPU가 게임퍼포먼스에 중요한부분에서 수행중에 GC가 발생하면 추가 오버헤드로 프레임이 떨어질 수 있습니다.

그리고 메모리파편화문제입니다. 

 

이를 피하려면 힙에 할당되는 부분을 알아야합니다.

스택과 힙에 할당되는 것은 무엇인가?

Unity에서 값유형의 로컬변수는 스택에 할당되고 나머지는 힙에 할당됩니다.

void Example(){
 int localInt = 5; // 스택에 할당
 List localList = new List(); // 힙에 할당
}

가비지콜렉션의 영향줄이기

1. 가비지콜렉션이 실행되는 걸리는 시간을 줄이기

2. 가비지콜렉션이 실행되는 빈도를 줄이기

3. 가비지콜렉션을 의도적으로 트리거하여 미리 수행하기.

 

이를 염두하면 아래와 같이 수행할 수 있습니다.

1. 게임을 구성할때 힙할당과 개체참조를 줄입니다. 힙객체가 적고, 검사할 참조가 적다는건 가비지컬렉션시 수행할 내용이 적어집니다.

2. 성능이 중요한 시간에 힙할당 및 할당해제 빈도를 줄입니다.

3. 가비지콜렉션 및 힙확장시간을 측정하여 예측가능하고 편리한 시간에 발생하도록 합니다. (로딩때 등)

가비지를 줄이는 전략

캐싱

void OnTriggerEnter(Collider other){
 var allRenderers = FindObjectsOfType<Renderer>(); // 이경우 매 이벤트 마다 힙할당 및 해제가 발생
 ExampleAction(allRenderers);
}

// allRenderers에 대한것을 캐싱해서 힙할당/해제의 빈도를 줄인다.

빈번하게 호출하는 함수에 힙할당을 줄이기

위쪽에 있는 update시 n프레임당 한번만 수행하는것 처럼 호출횟수를 줄이거나, 힙할당해야하는 때를 꼭 필요할때만 하도록 하자.

예를들어 x값을활용하여 많은 힙을할당하는 함수가 있다면, x값이 변경되는때에만 해당 함수를 호출하는 식으로.

private float 이전X위치;

void update(){
 float 현재X위치 = transform.position.x;
 if(현재X위치 != 이전X위치){
  가비지를많이발생하는함수(현재X위치);
  이전X위치 = 현재X위치;
 }
}

오프젝트풀링의 사용

일반적으로 객체를 비활성화하고 재활성화 하는것보다 인스턴스화하고 파괴하는것은 더 많은 비용이 듭니다.

Awake(), Start()에서 GetComponent()호출과 같은 코드가 포함된 경우 특히 그렇습니다.

오브젝트풀링은 객체의 인스턴스를 만들고 제거하는 대신 객체를 일시적으로 비활성화하고 필요에 따라 재활용하는 방식입니다.

메모리 사용량을 관리하는 기술로 알려져 있지만, 오브젝트풀링은 과도한 CPU사용량을 줄이기도 합니다.

불필요한 힙할당의 일반적인 원인

String

C#에서 문자열은 문자열의 "값"을 포함하는것처럼 보이지만 값 유형이 아닌 "참조"유형입니다.

즉, 문자열을 만들고 버리면 가비지가 생깁니다.

그리고 C#의 무자열은 변경할 수 없습니다. 즉, 처음 생성된 후에는 해당값을 변경할 수 없습니다. 문자열을 조작할 때 마다 Unity는 업데이트된 값으로 새 문자열을 만들고 이전 문자열을 버립니다.

 

몇가지 규칙으로 가비지를 최소화해봅시다.

  1. 불필요한 문자열 생성을 줄여야합니다. 동일한 문자열 값을 두번이상 사용하는 경우 문자열을 한번 생성하고 값을 캐싱합니다.
  2. 불필요한 문자열 조작을 줄여야합니다. 예를 들어 자주 업데이트되고 연결된 문자열을 포함하는 텍스트컴포넌트가 있는 경우 두개의 텍스트 컴포넌트분리를 고려할 수 있습니다. (ex. "TIME:" "10:10"와 같이 Text컴포넌트를 두개로 나눈다는 이야기)
  3. 런타임에 문자열을 빌드할때 StringBuilder를 사용합시다. StringBuilder는 할당없이 무자열을 빌드하도록 설계되어 있으며 복잡한 문자열을 연결할때 생성되는 가비지양을 줄입니다.
  4. Debug.Log의 경우도 필요하지 않으면 제거해야합니다. Debug.Log()의 호출은 내용이 비어있더라도 게임의 모든빌드에서 계속 실행되며, 하나이상의 문자열을 만드므로 가비지가 생깁니다.

Unity함수 호출때의 가비지발생

일부 Unity 함수 호출은 힙할당을 생성하므로 불필요한 가비지 생성을 방지하기 위해 주의해야합니다.

// 매 루프마다 myMesh.normals에서 힙할당이 발생
for(int i = 0; i<myMesh.normals.Length; i++){
 var normal = myMesh.normals[i];
}

// 아래와 같이 캐싱
var meshNormals = myMesh.normals;
for(int i = 0; i<meshNormals.Length; i++){
 var normal = meshNormals[i];
}

GameObject.name또는 tag함수는 둘다 문자열을 반환하는 접근자입니다. 즉, 가비지가 생성됨.

이를 피하려면 gameObject.CompareTag(playerTag); 같이 비교함수를 사용해야합니다. (gameObject.tag == playerTag 말고)

박싱(Boxing)

박싱은 참조유형변수 대신 값유형변수를 사용할 때 발생하는 일입니다. 박싱은 일반적으로 int또는 float와 같은 값유형변수를 Object.Equals()와 같은 개체 매개 변수가 있는 함수에 전달될때 발생합니다.

값형식변수가 boxed되면 Unity는 값형식변수를 래핑하기위해 힙에 임시 System.Object를 만들기에 임시개채가 삭제되면 가비지가 생성됩니다.

코루틴

StartCoroutine()을 호출하면 Unity가 코루틴을 관리하기위해 인스턴스를 생성해야하는 클래스때문에 소량의 가비지가 발생합니다. 이 때문에 StartCoroutine()는 많이부르거나 성능이 우려되는때는 제한되야합니다. 성능이 중요한 시간에는 미리 시작해야합니다.

그리고 중첩된 코루틴을 사용할때도 주의해야합니다.

 

yield return 0; 은 '0'인 int가 박싱되어 가비지가 생성되지만 yield return null;은 생성되지 않습니다.

yield return new WaitForSeconds(1f); 또한 힙할당이므로 미리 생성해서 재활동하는것이 좋습니다.

foreach

foreach는 unity5.5이전에는 박싱에 대한 가비지가 생성되었지만 이후 버전에서는 수정되었습니다.

 

함수참조

함수에 대한 참조는 익명메서드(delegate)를 참조하든 명명된메서드를 참조하든 Unity에서는 참조형식입니다. (힙할당이 발생함) 익명 메서드를 클로저로 변환하면 (익명메서드가 생성당시 범위의 변수에 액세스 할수 있음) 메모리사용량과 힙할당수가 크게 증가합니다.

public void Test3()
{
    int key = 10;

    Action<string> print = delegate (string msg)
    {
        string str = key + msg;
        Console.WriteLine(str);
    };

    print("A");  // 10A
}

LINQ와 정규식

linq와 정규식은 모두 배후에서 발생하는 박싱으로 인해 가비지를 생성합니다. 성능이 문제되는 곳에서는 모두 사용하지 않는것이 좋습니다.

가비지컬렉션의 영향을 최소화하기위한 코드구조

코드가 힙할당을 생성하지 않더라고 가비지콜렉터의 워크로드에 추가될 수 있습니다.

검사할 필요가 없는 항목이 있는 경우가 그 예인데, 구조체는 값형식이지만 참조형식변수를 포함하면 가비지콜렉터는 전체구조체를 검사해야합니다.

public struct ItemData{
 public string name; // 요놈
 public int cost;
 public Vector3 position;
}

그리고 불필요한 개채참조를 사용할 때 입니다. 가비지컬렉터가 힙개체에 대한 참조를 검색할때 코드에서 현재 개채참조를 모두 검사해야합니다. 코드에서 개체참조가 적다는건 힙의 총개체수를 줄이지 않더라도 수행할 작업이 줄어든다는 것을 의미합니다.

public class DialogData{
 private DialogData nextDialog;
 public DialogData GetNextDialog(){
  return nextDialog;
 }
}

유저가 특정 dialog를 보고있을때 next도 있으므로 nextDialog의 참조도 들고 있기에 해당 참조고 검사합니다.

(이는 dialog ID (:int)로 대체하여 해결가능합니다.)

이런것들은 사소하지만 다른객체에 대한 참조를 보유하는 객체가 많으면 힙의 복잡성이 늘어나므로 고민해야합니다.

수동으로 가비지 콜렉션

힙메모리가 할당되었지만 정리할 필요가 있고, 플레이에 영향을 미지치않을 때, 강제로 가비지콜렉션을 요청할 수 있습니다.

System.GC.Collect();

그래픽렌더링 최적화

Unity가 프레임을 렌더링할때 배우에서 어떤일이 발생하는지, 렌더링시 발생할 수 있는 성능문제의 종류 및 렌더링고 ㅏ관련된 성능 문제를 해결하는 방법에 대해 알아보자.

렌더링에 대한 간략한 소개

가장 기본적인 수준에서 렌더링은 다음과 같이 설명할 수 있습니다.

1. CPU는 무엇을 그려야하고 어떻게 그려야하는지 결정합니다.

2. CPU는 GPU에게 명령을 보냅니다.

3. GPU는 CPU의 명령에 따라 그림을 그립니다.

 

효율적인 렌더링은 정보흐름을 유지하는 것에 달려있습니다.

렌더링되는 모든 프레임에 대해 CPU는 다음작업을 수행합니다.

  1. CPU는 씬의 모든 객체를 확인하여 렌더링 여부를 결정합니다. 개체는 특정 기준을 충족하는 경우에만 렌더링됩니다 .예를들어 boundbox의 일부는 카메라의 뷰프로스텀 내에 있어야 합니다. 렌더링 되지 않는 오브젝트는 컬링 되었다고 합니다.
  2. CPU는 렌더링될 모든 개체에 대한 정보를 수집하고 데이터를 DrawCall이라는 명령으로 정렬합니다. DrawCall에는 단일메시에 대한 데이터와 해당메시를 렌더링하는 방법이 포함됩니다. 예를 들어 어떤 텍스처를 사용하는지, 특정상황에서 설정을 공유하는 개체는 동일한 DrawCall로 결합될 수 있습니다. 서로 다른 개채의 데이터를 동일한 DrawCall로 결합하는 것을 Batching이라고 합니다.
  3. CPU는 각 DrawCall에 대한 batch라는 데이터 패킷을 만듭니다. batch에는 때때로 drawcall이외의 데이터가 포함될 수 있지만 이러한 상황은 일반적성능 이슈에 기여할 가능성이 낮으므로 여기서는 생략합니다.

DrawCall이 포함된 모든 Batch에 대해 CPU는 이제 다음을 수행합니다.

  1. CPU는 RenderState라고 통칭되는 여러변수를 변경하기 위해 GPU에 명령을 보낼수 있습니다. 이 명령을 SetPassCall이라고 합니다. SetPassCall은 GPU에 다음 메시를 렌더링하는데 사용할 설정을 알려줍니다. SetPassCall은 렌더링 할 다음 메시가 이전 메시에서 RenderState를 변경해야하는 경우에만 전송됩니다.
  2. CPU는 DrawCall을 GPU로 보냅니다. DrawCall은 가장 최근의 SetPassCall에 정의된 설정을 사용하여 지정된 메시를 렌더링 하도록 GPU에 지시합니다.
  3. 특정상황에서 Batch에 대해 두번이상의 패스가 필요 할 수 있습니다. 패스는 쉐이더코드의 부분이며 새 패스를 사용하려면 RenderState를 변경해야합니다. Batch의 각 Pass에 대해 CPU는 새로운 SetPassCall을 전송한후 DrawCall을 다시 전송해야합니다.

그동한 GPU는 다음작업을 수행합니다.

  1. CPU에서 전송된 순서대로 GPU는 작업을 처리합니다.
  2. 현재 작업이 SetPassCall인 경우 GPU는 RenderState를 업데이트 합니다.
  3. 현재 작업이 DrawCall이면 GPU가 메시를 렌더링합니다. 이것은 쉐이더 코드의 별도 섹션에 의해 정의된 단계적으로 발생합니다 .렌더링의 이 부분은 복잡하여 다루지 않지만 버텍스쉐이더라는 코드섹션이 GPU에 메시의 버텍스를 처리하는 방법을 알려주고 Fragment쉐이더는 개별 픽셀을 그리는 방법을 알려줍니다.
  4. 이 프로세스는 CPU에서 전송된 모든 작업이 GPU에서 처리될때까지 반복됩니다.

렌더링문제의 유형

렌더링에 대해 이해해야할 가장 중요한 점이 이것입니다. CPU와 GPU는 프레임을 렌더링하기 위해 모든 작업을 완료해야합니다. 이러한 작업중 하나라도 완료하는데 오래걸리면 프레임렌더링이 지연됩니다.

 

렌더링문제에는 두가지 근본적인 원인이 있습니다.

1. 비효율적인 파이프라인으로 발생합니다. 비효율적인 파이프라인은 렌더링 파이프라인의 하나이상의 단계를 완료하는데 너무 오래걸리고 원활한 데이터흐름을 방해할때 발생합니다. (즉, 병목현상)

2. 단순히 파이프라인을 통해 너무 많은 데이터를 푸시하려고 할때 발생합니다. 가장 효율적인 파이프라인에서도 한프레임에 처리할 수 있는 데이터양에 제한이 있습니다.

 

CPU가 렌더링 작업을 수행하는데 너무오래걸려서 렌더링이 지연되는 경우는 CPU 바운드라고 하고, GPU가 렌더링 수행에 오래걸리면 GPU 바운드라고 합니다.

 

그래픽렌더링 최적화를 하기전에 CPU바운드 인지 GPU바운드인지 알아야합니다

CPU 바운드인 경우

일반적으로 프레임을 렌더링하기 위해 CPU가 수행해야하는 작업은 세가지 범주로 나뉩니다.

1. 무엇을 그려야하는지 결정

2. GPU 명령 준비

3. GPU에 명령 보내기

 

이러한 광범위한 범주에는 많은 개별작업이 포함되면 이러한 작업은 여러스레드에서 수행될수 있습니다. (멀티스레드)

 

Unity의 렌더링 프로세스는 세가지 유형의 스레드가 있습니다.

- 메인스레드 : 일부 렌더링 작업을 포함하여 게임에 대한 대부분의 CPU작업이 발생하는 곳입니다.

- 렌더링스레드 : GPU에 명령을 보내는 특수 스레드

- Worker스레드 : 각각 컬링 또는 메시스키닝과 같은 단일 작업을 수행합니다.

어떤 스레드가 어떤 작업을 수행하는지는 게임의 설정과 게임이 실행되는 디바이스에 따라 다릅니다. 해서 대상 디바이스에서 프로파일링하는것이 중요합니다.

 

멀티스레드렌더링은 복잡하고 하드웨어에 따라 다르기에 성능향상을 시도하기전에 어떤 작업이 게임의 CPU바인딩을 유발하는지 이해해야합니다. 컬링이 문제인지 GPU에 명령을 보내는게 문제인지에 따라 해결이 다르기에.

Graphics Jobs 옵션

이 옵션을 활성화하여 Unity가 그래픽 작업(렌더 루프)을 다른 CPU 코어에서 실행되는 Worker스레드로 오프로드 하도록 지시합니다. 이는 Camera.Render종종 병목 현상이 발생하는 메인 스레드 에서 소요되는 시간을 줄이기 위함입니다.

해당 옵션을 활성화/비활성화 하며 테스트가 필요합니다.

GPU에 명령보내기가 문제일때

GPU에 명령을 보내는데 걸리는 시간은 게임이 CPU바운드되는 가장일반적인 이유입니다.

GPU에 명령을 보낼때 가장무거운건 SetPassCall입니다. 이 SetPassCall의 횟수를 줄이는 것이 성능을 향상시키는 가장 좋은 방법일것입니다.

 

SetPassCall와 Batch와의 관계

1. 배치수를 줄이거나 더 많은 개체가 동인한 RenderState를 공유하도록 하면 대부분 SetPassCall이 줄어듭니다.

2. SetPassCall을 줄이면 대부분의 경우 CPU성능이 향상됩니다.

 

Batch수를 줄여도 SetPassCall이 줄어들지않으면 자체적으로 성능향상으로 이어질 수 있습니다. 이는 CPU가 동일한 양의 메시데이터를 포함하더라도 여러배치보다 단일 배치를 더 효율적으로 처리할 수 있기 때문입니다.

 

일반적으로 배치및 SetPassCall을 줄이는 세가지 방법이 있습니다.

첫째. 랜더링되는 개채수 줄이기

  1. 단순히 씬에서 보이는 물체의 수를 줄이는것이 효과적인 해결책이 될 수 있습니다.
  2. 카메라의 FarClipPlane 속성을 사용하여 카메라의 그리기 거리를 줄일 수 있습니다. 먼거리의 물체를 숨기는 데는 안개를 사용할 수 있습니다.
  3. 거리에 따라 오브젝트를 숨기를 것 보다 세밀한 접근방식을 위해 LayerCullDistance 속성을 사용하여 별도의 레이어에 있는 오브젝트에 대한 사용자 지정컬링거리를 제공할 수 있습니다. 이방법은 작은 세부장식이 많은 경우 유용할 수 있습니다.
  4. 오클루전컬링을 사용하여 다른 개체에 의해 숨겨진 개체의 렌더링을 비활성화 할 수 있습니다. 다만 모든 씬에 적합하지 않으며 추가 CPU오버헤드를 유발할 수 있고 설정이 복잡할 수 있지만, 일부 씬에서는 성능을 크게 향상할 수 있습니다. 오클루전컬링 베스트 프렉티스가 도움이 될 수 있습니다. 혹은 플레이어가 볼수 없는 오브젝트를 비활성화하는것의 자체적인 오클루전컬링도 도움이 됩니다.

둘째. 각 개체를 렌더링해야하는 횟수 줄이기

실시간조명, 그림자 및 반사는 개체가 여러번 렌더링되어 성능에 큰 영향을 미칠 수 있습니다.

1. 동적라이팅은 무겁습니다. 씬에 배경과 같이 움직이지 않는 오브젝트가 포함된 경우 베이킹을 사용하여 라이팅을 미리 계산합시다.

2. 실시간그림자도 무겁습니다. 품질설정을 조정하여 성능을 확인합니다.

3. 반사프로브는 사실적인 반사를 생성하지만 Batch측면에서 비용이 많이 들수 있습니다. 성능이 우려되는 곳에서 반사프로브를 최소화하고 사용되는 곳에서는 최대한 최적화하는것이 좋습니다.

셋째. 개체를 더 적은 Batch로 결합

특정조건이 충족되면 Batch에 여러 개체에 대한 데이터가 포함될 수 있습니다. 배칭에 적합하려면 객체는 다음을 충족해야합니다.

1. 동일한 인스턴스와 동일한 머티리얼

2. 동일한 머티리얼 설정 (ex. 텍스쳐, 쉐이더 및 쉐이더 파라미터)을 갖기.

 

적격한 개체를 일괄처리하는 방법에는 몇가지 방법이 있습니다.

  1. Static Batching은 Unity가 움직이지 않는 근처의 적격개체를 일괄처리할 수 있도록 하는 기술입니다. (메뉴얼참고) 다만, static batching은 메모리 사용량을 증가시킬수 있으므로 프로파일링이 필요합니다.
  2. Dynamic Batching은 Unity가 이동여부에 관계없이 적합한 개체를 일괄처리할 수 있는 기술입니다. (메뉴얼참고) 다만, 이는 CPU시간을 더 소모할 수 있습니다.
  3. Unity UI 배칭도 고려합니다.
  4. GPU 인스턴싱은 많은 수의 동일한 객체를 매우 효율적으로 일괄처리할 수 있는 기술입니다. 사용에는 제한이 있으며 특정하드웨어에서만 지원하지만, 동일한 개체가 한번에 여러개있으면 기술의 이점을 얻을 수 있습니다. (메뉴얼참고)
  5. 텍스쳐아틀라스는 여러텍스터를 하나의 큰텍스쳐로 결합하는 기술입니다.
  6. 에디터나 런타임에 코드를 통해 동일한 재질과 텍스처를 공유하는 메시를 수동으로 결합할 수 있습니다 .이런 방식으로 메시를 결합할때 그림자/조명/컬링이 여전히 오브젝트별 수준에서 작동한다는점을 알고 있어야합니다. 즉, 메시결합으로 인한 성능향상은 컬링되지 않음과 상쇄될 수 있습니다.
  7. 스크립트에서 Render.material에 접근할때 매우 주의해야합니다. 이렇게되면 머티리얼이 복제되고 새 카피에 대한 참조가 반환됩니다. 이렇게 하면 렌더러가 더이상 동일한 재질 인스턴스에 대한 참조가 없기 때문에 렌더러가 배치의 일부인 경우 배치가 중단됩니다. 스크립트에서 배치된 객체의 머티리얼에 접근하려면 Renderer.sharedMaterial을 사용해야합니다.

Culling, Sorting과 batching

컬링은 그 자체로 비용이 많이 들지 않지만 불필요한 컬링을 줄이면 성능에 도움을 줄 수 있습니다. 렌더링되지 않는 레이어에 있는 모든 활성씬개체에 대해 카메라마다 개채당 오버헤드가 있습니다. 이를 줄이려면 사용하지않는 카메라를 비활성화하고 현재 사용하지 않는 렌더러를 비활성화해야합니다.

 

배칭은 GPU로 명령을 보내는 속도를 크게 향상시킬 수 있지만 원치않는 오버헤드를 줄 수도 있습니다. 배칭이 게임의 CPU바운드에 기여하는 경우 게임에서 수동 또는 자동배치작업의 수를 제한할 수 있습니다.

Skinned Meshes

SkinnedMesh를 렌더링하는 것은 무거울 수 있습니다. 해당 mesh가 cpu 바운드에 기여하면 몇가지 시도할 사항이 있습니다.

  1. 실제 애니메이션을 사용하지 않는데 skinned mesh를 사용했는지 확인
  2. 일정시간만 애니메이션 하는 경우 (ex. 시작시에만 혹은 카메라의 특정거리내에 있을때만) 메시를 MeshRenderer로 교체합니다. (SkinnedMesh.BakeMesh)
  3. 스킨드메시를 사용하는 애니메이션 캐릭터 최적화에 대한 메뉴얼, 스킨드메쉬렌더러 메뉴얼에는 성능개선에 대한 조언이 있습니다. 그리고 메쉬스키닝 비용이 Vertex당 증가한다는 점도 염두해야합니다. 따라서 적은수의 Vertex를 사용하면 수행해야하는 작업의 양이 줄어듭니다.
  4. 특정플랫폼에서는 스키닝을 GPU에서 처리할 수 있습니다. (PlayerSetting/ComputeSkinning)

GPU바운드인 경우

FillRate

Fillrate는 CPU가 초당화면에 렌더링할 수 있는 픽셀수를 나타냅니다. 게임이 fillrate에 의해 제한된다면, 이는 게임이 GPU가 처리할 수 있는것보다 더 많은 픽셀을 프레임당 그리려고 한다는것을 의미합니다.

 

Fillrate로 인해 게임이 GPU바인딩되는지 확인하는것은 간단합니다.

1. 게임을 프로파일링하고 GPU시간을 기록합니다.

2. 플레이어설정에서 디스플레이해상도를 낮춥니다.

3. 게임을 다시 프로파일링합니다. 성능이 향상되었다면 fillrate문제일 가능성이 높습니다.

 

Fillrate문제일 경우 몇가지 접근방식이 있습니다.

  1. fragment쉐이더가 비효율적인지 확인합니다. 복잡한 fragment쉐이더는 일반적인 원인입니다.
  2. 게임이 내장쉐이더를 같이 사용하는 경우, 원하는 시각효과를 위해 가능한 가장 간단하고 최적화된 쉐이더를 사용하는것을 목표로 해야합나다. 예를 들어 내장 모바일쉐이더는 고도로 최적화되어있습니다. 그것을 사용하여 품질에 영향을 많이 주는지 확인해야합니다.
  3. 게임오브젝트가 Unity의 Standard Shader를 사용하는 경우 Unity가 현재 머티리얼 설정을 기반으로 이 쉐이더를 컴파일한다는 사실을 이해하는것이 중요합니다. 현재 사용중인 기능만 컴파일 됩니다. 즉, 디테일맵과 같은 기능을 제거하면 훨씬 덜 복잡한 fragment쉐이더가 생성되어 성능에 크게 도움이 될 수 있습니다.
  4. 프로젝트에서 커스텀 쉐이더를 사용할 경우 가능한 최적화하는것을 목표로 해야합니다. (유니티메뉴얼)
  5. 오버드로우는 같은 픽셀이 여러번그려진다는 용어입니다. 이것은 개체가 다른 개채위에 그려지고 fillrate문제에 크게 기여할때 발생합니다. 오버드로우를 이해하려면 Unity가 씬에서 오브젝트를 그리는 순서를 이해해야합니다. 개체의 쉐이더는 일반적으로 개체가 있는 Render queue를 지정하여 그리기순서를 결정합니다. Unity는 이 정보를 사용하여 엄격한 순서로 개체를 그립니다. 또한 다른 렌더큐의 개체는 그리기전에 다르게 정렬됩니다. 예를들어 Unity는 오버드로우를 최소화하기위해 Geometry큐에서 항목을 앞뒤로 정렬하지만, 필요한 시각적 효과를 얻기위해 transparent큐에서 객체를 앞뒤로 정렬합니다. 이러한 연속정렬방식은 실제로 transparent큐객체에 대한 오버드로우를 최대화하는 효과가 있습니다. 과도한 오버드로우의 원인은 투명머티리얼, 최적화되지 않은 파티클, 겹치는 UI요소이므로 최적화하거나 줄이는 실험을 해야합니다.
  6. PP의 사용도 fillrate 이슈에 크게 기여할 수 있습니다.

Memory bandwidth

메모리 대역폭은 GPU가 전용메모리에서 읽고쓸수있는 속도를 나타냅니다. 게임이 메모리대역폭에 의해 제한되는 경우 이는 일반적으로 GPU가 빠르게 처리하기에는 너무 큰 텍스쳐를 사용하고 있음을 의미합니다.

 

메모리대역폭이 문제인지 확인하기위해 다음을 수행할 수 있습니다.

1. 프로파일링하여 GPU시간을 기록합니다.

2. 퀄리티세팅에서 텍스쳐품질을 줄입니다.

3. 다시 프로파일링할때 GPU시간이 향상되면 메모리대역폭이 문제일 수 있습니다.

 

메모리대역폭문제일 경우

  1. 텍스쳐압축은 디스크와 메모리모두에서 텍스처크기를 크기줄일 수 있는 기술입니다.
  2. 밉맵은 Unity가 먼 물체에 사용할 수 있는 저해상도 버전의 텍스처입니다. 씬에 카메라에서 멀리 떨어진 물체가 포함된경우 밉맵을 사용하여 메모리 대역폭 문제를 완화할 수 있습니다.

Vertex Processing

정점처리는 GPU가 메시에서 각 정점을 렌더링하기위해 수행해야하는 작업을 나타냅니다. 정점처리비용은 렌더링해야하는 정점수와 각 정점에서 수행해야하는 작업수의 두가지 영향을 받습니다.

 

게임이 GPU바운드이고 fillrate, memory bandwidth문제가 아니라면 정점처리가 문제의 원인일 가능성이 높습니다.

 

정점수 또는 정점에서 수행하는 작업수를 줄이기위해 고려할 수 있는 몇가지 접근방식이 있습니다.

  1. 불필요한 메시 복잡성을 줄이는것을 목표로 해야합니다. LOD메시를 사용하거나 아트쪽에서 정점수를 줄여야합니다.
  2. 노멀매핑을 사용해서 디테일 품질을 올립니다. 다만 약간의 GPU오버헤드가 있습니다.
  3. 게임의 메시가 노멀매핑을 안하는 경우 메시임포트설정에서 해당메시에 대한 정점탄젠트사용을 비활성화할 수 있습니다. 이렇게하면 각 정점에 대해 GPU로 전송되는 데이터의 양이 줄어듭니다.
  4. LOD를 사용하여 렌더링할 vertex수를 줄입니다.
  5. vertex쉐이더의 복잡성을 줄입니다.
  6. 가능하면 내장모바일쉐이더로 해봅니다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31