본문 바로가기

유니티 개발 정보/개념

유니티 메모리 관리 - 4(마지막)

늦어서 죄송합니다. 그동안 잠시 다른 일이 생겨서 마지막 부분 업로드가 늦었습니다.
해당 글 원문은 http://unitygems.com/memorymanagement/에서 보실수 있습니다.
번역중에 이상한 부분은 댓글 남겨주시면 확인 후에 수정하겠습니다.
(저도 공부하면서 올리는 거라 미흡한 점이 많이 있습니다. 미흡한 부분이 있더라도 조금만 양해부탁드립니다^^;)


Static Variables
정적 변수


많은 초보자들은 이 간단한 실수를 한다:

1
2
3
4
public static int health;
void Start(){
    health=100;
}

다른 스크립트에서 우리는 다음과 같은 코드를 발견할 것이다.

1
2
3
4
5
6
void OnCollisionEnter(Collision other){
    if (other.gameObject.tag == “Enemy”){
        EnemyScript.health-=10;
        if(EnemyScript.health <=0)Destroy(other);
    }
}

그들은 적을 죽이려고 할 것이고, 그를 죽이는데 성공한다. 그러나 그때 더 많은 적들이 추가되고, 모두 갑자기 죽는것에 의아해 한다.

이유는 간단하다. 당신은 많은 enemy 클래스 인스턴스를 가지고 있다.(그것은 static이 아니다) 그러나 그들은 모든 적에 대해 오직 하나의 health 인스턴스를 가진다. 하나를 죽이는 것은 모두를 죽이는 것이다!

사실, 클래스에 선언된 정적 변수는 객체들에 속해 있는 것이 아니라 클래스 그 자체에 속해 있다.
이것이 바로 우리가 정적 변수에 접근할 때 인스턴스 이름이 아닌 클래스 이름을 사용하는 이유이다.

그것은 적의 체력에 대해서 올바르게 실행되지 않았다. 왜냐하면 각각의 적들은 그들 스스로의 health 변수를 가져야 하기 때문이다.

정적 클래스와 같은 방법으로, 정적변수를 선언한 클래스가 첫번째로 접근될 때 정적 변수는 인스턴스화된다.
이것은, 게임에서 클래스의 어떤 인스턴스도 없더라도, 우리가 정적변수에 접근하려고 할 때 정적 변수는 존재한다는 것을 의미한다.

적 카운터를 고려할 때, 우리는 게임 안에서 적들의 숫자를 추적하기를 원한다.

static var counter;
 
public void CreateEnemy(){
    GameObject obj=Instantiate(enemyPrefab, new Vector3(0,0,0).Quaternion.identity);
 
    if(obj)counter++;
}
 
public void DestroyEnemy(){
    Destroy(gameObject);
    counter--;
}

이 스크립트는 각 적들에게 첨부될 것이다. 모든 적들이 다른 스크립트를 가지고 있더라도, 모든 적들은 같은 counter 변수를 공유한다

우리의 게임에 이 함수를 사용하는 것은 우리가 계속해서 적들의 숫자를 추적 가능하게 한다.
Instantiate가 객체를 만드는 것을 실패했을 경우에(예를 들면, 힙 파편화로 인한), Instantiate는 null 참조를 반환하기 때문에, 참조 변수 사용에 주의해야 한다. 

만약 어떤 이유로 인스턴스 생성이 실패되었을 경우, 다음과 같은 if문의 확인없이 적 카운트를 증가시킨다면, 우리는 잘못된 counter를 가질 것이다.

지금 만약 다음과 같이, counter변수를 사용하여 레벨을 끝내고, 새로운 레벨을 불러오려고 한다면,
if(counter<=0)Application.LoadLevel(nextLevel);
만약 확인을 하지 않고 만약 인스턴스화를 실패했다면, 모든 적을 죽인 후에, 우리가 counter가 1이고 우리의 게임은 버그가 생긴 것이다.

