티스토리 뷰

Study/CS

유니티 최적화 기법들

jhin.lee 2019. 2. 11. 11:08

최조 작성일 : 2019.02.11


출처 및 참고자료


유니티 모바일 최적화 문서 : https://docs.unity3d.com/kr/current/Manual/MobileOptimizationPracticalGuide.html

수양버들님의 블로그 : https://egohim.blog.me/70170987774

어른이의 공부방 : https://usroom.tistory.com/entry/효과적인-C-메모리-관리-기법?category=553244




1. 오브젝트 풀링(object pooling)


짧게 사용되고 사라진 뒤에 다시 사용되는 오브젝트의 경우 (총알이나 퍼즐게임의 블럭등)

매 번 다시 생성/해제를 반복하면 메모리 부하가 심해진다.

그래서 화면에 보이지 않게만 하고 해제시키지 않은 채로 필요할 때 마다 다시 사용하는 기법.

메모리 할당을 더 간단하게 할 수 있고 동적 메모리 할당 오버헤드와 가비지 컬렉션(GC)을 줄일 수 있다.


효율적인 구현방법에 대해서 더 고민해보고 구현 연습을 해봐야 할 것 같다.

문서에서 소개되는 단점도 있다.


1. 다른 목적으로 사용할 가용 힙 메모리의 양이 줄어든다. 따라서 현재 막 생성한 풀 외에도 메모리를 계속 할당한다면, 가비지 컬렉션이 더욱 자주 실행될 수 있다. 


2. 가비지 컬렉션에 걸리는 시간은 살아있는 오브젝트의 수에 비례하여 증가하기 때문에 매번 더 느려질 수 있다.


너무 큰 풀을 할당하거나 또는 풀에 있는 오브젝트가 한동안 필요가 없는 상황에서 풀을 활성화하여 유지한다면 성능에 지장이 생기게 된다.


따라서 "오브젝트 풀 오버헤드 < 오브젝트 풀을 사용하지 않았을 때 생기는 메모리 부하"인 경우에만 사용 해야 한다.

설계 단계에서도 고민을 많이 해봐야 하고 구현단계에서도 프로파일링으로 이득인지 실인지를 잘 따져봐야 할 듯.


2. Class vs Struct


클래스의 인스턴스는 힙 영역에 할당된다.

구조체의 인스턴스는 스텍에 할당된다.

즉, 구조체는 아무리 만들고 없애도 GC에 부담을 주지 않지만 클래스를 반복적으로 할당 해제를 하는 것은 GC에 부담을 주게 된다.

수백번 호출되는 메소드에서 클래스를 계속 만들었다 해제하면 C의 Memory Fragmentation과 유사한 문제를 야기할 것이다.


장시간 유지되어야 하는 오브젝트는 = 클래스

단시간만 사용할 오브젝트는 = 구조체


예를 들면, Vector3도 구조체다.


클래스와 구조체의 차이 정도는 알았지만 최적화에 큰 영향을 줄 정도라고는 생각하지 않았는데 예제등을 읽어보면서 깨달음을 얻었다.

메소드에서 반복적으로 생성이 필요한 사용자 정의 테이블을 만들때는 무조건 구조체 형식을 사용해야겠다.


3. Immutable, String 반복 생성


<리스트 1> ‘+’로 연결한 문자열 조합

class Names

{

    public string[] name = new string[100];

    public void Print()

    {

        for (int index = 0; index < name.Length; index++)

        {

            string output = "[" + index + "]" + name;

            Console.WriteLine(output);

        }

    }

}


C#에서 String은 Immutable 객체다.

따라서 "안녕" + "하세요"등의 연산을 시키면 새로운 "안녕하세요"객체를 생성하게 된다.

위의 예제를 보고 큰 충격에 빠졌는데 사실 C#의 문자열을 +로 이어주는 구문은 내가 너무 좋아하고 자주썼던 구문이기 때문이다.

하지만 이 경우 반복문 안에서는 계속해서 객체의 할당과 해제가 수차례 발생하게 된다.

당연하게도 단편화 문제가 생기고 GC가 또 출근을 해야하는 상황을 만들게 된다.


<리스트 2> System.Text.StringBuilder 객체 사용

class NewNames

{

    public string[] name = new string[100];

    private StringBuilder sb = new StringBuilder();


    public void Print()

    {

        sb.Clear();     // sb.Length = 0;

        for (int index = 0; index < name.Length; index++)

        {

            sb.Append("[");

            sb.Append(index);

            sb.Append("] ");

            sb.Append(name);

            sb.AppendLine();

        }

        Console.WriteLine(sb.ToString());

    }

}


위와 같이 StringBuilder를 이용해 Append() 메소드를 통해 문자열을 추가하며, string 객체를 만들어내는 게 아니라 이미 잡아놓은 메모리 공간에 문자열만 복사해 뒀다가 한번에 ToString()으로 string 객체를 생성해낸다.

내부의 for문은 다음과 같이 정리 할 수도 있다.


for (int index = 0; index < name.Length; index++)

{

    sb.AppendFormat("[{0}] {1}", index, name.ToString());

}


4. 애니메이션 스프라이트 파티클 시스템



