https://stackoverflow.com/questions/50689859/download-large-file

 

Download large file

By downloading a file with UnityEngine.WWW, I get the error OverflowException: Number overflow. I found out the error is caused form the structure itself, because the byte-array has more bytes ...

stackoverflow.com

https://forum.unity.com/threads/uwp-memory-leak-when-downloading-large-files.765122/#post-5103632

 

UWP: Memory leak when downloading large files

The following code run on UWP will raise the memory by 100MB every iteration until the application crashes. What am I missing here? We're seeing...

forum.unity.com

 

위 두 링크의 내용을 축약하면..

UnityWebRequest를 사용하여 텍스쳐를 다운로드를 한 후, 다운로드된 리소스를 처리하기 위해 직접 www.downloadHandler.data에 Access 하게 되면, 리턴을 위해 내부에서 새로운 바이트 배열이 생성되므로 메모리 관리에 매우 취약하다는 내용.

 

첫 번째 링크처럼 만약 다운로드한 리소스를 파일로 저장하거나 특정 목적으로 사용할 경우 그에 맞는 downloadHandler를 전송 전에 할당하여 처리할 수 있도록 해야 함.


WRITTEN BY
빨강꼬마

,

역시 NDC가 꿀이네 ㄷㄷㄷ


http://www.inven.co.kr/webzine/news/?news=198239&sclass=0&page=2


WRITTEN BY
빨강꼬마

,

약 35개 의 객체에 itween을 적용해서 이동시켰을때 프로파일러상에서 GC.collect의 메모리 할당량이 140kb 정도로 부하가 발생함..

해외 사이트를 검색해보니 itween의 경우 부하가 좀 발생한다고 함..

테스트로 NGUI에서 제공하는 tweenPosition 기능으로 변경해서 적용한 결과, 5~60kb 사이로 반 이상 줄어들었음.

타겟 디바이스가 모바일인 경우 이정도 부하면 민감하므로 적용시 참고.


WRITTEN BY
빨강꼬마

,

유니티 문서를 보면 언급되는 Managed Memory란 단서에서 흔히 추측하기로 유니티가 알아서 모든 메모리를 잘 관리하고 있을 것 처럼 생각되지만 사실은 그렇지 않다는 것이 함정.

사실은 유니티 시스템은 메모리를 어떻게 처리할 것인가에 대한 단서를 당신이 만드는 코드내에서 제공해주기를 원한다. 따라서 잘못된 코드는 줄줄 새는 메모리로 당신의 앱을 디바이스에서 뻗어 버리게 만들것이다. 요즘은 모바일 디바이스에서 조차 64bit 시스템(iPhone 5s)가 올라가고 기본 장착 메모리가 2Gb 이상이 되는 등 모바일 앱으로서는 무한한 자원이 있는것 같지만 사실상 아직도 지구상의 대부분의 유저는 허접한 디바이스로 연명하......


링크 - http://la-stranger.blogspot.kr/2013/09/blog-post_25.html

'Unity > 메모리관리' 카테고리의 다른 글

GC 로깅 (메모리 단편화 추정될때)  (0) 2018.05.15
itween은 되도록 자제...  (0) 2014.08.12
유니티 메모리 관리 4 (마지막)  (2) 2013.10.16
유니티 메모리 관리 3  (0) 2013.10.16
유니티 메모리 관리 2  (0) 2013.10.16

WRITTEN BY
빨강꼬마

,

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에 대한 코드를 재작성함으로써, 우리는 효율적으로 우리의 코드 어떤 곳에서든 일반적인 적의 목적 버퍼를 사용할 수 있을 것이고, 훨씬 더 할당의 필요성을 피할 수 있을 것이다.


두번째 예제는 꽤 지나친 경우이다. 그리고 만약 실제로 메모리 문제를 야기시키는 거대한 집합을 가진다면 반드시 이것을 해야한다. - 일반적으로 우리는 언제나 읽기쉽고, 약간의 메모리를 아낄 수 있는 이해할 수 있는 코드를 작성해야 한다.
- 유니티안 Dexter님께서 http://unitygems.com/memorymanagement/ 의 원문을 번역하여 올려주신 걸 따로 가지고 있기 위해 올림.
덱스터님 의 원문은 [여기]를 참고해주세요 ^_^


'Unity > 메모리관리' 카테고리의 다른 글

itween은 되도록 자제...  (0) 2014.08.12
유니티에서의 메모리 관리  (0) 2014.04.28
유니티 메모리 관리 3  (0) 2013.10.16
유니티 메모리 관리 2  (0) 2013.10.16
유니티 메모리 관리 1  (0) 2013.10.16

WRITTEN BY
빨강꼬마

,


Struct vs Class

그래서 어떤 상황에서 어느 것을 사용해야 하는가? 우리는 클래스를 사용하는 것은 추가적인 변수(참조변수)가 추가 된다는 것을 알았다. 만약 수천 개의 객체를 만든다면, 수천개의 추가적인 변수를 얻을 것이다. 하지만 구조체는 그것들을 만들지 않는다.

밑의 예제를 보자:

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
32
33
34
35
36
37
38
39
40
public class DogC{
    public string name { get; set;}
    public int age { get; set;}
}
 
public struct DogS {
    public string name;
    public int age;
}
 
using UnityEngine;
using System.Collections;
 
public class Memory : MonoBehaviour {
    DogC dogC = new DogC();
    DogS dogS = new DogS();
 
    void Update () {
        if(Input.GetKeyDown(KeyCode.Alpha1))
            Print (dogS);
        if(Input.GetKeyDown(KeyCode.Alpha2))
            Print (dogC);
        if(Input.GetKeyDown(KeyCode.Alpha3))
            print ("Struct: The dog's name is "+dogS.name +" and is "+dogS.age);
        if(Input.GetKeyDown(KeyCode.Alpha4))
            print ("Class: The dog's name is "+dogC.name +" and is "+dogC.age);
     }
 