우리의 플레이어는 어떤가, 정적 health를 사용할 수 있는가? 그렇다. 우리는 우리의 플레이어의 체력을 위해서 정적 변수를 사용할 수 있다. 사실 정적 변수에 접근하는 것은 심지어 인스턴스 맴버보다 더 효율 적이다.

인스턴스의 멤버를 호출할 때, 컴파일러는 객체의 존재를 확인하기 위한 작은 함수 요청이 필요하다. 왜냐하면 존재하지 않은 데이터를 수정할 수 없기 때문이다.


Static Functions
정적 함수

정적 함수는 비정적 클래스에서 구현될 수 있다. 이 경우에 있어서, 오직 그 클래스의 정적 변수만이 정적함수안에서 접근될 수 있다. 정적 함수는 인스턴스가 아닌 클래스를 통해 호출되기때문에, 
존재하는 것이 확신되지 않는 맴버에 접근하는 것은 문제가 될 수 가 있다.

반면에, 정적 함수에 매개변수를 전달할 수 있고, 당신은 아마 이런 함수를 꽤 많이 사용하고 있을 것이다.
1
2
3
4
5
6
Vector3.Distance(vec1.vec2);
vec1.Normalize();
 
Application.LoadLevel (1);
 
if (GUI.Button(Rect(10,10,50,50),btnTexture)){}

그 목록들을 다 말하기에는 너무 많다. 두번째 라인은 정적 함수가 아니다. 그것은 vector3타입의 vec1 인스턴스를 통해 호출된다. 그 밖의 예는, 클래스가 정적 함수를 호출하기위해 사용되는 것을 보여 준다.

정적 함수는 비정적함수보다 더 빠른 경향이 있다. 컴파일러는 인스턴스함수의 호출이후에 인스턴스가 존재하는지 확인을 하기위해 작은 오버헤드가 발생하기 때문이다. 정적 함수는 존재의 유무가 확실한 클래스를 사용한다. 그렇지만 시간의 이득은 작다.


정적 클래스의 또 다른 이점은 그들의 존재가 영구적이고, 컴파일러에게 알려져 있으며,
따라서 C#에서 정적 클래스는 확장메소드 를 정의하기 위해 사용되어 질 수 있다.
확장메소드는 다른 타입의 변수에 첨가된 추가적인 함수처럼 보인다.
이는 syntactic sugar이다. 이런 식으로 확장된 클래스는 사실 열려있지 않지만, 
(그들의 private과 protected 변수는 확장메소드에서 접근이 불가능하다.)
이는 겉보기에 기존의 객체에 새로운 함수를 주는 깔끔한 방법 - 코드의 가독성을 향상시키고, 개발자들에게 더 쉬운 API를 제공하는 - 이다.

예를 들어 표면상으로 다음과 같이 Transform에 새로운 함수를 추가할 수 있다.

transform.MyWeirdNewFunction(x);
확장 메소드의 예:


기존의 함수(OrderBy, Sort,...)를 확장하는 배열에 함수를 만들기를 원한다고 생각해보자.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
using UnityEngine;
using System.Collections;
 
public static class ExtensionTest {
 
    public static int BiggestOfAll(this int[] integer){
        int length = integer.Length;
        int biggest = integer[0];
        for(int i = 1;i<length;i++){
            if(integer[i]>biggest)biggest = integer[i];
        }
        return biggest;
    }
}


우리는 정적 함수를 정의한다. 전달된 매개변수가 어떻게 함수를 호출하는 인스턴스가 되는지를 봐라.
매개변수의 타입을 수정하는 것은 다른 객체에서 작동하도록 한다.


이제 선언해서 배열을 채워보자:

01
02
03
04
05
06
07
08
09
10
11
using UnityEngine;
using System.Collections;
 
public class Test : MonoBehaviour {
 
        int[] array = {1,25,12,120,12,6};
 
    void Start () {
        print(array.BiggestOfAll());
        }
}

배열 이름 뒤에 .(점)을 입력하면, 위에서 입력한 새로운 함수가 나타나는 것을 보게 될 것이다.