2D 횡스크롤 게임에서 수많은 동전이 떨어지고 튕기고 회전하는 효과를 표현하려고 한다. 동전은 점 광원에 의해 동적으로 빛난다.


하드웨어가 고성능이라면 모든 동전을 오브젝트로 만들고, 버텍스 릿, 포워드 또는 디퍼드 라이팅 중에 하나를 가지고 셰이드하고, 이미지 이펙트로 상단에 글로우 효과를 추가해서 밝게 반사하는 동전이 주변에 광원을 뿌리도록 하면 된다.


그러나 모바일 하드웨어에서 이렇게 많은 오브젝트를 만들면 크게 부담이 되며, 글로우 효과는 거의 불가능하다.


이 예제의 요점은 동전의 회전등을 물리적으로 구현하는게 아니라.

물리적 구현의 결과 (회전)를 미리 계산하고 그 것을 이미지 스프라이트 파티클로 대체하라는 것이다.;

이 때, 모든 동전이 단조롭게 동시 회전하는 것을 방지하기 위해 회전과 수명 주기를 직접 추적하고 파티클 수명 주기에 맞춰 회전을 “렌더링”하는 방식을 사용해야 한다.


그 밖에도 모든 동전을 오브젝트로 만들지 않기 때문에 생기는 수많은 문제점들을 여러 방법을 통해서 해결하고 있다. 

원문에는 예제 파일도 같이 올라와 있기 때문에 예제 파일을 뜯어서 익히면 도움이 많이 될 것 같다.


절대강좌 Unity 책에서 그림자를 동적 생성하는 것의 위험에 대해 써 있었던 것이 기억난다.


원문에서 가장 소름돋았던 포인트는,


Things to try to optimize further:

 - Instead of calculating lighting for every coin individually, cut the world into chunks and calculate lighting conditions for every rotation frame in every chunk.

 - Use as a lookup table with coin position and coin rotation as indices.

 - Increase fidelity by using bilinear interpolation with position.


즉, 월드 맵을 몇 구역으로 나누에서 해당 구역에서 일정 회전 프레임 마다 빛을 어떻게 적용해야 하는지 미리 계산해서 해당 테이블 값을 참조해 코인에 빛을 주라는 말이다. 


이 예제는 꼭 공부하고 해체해 봐야겠다. 

원문 : https://docs.unity3d.com/kr/current/Manual/MobileOptimizationPracticalScriptingOptimizations.html



5. 불필요한 검색 대신 캐시 레퍼런스 사용


이것 같은 경우는 유니티 예제에서 많이 본 내용이다.


 var speed = 5.0;

 

 function Update () {

    transform.LookAt(GameObject.FindWithTag("Player").transform);

    // this would be even worse:

    //transform.LookAt(FindObjectOfType(Player).transform);

 

    transform.position += transform.forward * speed * Time.deltaTime;

 }


이거 대신에


// EnemyAI.js

 var speed = 5.0;

 

 private var myTransform : Transform;

 private var playerTransform : Transform;

 

 function Start () {

    myTransform = transform;

    playerTransform = GameObject.FindWithTag("Player").transform;

 }

 

 function Update () {

    myTransform.LookAt(playerTransform);

 

    myTransform.position += myTransform.forward * speed * Time.deltaTime;

 }


이렇게 코딩 하라는 말이다. 즉, 오브젝트를 메소드 안에서 반복 생성하지 말고

자주 쓸 것 같으면 맴버 변수로 한번 기억을 시켜놓고 메소드에서는 맴버 변수를 호출해서 사용하라는 뜻.


그 와중에 FindObjectOfType가 GameObject.FindWithTag보다 느리다는 미세 팁 까지 있었다. (당연한가?)



6. 비용이 큰 수학 연산 줄이기


Transcendental functions (Mathf.Sin, Mathf.Pow, etc), Division, and Square Root all take about 100x the time of a multiplication.

Sin, Pow, 나눗셈, 제곱근, 루트 등의 연산이 곱셈 연산보다 100배 가량 느리다고 한다.


예를 들어서 메소드에서 n/c  (n = 변수, c = 맴버상수)인 경우라면 1.0/c을 미리 기억 해 놨다가 곱셈 연산으로 바꾸는 편이 좋다는 이야기다. 100배 느리다고 해도 큰 차이가 없을 것 같긴 한데 엄청나게 반복 실행된다면 이야기가 또 다를테니 기억해 놔야겠다.


또 다른 예로는 백터 정규화 등이 있었다. 이것도 캐싱을 하거나 normalized 프로 퍼티 대신에 역을 곱한다거나 하는 방식에 대한 말이다.


거기에 Math.OOO 들은 어쨌거나 함수이므로 호출되는 것 만으로도 약간의 오버헤드가 발생한다.

x = Mathf.Abs(x)와 같은 함수를 프레임당 수천 번씩 호출한다면, x = (x > 0 ? x : -x);와 같이 처리하는 편이 더 좋다는 말이다.






'Study > CS' 카테고리의 다른 글

송신 포트 vs 수신 포트 (Source Port vs Destination Port)  (0) 2019.07.30
TCP - Connection Establishment/Termination  (0) 2019.07.16
TCP - Checksum  (0) 2019.07.16
TCP - Segment Structure  (0) 2019.07.12
TCP  (0) 2019.07.12
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/03   »
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
글 보관함