    void Print(DogS d){
        d.age = 10;
        d.name = "Rufus";
        print ("Struct:The dog's name is "+d.name +" and is "+d.age);
     }
 
    void Print(DogC d){
        d.age = 10;
        d.name = "Rufus";
        print ("Class:The dog's name is "+d.name +" and is "+d.age);
    }
}

우선, 세번째 입력은 우리가 예상했던 값을 출력하지 않을 것이다. 구조체의 name과 age는 값이 사라졌다. 
반면에 클래스는 값을 유지한다.
(역주: 유니티에서 테스트해보시면 쉽게 이해하실 수 있습니다. 업데이트문에 있는 순서대로 입력을 해보시기 바랍니다. 참고로 KeyCode.Alpha1은 숫자1키를 말합니다.)  

우리가 말했던 것을 기억해라. 클래스는 참조 타입이고 구조체는 값 타입이다. 구조체에 대해서 함수를 호출 할 때, 구조체의 복사본을 전달하지만, 그 복사본은 원래의 데이터가 아니다. 그 데이터를 수정할 때, 우리는 단지 스택에 있는 복사본의 데이터를 수정하는 것이다. 이들은 함수의 마지막 부분에서 사라진다.

클래스를 매개변수로 받는 함수는 원래의 데이터로 접근할 수 있는 참조값을 전달 받는다. 
그리고 함수안에서 수정도 가능하다. 함수가 끝나더라도 수정한 것은 그대로 유지된다.

우리가 생각해야 하는 것은 무엇인가? 만약 구조체가 10개의 변수를 포함할정도로 크다면, 우리가 함수에 구조체를 전달할 때 스택에는 10개의 공간이 생성될 것이다. 지금 내가 이 구조체 50개를 가지고 있는 구조체배열을 전달한다면 스택에는 500개보다 더 큰 공간이 발생할 것이다. 스택의 사이즈는 제한되어 있고, 만약 구조체와 배열의 크기가 더 크다면 스택 오버플로우가 발생할 수도 있다.



클래스를 전달 할 때는, 오직 하나의 변수만 전달한다. 50개의 클래스 인스턴스를 전달할 때는 스택에 50개의 변수에 대한 공간만 생성될 것이다.

종합해보면, 클래스는 메모리를 절약하지만 구조체는 대신 더 빠르다. 핵심은 2개의 균형을 알아내는 것이다. 
5개~8개의 멤버변수를 가질 때 구조체를 사용하는 것을 고려해라. 그 보다 위에는 클래스를 사용해라.


You must also bear in mind that every time you access a variable which is a structure (for example by doing transform.position) you are creating a copy of that structure on the stack - that copy takes time to create.
구조체 변수에 접근할 때마다(예를 들어 transform.position를 사용할 때) 스택에 구조체의 복사본을 만들고, 복사하는데도 시간이 든다는 것을 명심해야 한다.
(역주: 구조체 변수에 접근할 때마다 스택에 구조체의 복사본을 만든다는게 잘 이해가 안되서
원문과 같이 나뒀습니다. 혹시 이글을 보시고 부연설명이 가능하신분은 댓글로 좀 부탁드리겠습니다.)

클래스를 만들때 마다, 힙에 메모리를 할당할 것이다. 이는 빠른속도로, 버려진 클래스들을 위한 가비지 컬렉션 사이클로 이어진다. 구조체는 항상 스택에 할당되며 이런 이유로 가비지 컬렉션을 절대 유발시키지 않는다.

유니티에서 구조체는 종종 3~4개의 변수를 가진다. 
(예를 들어, position, color, quaternion, rotation, scale...이들 모두다 구조체이다)


오래가지 못하는, 자주 할당되는 객체는 구조체의 좋은 후보가 된다. 왜냐하면 이들은 가비지 컬렉션을 유발시키지 않기 때문이다. 오래가고 큰 객체들은 클래스의 좋은 후보들이다. 왜냐하면 그들은 접근될 때 마다 복사되지 않으며, 그들의 지속된 존재는 가비지 컬렉션을 유발시키지 않는다. 
Vector3, RayCastHit같이 많은 작은 객체들은 바로 이 이유때문에 유니티에서 구조체인 것 이다.
다음을 넘어가기전에 마지막 핵심 하나가 있는데,
함수에서 구조체를 수정하고 값을 계속 유지하기 위해서, 구조체를 참조로써 전달하는 것이 가능하다.


구조체 변수를 정의한 함수가 종료되었을 때 구조체에 대한 참조를 유지할 수 없다.이 같은 문제를 해결하기 위해 클로저의 사용(람다 함수 또는 인라인 함수에 의해서 생성된)처럼 많은 방법이 있지만 그것은 꽤 고급 기술에 속한다.
1
2
3
4
5
void PrintRef(ref DogS d){
    d.age = 10;
    d.name = "Rufus";
    print ("Structure:The dog's name is "+d.name +" and is "+d.age);
}

우리는 위에 것을 다음처럼 호출할 것이다:

1
2
3
4
void Update(){
    PrintRef(ref dogS);
    print ("Structure:The dog's name is "+dogS.name +" and is "+dogS.age);
}

당신은 키워드 ref에 대해서 알았다. 이것은 컴파일러에게 스택에 구조체를 복사하지 말고 구조체에 대한 참조를 만들라고 말하는 것이다. 함수 밖에서 구조체는 함수 안에서 받은 값들이 유지된다.
이 키워드는 어떤 값타입 형식과 사용될 수 있다.


Creating a Reference
참조 만들기