그들의 타입과 상관없는 다른 배열들을 받아들이는 더 제너릭한 함수를 만들기를 원한다면, 
정적 제너릭 확장 메소드를 구현해야 할 것이다.


01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
 
public static class ExtensionTest {
 
    public static T TypelessBiggestOfAll<T>(this T[] t){
        int length = t.Length;
        var biggest = t[0];
        for(int i = 1;i<length;i++){
            biggest =((Comparer<T>.Default.Compare(t[i], biggest)>0)?t[i]:biggest);
        }
        return biggest;
    }
}


using System.Collections.Generic의 추가를 주목해라. 컴파일러가 T타입이 무슨타입인지 알지 못하기 때문에, 우리는 간단하게  < 또는 >를 통해서 값을 비교할 수 없다. 우리는 Comparer<T>를 사용해야 한다.
예를 들어,  삼항연산자가 사용된다. 우선 약간 혼란스러운 코딩을 만드는 경향이 있지만, 또한 약간의 라인을 줄여준다.


삼항 연산자는 다름과 같에 생겼다.  a ? b : c;

a는 수행된다. 그리고 만약 참이면 b가 명령문에서 내보내진다. 만약 거짓을 경우 c가 보내진다. 
일반적인 if문과 비교해봤을 때 장점은 비교후 값을 리턴한다는데 있다. 
result = a ? b : c; result는 a에 의존하여 b 또는 c값을 받는다.
이제 다른 타입의 배열을 선언한 뒤 같은 함수를 사용하는 것이 가능하다.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
using UnityEngine;
using System.Collections;
 
public class Test : MonoBehaviour {
 
    int[] arrayInt = {1, 25, 12, 120, 12, 6};
    float[] arrayFl = {0.5f, 52.456f, 654.25f, 41.2f};
    double[]arrayDb = {0.1254, -15487.258, 654, 8795.25, -2};
 
    void Start () {
        print(arrayInt.TypelessBiggestOfAll());
        print (arrayFl.TypelessBiggestOfAll());
        print (arrayDb.TypelessBiggestOfAll());
       }
}

이는 값 120, 654.25, 8798.25를 출력한다.

이제 나는 당신에게 이것이 유용하다는 것을 보여주고싶다.  많은 유니티 유저는 객체 내부의 지역을 통제하는 방법에 대해서 묻는다. 예를 들어, 화면 내부에 객체를 유지하고 싶기를 원한다면, 당신은 이 함수를 사용하여 그 객체의 Transform을 고정해야 한다.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
using UnityEngine;
using System.Collections;
 
public static class GameManager{
 
    public static void ClampTransform(this Transform tr,Vector3 move, Vector3 min, Vector3 max){
        Vector3 pos = tr.position + move;
        if(pos.x < min.x ) pos.x = min.x;
        else if(pos.x > max.x) pos.x = max.x;
 
        if(pos.y  < min.y ) pos.y = min.y;
        else if(pos.y> max.y) pos.y = max.y;
 
        if(pos.z < min.z) pos.z = min.z;
        else if(pos.z > max.z) pos.z = max.z;
 
        tr.position = pos;
    }
}

그리고 당신이 필요한 어떤 곳이든 사용하면 된다:

1
2
3
Vector3 minVector = new Vector3(-10,0,-10);
Vector3 maxVector = new Vector3(10,0,10);
transform.ClampTransform(move,minVector,maxVector);

이 예에서,  객체는 (0, ,0, 0)을 중심으로한 20*20 사각형의 2D 환경에 제약이 걸려 있다.

Heap Fragmentation, Garbage Collection and Object Pooling
힙 파편화, 가비지 컬렉션, 오브젝트 풀링


힙은 데이터가 무작위로 저장되어 있는 큰 메모리 공간이다.  사실 그렇게 무작위는 아니다. OS는 메모리를 조사할 것이고, 요구된 데이터에 적합한 크기의 첫번째 메모리 지역을 찾을 것이다. 
힙의 크기와 위치는 당신이 선택한 플랫폼에 따라 달라진다.