우리는 방금 전에 값타입 변수에 참조를 만드는 것이 가능하다는 것을 봤다. 우리는 함수 안에 정수형 변수를 만들고, 그것이 후에 사용되기 위해서 계속해서 유지되는 것을 원한다고 생각해보자.
간단하게, 함수내부에 변수를 선언하는 것은 함수의 끝에 사라지는 automatic 변수를 만드는 것이다.


우리는 동적으로 할당되는 변수를 만들 수 있다. 이는 참조와 함께 힙에 저장될 것이다.

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
using UnityEngine;
using System.Collections;
 
public class Test:MonoBehaviour{
    int number;
 
    void Update () {
        if(Input.GetKeyDown (KeyCode.C))
            CreateVariable();
        if(Input.GetKeyDown (KeyCode.Space))
            PrintVariable(number);
    }
 
    void CreateVariable(){
         //int number = new int();
         number = new int();
         number = 10;
        print ("In function "+number);
    }
 
    void PrintVariable(int n){
        print (n);
        n+=10;
    }
}

첫 번째로, 스크립트에 전역으로 정수형 변수 number를 선언한다. 우리는 이 변수를 CreateVariable함수에서 사용하고, new int()를 사용하여 number에 정수형 변수 참조를 대입한다.


이 상황을 명확히 하기 위해서, new 키워드는 힙영역에 만들어진 변수에 대한 참조를 반환한다.
이 참조는 number 변수안에 저장되고, 이제 number 변수는 새롭게 생성된 변수의 주소를 가지고 있다. number를 사용하는 것은 간접적으로 이 변수를 사용하는 것이다. 우리가 새롭게 생성한 변수는 함수 밖에서도 여전히 존재한다.









주석으로 된 부분을 고려해보면, (//int number = new int();)
만약 이 같은 방법으로 사용했다면, 우리는 함수의 끝에서 참조를 잃었을 것이다.
다음 청소 라운드에서, 가비지 컬렉터는 참조가 없는 데이터(함수 안에 선언된 number)를 찾을 것이고 그것을 메모리로부터 제거할 것이다.
이 같이 우리가 함수 내부에서 했던 것처럼 지역 변수를 선언하는 것을 생각해 보면, int number은 전역 int 
number 변수를 숨긴다는 것을 알 수 있다.


참조안에서 int같은 원시 변수를 유지할 때,  컴파일러는 boxing(박싱)이라고 불리는 함수를 수행한다. 이는 기본적으로 원시 변수를 클래스 안으로 싸는 것이다.
이런 식으로 선언된 변수는 힙에 할당되고, 이들이 해제될 때 가비지 컬렉션이 발생하도록 한다. 이들은 기본적인 원시 변수보다 더 많은 메모리를 차지한다.
(역주: 박싱에 대한 그림 참조)

기본적인 박싱 상황을 생각해보자. Debug.Log(object)함수를 사용할 때, 매개변수는 object이다. 
이는 어떤 변수의 객체도 함수로 전달할 수 있다는 의미이다. 그러나 정수형을 전달할 때, 컴파일러는 정수형을 오브젝트 안으로 넣어 버린다. 이렇게 처리되는 것을 예상함으로인해 박싱을 최대한 적절하게 사용하는 것이 가능하다.



1
2
3
4
5
void PrintingManyTimes(int n){
    object obj = n;
    for(int i = 0;i<500;i++)
        Debug.Log(obj);
}
이 상황에서 컴파일러는 매번 Debug.Log를 호출할 때마다 박싱을 수행하는 것이 필요가 없다. 박싱은 한번 일어나고 500번 호출될 뿐이다.






Static
정적

우리는 이제 튜토리얼의 마지막 부분으로 가고 있다. 한번 더 말하지만, 당신이 C에 대한 배경지식을 가지고 있다면, 당신이 static(정적)에 대해서 아는 것은 잊어 버려라. 
여기서의 static의 사용은 다르다.


.NET의 정적 변수는 High Frequency Heap이라고 불리는 Heap의 특별한 공간에 저장된다.

(역주: High Frequency Heap에 대한 추가 설명)
정적 변수가 참조타입이든 값타입이든 상관없이 모든 정적 변수는 힙에 저장된다.
얼마나 많이 인스턴스가 만들어졌는지 상관없이 통틀어서 오직 하나의 슬롯(공간)만이 있다.
(그렇지만 그 공간을 채우기 위해 인스턴스를 만들필요는 없다.)
이 힙은 "High frequency heap"으로 알려져 있으며, 이 힙은 보통의 가비지 컬렉션되는 힙으로 부터 분리되어 있다. 이는 어플리케이션 도메인당 하나씩 존재한다.


앞에서 언급한바와 같이, 우리는 튜토리얼에서 종종 정적변수를 만나며, 초심자는 단지 쉽게 사용할 수 있기를 기도한다. 점잖은 교사는 종종 정적변수의 이익과 위험에 대해서 충분히 설명하는데 시간을 쓰지 않는다. 우리는 이제 정적 객체가 무엇인지, 어떻게 사용하는지, 어디에 그것을 사용하고, 사용하지 않는지에 대해서 명확히 하려고 한다.

여기 정적 변수의 개념을 소개한 빠른 비디오 영상이 있다.(영어)

Static classes
정적 클래스


정적 클래스는 객체 인스턴스를 가지지 않는 클래스이다. 당신은 정적 클래스의 객체 인스턴스를 만들 수 없을 것이다.

그 결과로, 오직 하나의 인스턴스를 가진다. 방금 내가 "그것의 어떤 인스턴스도 만들 수 없다"고 말한 것때문에 혼란스러울수도 있다.
프로그램은 Heap에 이 정적객체를위해 메모리를 할당할 것이다. 그리고 유저의 정적객체에 대한 어떤 인스턴스화도 허용하지 않을 것이다.

정적 객체는 그들의 첫번째 호출에서 생성되며, 프로그램이 끝날 때 파괴될 것이다.
(혹은 크래쉬가 일어나거나..)


1
2
3
4
public static class GameManager{
    public static int score; 
    public static float amountOfTime;
}

당신은 GameManager타입의 객체를 선언할 수 없을 것이다.
GameManager GM = new GameManager();
이는 에러를 리턴할 것이다.


우리는 간단하게 다음의 명령어를 통해서 정적 클래스의 멤버에 접근할 수 있을 것이다.
GameManager.score = 10;
이는 게임안에서 언제 어디서나 가능하다. 정적 클래스 또는 멤버는 모든 파일안에서 접근가능하고 수명또한 전체 프로그램의 수명과 같다.


An example for using static class
정적 클래스 사용 예시

새로운 씬을 불러올 때, 이전 씬의 모든 변수는 파괴된다. 만약 예를들어 score같은 변수를 보존하기를 원한다면 다음과 같은 해야한다.
DontDestroyOnLoad(score);
정적 클래스의 유용한 장점은 그들의 수명에 있다. 정적 클래스는 죽지 않기때문에, 만약 씬을 불러올 때 score변수가 살아남기를 원한다면, 우리는 간단하게 정적 클래스를 사용할 수 있다. 레벨의 끝부분에 저장되기 원하는 값을 정적 멤버에 저장하고, 새로운 씬을 시작할 때 그 값을 복원한다.

if(newLevel){
    GameManager.score = tempScore;
    Application.LoadLevel(nextLevel);
}

그리고 새로운 레벨에 대한 스크립트에서:

1
2
3
void Start(){
    scoreTemp = gameManager.score;
}

여기서 작은 트릭은, 끝날때까지 GamaManager.score를 사용하지 않는 것이다.

한 레벨당 최대 200포인트를 얻을수 있는 레벨이 10개가 있는 게임을 생각해보자.

한 유저는 한번에 게임을 끝냈다. 그리고 2000포인트를 얻었다.

다른 유저는 여러번 플레이해서 게임을 끝냈고, 5000포인트를 얻었다.

두번째 플레이어는 좋지 않을 뿐만 아니라 더 많은 포인트를 얻었다. 왜냐하면 그는 각 레벨을 여러번 시도했고 그 포인트가 축적되었다. 만약 다음 레벨을 불러올 때 정적변수에 포인트를 전달한다면, 레벨을 재시도할때는 반드시 축적된 포인트를 취소해야 한다.

게임을 나가는 동안, 정적 변수의 데이터는 저장공간으로 보내질 수 있다.


정적 클래스와 정적 변수는 프로그램 실행중에 그들이 처음으로 맞닥뜨릴때 할당된다.
당신이 실제로 A 정적 클래스의 변수나 함수에 한번도 접근하지 않는다면 A 정적 클래스는 절대 만들어 지지 않는다.



- 유니티안 Dexter님께서 http://unitygems.com/memorymanagement/ 의 원문을 번역하여 올려주신 걸 따로 가지고 있기 위해 올림.

덱스터님 의 원문은 [여기]를 참고해주세요 ^_^

'Unity > 메모리관리' 카테고리의 다른 글

itween은 되도록 자제...  (0) 2014.08.12
유니티에서의 메모리 관리  (0) 2014.04.28
유니티 메모리 관리 4 (마지막)  (2) 2013.10.16
유니티 메모리 관리 2  (0) 2013.10.16
유니티 메모리 관리 1  (0) 2013.10.16

WRITTEN BY
빨강꼬마

,

참조형식과 힙


참조형식 변수는 참조형으로 메모리에 저장된 객체를 말한다. 새로운 클래스 인스턴스를 만들 때,
이들의 데이터 위치의 참조변수를 만들뿐만 아니라 데이터도 힙에 저장된다.
이 참조는 값형식으로 객체의 주소값을 가지는 변수이다.(참조변수) 클래스의 인스턴스를 만들기 위해서 new 키워드를 사용하는데, 이는 OS에게 객체의 타입에 맞는 메모리 공간과 객체를 초기화 하는데 필요한 코드를 수행하도록 요청한다. 밑에 Dog타입의 객체의 예시를 봐라.

(역주: 위에서 말한 참조형식 변수는 Class, Object, String같은 클래스를 의미한다. 이는 힙에 저장되며
참조변수는 객체의 주소값을 의미하는데, 이는 스택에 저장된다. 
참조 형식 변수와 참조변수가 햇갈리지 않도록 주의할 것)



1
2
3
4
5
6
7
8
public class Dog{
     public string name { get; set;}
     public int age { get; set;}
     public Dog(string s, int n){
           name = s;
           age = n;
    }                         
}
01
02
03
04
05
06
07
08
09
10
11
public class Test : MonoBehaviour {
    Dog dog1 = new Dog("Rufus",10);
    Dog dog2 = new Dog("Brutus",8);
 
    void Update (){                                                 
        if(Input.GetKeyDown(KeyCode.Alpha1))
            print ("The dog1 is named "+dog1.name+" and is "+dog1.age);
        if(Input.GetKeyDown(KeyCode.Alpha2))
             print ("The dog2 is named "+dog2.name+" and is "+dog2.age);
    }
}

dog1과 dog2 변수는 메모리안에서 이들 객체와 연관된 데이터가 저장된 위치를 참조한다.



밑에 Instantiate 함수와 관련된 행동을 봐라. 
우리는 3D모델이 포함된 Enemy 프리팹과 약간의 컴포넌트와 스크립트를 가지고 있다고 생각하자.

01
02
03
04
05
06
07
08
09
10
11
using UnityEngine;
using System.Collections;
 
public class Test : MonoBehaviour{
    public GameObject enemyPrefab;
 
    void Update(){
        if(Input.GeyKeyDown(KeyCode.Space))
               Instantiate(enemyPrefab,new Vector3(0,0,0), Quaternion.identity);
    }
}

우리는 새로운 enemyPrefab의 인스턴스를 만들었고, 그것은 사용되고 있다. 우리는 새로 생성한 enemyPrefab의 인스턴스를 참조하지 않고 있기 때문에, 만약 인스턴스에 접근하기 원한다면 문제가 생길 것이다. 우리가 새롭게 생성한 객체에 어떤 행동을 적용하길 원한다면 우리는 그 객체를 참조하는 변수를 만들어야 한다.

1
2
3
4
5
6
7
void Update(){
    if(Input.GeyKeyDown(KeyCode.Space))
       GameObject enemyRef = Instantiate(enemyPrefab, new Vector3(0,0,0), Quaternion.identity);
        enemyRef.AddComponent(Rigidbody);
        Destroy(enemyRef);
     }
}