예를 들어, 정수에 대해 요청할 때, OS 연속적인 4바이트의 메모리공간을 찾는다. 사실은, 프로그램이 실행될 때, 메모리 지역은 실제 규칙없이 사용되고, 해제된다. 그리고 당신의 프로그램은 힙 파편화가 발생될 것이다.


이 사진은 힙 파편화의 예를 보여준다. 명백하게 이것은 좀 지나치게 강조하고 있지만, 본질은 틀리지 않다.

어떤 데이터도 추가 되지 않았다는 것을 명심해라. state1에서의 같은 양의 데이터가 있다.
오직 움직임이 힘 파편화를 만들었다.



유니티는 .NET managed 메모리를 사용한다. C 프로그램에서 힙 파편화는 메모리 블럭을 할당하는 것이 불가능해지는 상황을 초래할 수 있다. 왜냐하면 사실 충분한 메모리 공간이 있더라도 적합한 크기의 인접한 블록이 없기 때문이다. managed 메모리는 이 제한으로 부터 영향을 받지 않는다. 만약 메모리 블록 발견되지 않는다면, 실제 .NET 메모리 관리와 가비지 컬렉션 시스템은 메모리를 움직임으로써 힙의 파편화를 제거할 수 있다. 유니티에서는 가비지 컬렉션의 실행으로 힙 파편화 현상이 일어나지 않는다.
한번에 하나 이상의 객체가 제거됨으로써 공간을 찾을 것이다. 그러나 이것은 그들이 적합할 때까지 블록을 움직이는 것만큼 효율적이지는 않다.
프로그래머의 해답은 오브젝트 풀링을 사용하는 것이다. state1에서 data3를 파괴하는 대신에 우리는 간단하게 나중의 사용을 위해서 그것을 비활성화 시키면 어떻게 될까? 아마 힙 파편화를 피했을 것이다.

우리는 data를 파괴하지 않는다. 우리는 그것을 잠자게 할 것이고, 필요할 때 우리는 그것을 깨울 것이다.

이 해법에 많은 장점이 있다. 우선 우리는 위에서 봤던 경우를 피할 것이고, 우리는 또한 Instantiate과 Destroy 함수 호출을 피할 것이다. 우리는 오직 gameobject.SetActiveRecursevely(true/false)를 사용하면 된다.

마지막으로 우리는 파괴하지 않기 때문에, 우리는 비용이 비싼 행위인 가비지 컬렉터를 호출하지 않는다.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
ObjectScript.cs
using UnityEngine;
using System.Collections;
 
public class Test : MonoBehaviour {
    public GameObject prefab;
    GameObject[] objectArray = new GameObject[5]; 
        public static int counter;
 