예를 들어, enemyRef는 선언되었고, 새로 생성된 객체의 주소값을 할당받았다. enemyRef는 이제 그 객체를 참조한다. 어떻게 우리가 Componet를 추가하는지 봐라. 이 변수는 객체의 위치를 알기때문에 객체의 public변수에 접근하거나 추가하는 것이 가능하다. 마지막 줄을 보면 객체가 파괴되는데 이런 방식으로 거의 코드를 짜지 않지만, 이것은 단지 예시를 위한 목적이다.

참조변수는 오직 if문{ } 안에서만 생존하는 automatic 변수이다.
그 밖에서는 참조변수는 더 이상 존재하지 않으며, enemy 객체를 다시 찾기위해서는
우리는 다음과 같은 방법을 사용할 수도 있다.
GameObject enemyRefAgain = GameObject.Find(“Enemy”);

참조형식 변수는 힙공간에 저장된다. 그들은 다음과 같다. 
(역주: 위에서도 말했지만 참조형식 변수의 주소값을 가지는 참조변수는 스택에 저장된다.)

  • class   
  • object 
  • string 
    (string이 변수처럼 보일지 몰라도, 사실은 Class String의 실제 객체이다.)
값형식 변수와 참조형식 변수의 주된 차이는 데이터가 저장된 곳을 가리키는 참조변수의 추가이다.
참조변수는 우리가 데이터를 찾기전에 첫번째로 접근해야한다.
이것은 클래스가 직접적으로 바로 접근할 수 있는 구조체보다 느리게 만든다.


다른 주요 문제는 다음과 같은 연산이다.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
using UnityEngine;
using System.Collections;
 
public class Memory : MonoBehaviour {
    DogC dogC1 = new DogC();
    DogC dogC2 = new DogC();
    int number1;
    int number2;
 
    void Update () {
        if(Input.GetKeyDown(KeyCode.Space)){
           number2=number1 = 10;
           number1 = 20;
           print (number1+" "+number2);
           dogC1.name = "Rufus";
           dogC1.age = 12;
           dogC2 = dogC1;
           dogC1.name = "Brutus";
           print (dogC1.name+" "+dogC1.age+" "+dogC2.name+" "+dogC2.age);
        }
    }
}

우리는 하나가 다른 하나를 수정하지 못한다는 것을 보았을 때 두개의 number변수는 독립적이라는 것을 알게 될 것이다.

반면에, 클래스에 다른 클래스를 대입할 때
하나의 수정은 다른 하나에 영향을 미친다.

이것은 dogC2 = dogC1를 할때 우리는 dogC1의 주소를 dogC2 참조 변수에 대입하기 때문이다.
메모리에서 dogC2의 원래 주소값을 잃어 버리고
dogC1또는 dogC2를 바꾸는 것은 같다.
우리는 하나의 지역을 가리키는 2개의 참조변수를 가지고 있다.


수명과 범위 그리고 가비지 컬렉션

참조형식 변수인 경우에, 수명을 가비지 컬렉터(GC)가 담당하기 때문에 불분명하다.
C와 C++의 옛시절로 돌아가보면, 동적으로 할당된 변수가 메모리로부터 제거되는 것은 프로그래머의 의무였다. 
(C에서는 Free(), C++에서는 ~(파괴자)에서 담당한)


C#에서, 가비지 컬렉터는 변수가 아직 사용되고 있는지, 그것(실행코드안에 있는)을 참조하고 있는 것이 있는지 없는지 확인할 것이다. 그리고는 자유롭게 사용가능한 메모리 지역을 표시할 것이다.
COM인터페이스 또는 다른 레퍼런스 카운팅 시스템에 익숙한 프로그래머는 종종 가비지 컬렉션의 이상한 개념을 발견한다. 왜냐하면 레퍼런스 카운팅 시스템에서 서로를 참조하는 두 객체를 가지는 것은
(부모가 자식을 참조하고, 자식이 부모를 참조하는것 같은) 메모리가 절대 해제되지 않을 수 있기 때문이다. 가비지 컬렉터는 이러한 제약을 가지지 않는다.


가비지 컬렉터는 큰 주제이고, 유니티는 콜렉션 프로세스의 수행에 관한 많은 특이점을 가지고 있다. - 만약 당신이 전통적인 C#에 익숙하다면 기본적으로 GC는 짧은 수명을 가지는 오브젝트에 대해 처리비용이 싸야된다고 예상하겠지만, 유니티에서는 그렇지 않다.

메모리 블럭이 가득찰 때마다, 시스템은 힙에 할당된 모든 객체에 대한 도달가능성(객체에 대한 참조가 있는지 없는지)을 확인해야 한다. 이것은 그들이 다른 코드로 부터 접근되고 있는지 혹은 실행되고 있는지를 확인하는 것을 의미한다. 만약 그들이 실행되고 있거나 접근되어지고 있다면 메모리에 유지될 것이며, 그렇지 않으면 해제가 될 것이다.
일반적인 .Net 가비지 컬렉션은 제너레이션(세대)안에서 발생한다. 이는 도달가능성 테스트와 관련된 처리를 최소화하도록 하는데, 새롭게 만들어진 객체(1세대)를 먼저 검사하고, 그래도 여전히 메모리가 부족하다면 오래된 객체(2세대, 3세대)를 검사한다.
유니티는 항상 모든 객체를 검사하고 그래서 가비지 컬렉션은 주된 과부하 요소이며,
일반적으로 .NET 언어에서 예상되는 것보다 훨씬 크다.

도달가능성 테스트는 가비지 컬렉션 뒤에서 작용하는 마법같은 것이다. - 
이는 컬렉터가 현재 어떤 코드가 실행되고 있는지 혹은 실행될 것인지를 알아내도록 한다. 
왜냐하면 컬렉션의 후보가 되는 객체를 참조하는, 라이브 참조변수가 있을 수도 있기 때문이다.
이는 함수안에서 클로저의 사용을 포함한다. - 그것은 매우 복잡하고, 매우 강력하며, 그러나 게임을 빠르게 하는 방법은 아니다.
클로저는 지역 변수에 대한 참조 또는 함수 내부 안에 정의된 익명의 함수안의 파라미터에 의해 만들어 진다. 익명의 함수가 호출될 때, 그 루틴이 실행했을 때와 동일하게 하기 위해서 변수의 값이 유지된다 

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
List<Action> actionsToPerformLater = new List<Action>();
 
void DisplayAMessage(string message)
{
      var t = System.DateTime.Now;
      actionsToPerformLater.Add(()=>{
          Debug.Log(message + " @ " + t.ToString());
      });
}
 
void SomeOtherFunction()
{
      DisplayAMessage("Hello");
      DisplayAMessage("World");
      SomeFunctionThatTakesAWhile();
      DisplayAMessage("From Unity Gems");
 
      foreach(var a in actionsToPerformLater)
      {
           a();
      }
 
}

코드내부에서 DisplayAMessage 함수를 호출할 때 마다, 전달 받은 message와 현재 시스템 타임(t)에 대한 클로저를 생성한다. 우리는 끝에 있는 foreach 루프를 실행할 때마다.
디버그 메시지는 전에 리스트에 추가하기 위해 함수를 호출했을 때 전달된 파라미터와 함께 기록될 것이다.
클로저는 매우 강력한 도구이며 그것은 다음 글에 대한 주제가 될 것이다.

가비지 컬렉션은 시스템이 연산을 위해 메모리가 충분치 않다고 판단될 때 일어난다. 이는 버려진 객체가 상당한 기간 동안 수거되지 않을 수도 있다는 것을 의미한다. - 그러므로 객체가 더 이상 필요하지 않을 때 명시적으로 외부의 연결을 끊는 것 또는 외부 리소스를 해제하는 것은 흔히 있는 일이다.
(역주: 언제 가비지 컬렉션이 발동될지 모르기 때문에 수동으로 메모리를 해제한다는 의미이다.)

 명시적으로 내부의 객체(당신의 프로젝트에 있는)를 해제할 필요는 없다. 그러나 스트림(파일)과 시스템에 영향을 주는 데이터베이스 연결을 가지는 프로젝트의 외부에서 사용할 경우에는 필요하다.
예를 들어 파일을 닫지 않는 것은 다음에 파일에 접근할 때 실패하게 만든다.
파일은 여전히 버려진 객체에 의해 열려있기 때문이다.
가비지 컬렉션은 매우 비싼 연산이며, 게임에 영향이 없을 때 - 레벨을 로드할 때, 플레이어가 일시정지 메뉴로 들어갔을 때,  플레이어가 알지 못하는 많은 경우 - 수동적인 가비지 컬렉션의 가동은 적절하다. 
System.GC.Collect(); 명령어를 통해서 수동적으로 가비지 컬렉션을 작동시킬 수 있다.

참조타입 변수에 관해서, GameObject.Find()를 사용하는 것은 어떤 스크립트에서든 객체에 대한 참조를 리턴한다. 이처럼 객체를 찾기 위한 적절한 행동을 한다면, 프로그램안에서 어디서든 접근이 가능하다. 
게임 플레이 동안 클래스의 인스턴스를 찾는 방법에 대한 좀 더 자세한 설명은 GetComponent 튜토리얼에 나와있다.

- 유니티안 Dexter님께서 http://unitygems.com/memorymanagement/ 의 원문을 번역하여 올려주신 걸 따로 가지고 있기 위해 올림.
덱스터님 의 원문은 [여기]를 참고해주세요 ^_^


'Unity > 메모리관리' 카테고리의 다른 글

itween은 되도록 자제...  (0) 2014.08.12
유니티에서의 메모리 관리  (0) 2014.04.28
유니티 메모리 관리 4 (마지막)  (2) 2013.10.16
유니티 메모리 관리 3  (0) 2013.10.16
유니티 메모리 관리 1  (0) 2013.10.16

WRITTEN BY
빨강꼬마

,


만약 당신의 게임이 실행되고 있는 컴퓨터나 
장치의 

메모리에서 떤 일이 일어나는지 궁금했던 적이 있다면 

이 글은 당신에게  좀 더 효율적인 게임을 만들고 클래스와

함수의 메모리사용을 최적화하는데 도움을 줄 것이다.