         void Start(){
        for(int i = 0;i<objectArray.Length;i++){
            objectArray[i] = (GameObject)Instantiate(prefab,new Vector3(0,0,0),Quaternion.identity);
            objectArray[i].SetActiveRecursively(false);
        }
        }
        void Createobject(){
        if(counter < objectArray.Length){   
                // iterate through array
            for(int i = 0;i<objectArray.Length;i++){ 
                                // Find an inactive object                                    
                if(!objectArray[i].active){   
                               // Increase the counter, activate the object, position it                                             
                    counter++;                                             
                    objectArray[i].SetActiveRecursively(true);            
                    objectArray[i].transform.position = new Vector3(0,0,0); 
                    return;
                }
            }return;
        }else return;
    }
}
1
2
3
4
void OnCollisionEnter(CollisionEnter){
    if(other.gameObject.tag=="Object"){
        other.gameobject.SetActiveRecursively(false);       //Deactivate the object
        ObjectScript.counter--;                                             //Decrease counter

이 씬에서는 한번에 절대 5개의 객체를 가지지 않는다. 1개가 비활성화 될 때, 우리 원하는 곳에 다시 재활성화 시킬 수 있다. 가비지 컬렉션은 필요하지 않는다.


만약 당신 앞에 나타나는 적들의 웨이브를 가진다면, 죽은 적들을 파괴하는 대신에, 당신의 적에 대해서 이 트릭을 사용할 수 있고, 그들을 다시 재위치 시킬 수 있다. 


탄약에서도 똑같이, 각각의 총알을 파괴하는 대신에 그것을 비활성화 시킨다.
만약 당신의 총 앞의 위치와 적절한 속도로 다시 발사할 때, 그것을 다시 활성화 시키면 된다.


Note that pooling at the memory level is a different process managed by the OS consisting in reserving always the same amount  of memory whatever size the object is. As a result there will never be tiny memory locations lost all over the memory.
메모리 단계에서 풀링은 객체의 사이즈가 얼마이든 간에, 항상 같은 양의 메모리를 확보하는 OS에 의해 관리되는 다른 프로세스이다.

Array Reuse
배열 재사용

객체를 재사용하는 것에 더하여, 우리는 각 호출 또는 같은 호출에서 반복적인 코드를 재사용하는 버퍼를 생성하는 것은 생각해볼 만한 가치가 있다.

01
02
03
04
05
06
07
08
09
10
11
12
void Update()
{
      if(readyToFire && Input.GetKeyDown(KeyCode.F))
      {
          var nearestFiveEnemies = new Enemy[5];
 
          //Do something to fill out the list finding the nearest
          //enemies
 
          TargetEnemies(nearestFiveEnemies);
      }
}

이 예제에서, 우리는 가장 가까운 5개의 적을 타겟팅 할 것이고, 자동적으로 그들에게 발사할 것이다.
문제는 발사 버튼을 누를 때 마다 메모리가 할당된다는 점이다. 대신에 만약 우리가 오랫동안 유지되는 적들의 배열을 만들었다면, 우리는 코드 어디에서든 사용할 수 있을 것이고, 우리는 메모리를 할당하지 않아도 되며 느려지는 현상을 피할 수 있을 것이다.

이것은 간단한 케이스가 될 수 있다:

01
02
03
04
05
06
07
08
09
10
11
12
Enemy[] _nearestFiveEnemies = new Enemy[5];
 
void Update()
{
      if(readyToFire && Input.GetKeyDown(KeyCode.F))
      {
          //Do something to fill out the list finding the nearest
          //enemies
 
          TargetEnemies(_nearestFiveEnemies);
      }
}

그러나 어쩌면 우리가 종종 적들의 배열이 필요할 때,  우리는 그것을 더 일반적으로 만들 수 있을 것이다.

01
02
03
04
05
06
07
08
09
10
11
12
Enemy[] _enemies = new Enemy[100];
 
void Update()
{
      if(readyToFire && Input.GetKeyDown(KeyCode.F))
      {
          //Do something to fill out the list finding the nearest
          //enemies
 
          TargetEnemies(_enemies, 5);
      }
}

카운트를 가지는 TargetEnemies에 대한 코드를 재작성함으로써, 우리는 효율적으로 우리의 코드 어떤 곳에서든 일반적인 적의 목적 버퍼를 사용할 수 있을 것이고, 훨씬 더 할당의 필요성을 피할 수 있을 것이다.


두번째 예제는 꽤 지나친 경우이다. 그리고 만약 실제로 메모리 문제를 야기시키는 거대한 집합을 가진다면 반드시 이것을 해야한다. - 일반적으로 우리는 언제나 읽기쉽고, 약간의 메모리를 아낄 수 있는 이해할 수 있는 코드를 작성해야 한다.


'유니티 개발 정보 > 개념' 카테고리의 다른 글

상급 코루틴 개념(Advanced Coroutines)  (1) 2013.10.11
코루틴(Coroutine)++  (3) 2013.10.08
유니티 메모리 관리 - 3  (1) 2013.08.13
유니티 메모리 관리 - 2  (0) 2013.08.09
유니티 메모리 관리 - 1  (0) 2013.08.07