이번 글에서는
  • 메모리 구역
  • 값타입과 스택
  • 참조타입과 힙
  • 구조체와 클래스
  • 참조타입을 만드는 것
  • 정적 클래스
  • 정적 변수
  • 정적 함수
  • 힙 파편화, 오브젝트 풀링, 가비지 컬렉션
이 튜토리얼은 유니티 개발을 위해 C#을 사용하게된 C/C++ 프로그래머에게 특별히 유용하다. C#은 처음에는 혼란과 마법같은 방식으로 메모리를 다루고, 그것은 그것 자체의 위험과 특이한 방식을 가지고 있다.
유니티 프로그래밍을 시작할 때 그리고 일반적인 프로그래밍을 시작할 때 변수 타입의 용도에 많이들  머리를 섞일 것이다. 예를 들어, 왜 어떤때는 변수들이 업데이트되고 다른 변수들은 똑같은 값으로 유지되며, 왜 내가 여전히 필요로 하는 변수는 사라지는지 말이다.

또 다른 초심자의 공통적인 문제는 정적 변수의 사용이다. 당신은 종종 인터넷의 다양한 튜토리얼에서 정적 변수를 만나지만, 강사는 시간을 들여 명확하게 설명을 해주지 않으며, 오직 "변수에 대한 쉬운 접근"이라고만 말한다.

문제는 정적 변수(다르게 클래스 변수라고 불리는)는 쉬운 주제가 아니라는 것이다.

몇몇은 심지어 정적 변수로 작성된 것은 다른 타입으로 처리가 될 수 있다며, 그것을 사용하는 것을 피하라고 한다. 우리는 그것이 진실인지, 그것이 어쩔수 없는 최고의 해법이 아닌지에 대해 알아 볼 것이다.


시작하기에 앞서, 당신이 C 프로그래밍에 대한 지식을 가지고 있거나 객체지향 프로그래밍에 대해 잘 알지 못한다면, 당신이 메모리 관리와 저장 키워드에 대해 알고 있는 것은 OOP(객체지향 프로그래밍)에서는 전체적으로 유사하지 않다는 것을 명심해라.
C#의 C를 연관시키지 않으려고 노력해라.
이 튜토리얼에서, 우리는 각각의 메모리 지역을 다룰것이며, 값형식, 참조형식, 동적메모리 할당 그리고 정적 변수도 다룰것이다.


메모리 지역

프로그램이 시작할 때, 운영체제는 메모리의 일정부분을 프로그램에 할당한다.  할당된 메모리에는 4개의 메모리 지역이 존재하는데, 콜 스택, 힙, 레지스터 그리고 정적 메모리 지역이다.

C#언어 개발자는 친절하게도 4개의 메모리 지역을 2개로 좁혔는데, 2개의 지역이 바로 
스택과 힙 영역이다. 스택은 순서가 있고 빠르지만 제한적이다. 힙은 무작위이고, 크지만 느리다.

프로그래머는 각각의 변수에 어느 메모리 영역을 사용할 것인지를 변수를 사용하는 목적과 변수의 생명주기를 고려하여 결정할 수 있다.

메모리 할당을 위한 3개의 키워드가 있다. 바로 auto, static(우리가 이제 막 다룰려고 하는 것), extern이다.
(C언어에서는 여기에 register라는 키워드가 추가되어 총 4개의 키워드가 존재한다.)

extern 키워드는 아마 본적이 있을 것이다. 그것은 당신의 프로젝트에 있는 다른 코드에 선언된 변수를 선언한다는 의미이다. 
예를 들면 .NET 언어가 아닌 다른 언어로 쓰여진 DLL파일에 포함된 변수가 있다.


만약 당신이 C 프로그래머라면, extern의 의미는 C#과 미묘하게 다르다는 것을 알아야 한다. C#에서는 managed memory에 할당되지 않는 변수를 참조한다.

*(역주: managed memory와 unmanaged memory에 대해)
이것은 모두 똑같은 물리적 메모리이다. 차이는 단지 누가 그것을 제어하는가이다.
마이크로소프트는 managed memory를 Garbage Collector(GC)에 의해서 정리된다고 정의한다. 예를 들어, 정기적으로 물리적인 메모리의 어떤 부분이 사용되고 사용되지 않는지를 알아내는 프로세스가 여기에 해당된다.

unmanaged memory는 그밖의 다른 것에 의해서 정리된다.
예를 들어 당신의 프로그램자체내에서 또는 운영체제가 여기에 해당된다.
1
extern int number;

컴파일러는 number를 메모리에 할당하지 않는다. 그리고 변수는 다른 어느 곳에서 발견 될 것이라고
예상한다. 우리는 더이상 이 주제에 대해서 연연에 할 필요가 없을 것이다.
이 링크에서 더 많은 정보를 찾을 수 있을 것이다.


값형식과 스택

auto는 automatic 변수를 나타낸다. 하지만 C#에서 더이상 사용되지 않기 때문에 당신은 이 키워드를
한번도 보지 못했을지도 모른다.

1
2
int variable = 10;
auto int variable = 10;

두 라인은 정확하게 같다.

automatic 변수의 특징은 콜 스택에 할당된다는 것이다. 
스택에서는 오직 맨윗부분에 변수를 추가하거나 제거할 수 있고, 맨 아랫 부분에는 변수를 추가하거나 삭제를 할 수가 없다.  이것은 접시 한무더기와 비슷하다. 밑에 그림을 봐라


가장 위에 있는 스택을 알기위해서 프로그램은 스택 포인터를 사용한다. 스택 포인터는 CPU안에 있으며, 위 그림에서 보이는 검은색 화살표에 의해서 나타나는 현재 위치를 추적한다. 변수를 추가할 때 마다 스택 포인터는 증가된다.(또는 자신이 사용하는 OS의 메모리 관리에 따라 감소될수도 있다.)

스택은 함수 호출을 위해 사용되며, 전체 프로그램은 다른 함수들을 차례로 호출하는 Update함수를 호출하는 메인 함수이다. 
그래서 당신의 스크립트안에 있는 변수는 다른 어떤 것으로 정의되어 있지않다면 stack에 저장되는
automatic 변수이다.

또한 스택 프레임(Stack frame)에 대한 개념을 만날 것이다. 간단히 말해서 이것은 현재 실행된 함수에 의해 지역적으로 할당된 변수들의 집합이다.(또한 return 주소값을 포함하는데, 이것은 해당 함수가 리턴되고 난후에 바로 다음에 실행될 명령 주소를 뜻한다.)
컴파일러는 스택 프레임의 끝을 가리키는 포인터를 유지하며, 실제로는 당신의 로컬 변수가 차지하는 메모리를 찾기위해 이 포인터로부터 -offest을 사용한다.
이것의 실제적인 결말은 함수가 리턴되고 난 후이다. 그리고 그 함수의 로컬 변수가 사용된 공간들중 어느 것도 사용되거나 할당되지 않는다. (다음 함수 호출을 위한 여분의 공간을 제외하고는)

만약 재귀 프로그램(함수가 자기자신을 호출하는) 작성한다면, 어쩌면 스택의 공간을 모두 써버릴수도 있다.
각 함수 호출은 모든 지역변수와 리턴 주소값이 스택에 존재하기 위한 공간을 요청한다.
만약 당신의 코드에 그들 스스로 멈추지 않고 실행되는 루틴이 존재한다면, 아마 스택 오버플로우 메시지를 보게 될 것이다.

그래서 생명주기와 automatic 변수의 범위는 서로 관련되어 있다. 변수는 중괄호{와 }안에서 존재한다.
이것은 설명이 필요하다.


01
02
03
04
05
06
07
08
09
10
11
void Update(){
    int number = 10;
    Fct(number);
    print(number);
}
 
void Fct(int n){
    int inside=20;
    n = n + inside;
    print(n);
}


업데이트는 호출되고 첫번째 명령어는 변수 선언이다. 스택포인터는  밀어올려지고, number의 값(10)은  메모리 장소에 위치한다.
두번째 명령어는 함수 호출이다. 함수를 호출할 때 프로그램은 멈추고, 함수의 주소로 이동하며,
프로그램의 원래의 위치로 되돌아 가기위한 약간의 데이터는 스택에 전달된다.
매개변수는 원래의 값을 복사하여 스택에 저장된다.
변수 number는 Fct함수에서 사용되기위해 스택에 저장되지 않고 number의 값을 복사한 새로운 변수 n이 만들어져 스택에 저장된다.
제 Fct함수안에는 number 변수의 값을 가지고 있는 변수 n을 가지고 있다. 그렇다고 Fct함수에 number함수가 존재하는 것은 아니기때문에 number변수에는 접근할 수가 없는 것이다.
함수안에서 새로운 변수 inside가  선언되었다. 스택은 다음과 같은 상태이다.(그림에서는 스택의 상황을 매우 단순화 시켰다는 것을 명심하라)


함수의 끝부분에, n은 출력되고 30의 값을 가진다. 함수 내부에서 만들어진 모든 변수는 파괴되고 사라진다. 그것은 n과 inside을 포함한다. 변수가 여전이 메
모리에 존재할지라도, 그들은 간단하게 무시되고 시스템에 의해 더 이상 고려되지 않는다.

Update함수로 돌아왔을 때, 원래 값 10을 가지는 number변수를 출력하는데, Fct함수에서 사용된 것은 number변수의 복사값이기 때문이다.
스택 포인터는 n과 inside의 위치를 잃어버리는데, 만약 Update함수로 돌아와서 다시 n과 inside함수에 접근하려고 한다면 컴파일러는 그 변수는 현재 지역에 존재하지 않는다는 에러메시지를 반환할 것이다.

우리는 automatic 변수는 범위는 중괄호{, }에의해 정의 된다는 것을 보았다. 그 밖에서는, 변수는 존재하지 않고 보이지 않는다. 중괄호는 함수, if문또는 반복문의 범위를 표시할 수 있다.

변수의 수명과 범위

수명은 프로그램안에서 변수가 존재하는 시간을 의미하며, 범위는 프로그램에서 변수가 보이는 지역을 정의한다. automatic 변수의 경우에는 수명과 범위가 유사하다.

값타입의 변수는 스택에 저장된다. 그들은 다음과 같다

  • int
  • unsigned
  • float
  • double
  • char
  • struct
  • bool
  • byte
  • enum
  • long
  • short
그 당시에 존재하지 않았던 bool형을 제외한 거의 모든 타입을 C로부터 상속받았다.

automatic 변수에 대해서, 그것의 수명은 compiler에 의해 관리된다. 선언된 범위의 끝은 변수의 끝을 표시한다. 프로그래머는 메모리 지역을 해제하는데 신경쓰지 않아도 된다. 그것은 자동으로 처리된다.


- 유니티안 Dexter님께서 http://unitygems.com/memorymanagement/ 의 원문을 번역하여 올려주신 걸 따로 가지고 있기 위해 올림.
덱스터님 의 원문은 [여기]를 참고해주세요 ^_^



'Unity > 메모리관리' 카테고리의 다른 글

itween은 되도록 자제...  (0) 2014.08.12
유니티에서의 메모리 관리  (0) 2014.04.28
유니티 메모리 관리 4 (마지막)  (2) 2013.10.16
유니티 메모리 관리 3  (0) 2013.10.16
유니티 메모리 관리 2  (0) 2013.10.16

WRITTEN BY
빨강꼬마

,