레이블이 Unity3D_최적화인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Unity3D_최적화인 게시물을 표시합니다. 모든 게시물 표시

2014년 1월 27일 월요일

C# 메모리 관리 기법 - WeakReference와 IDisposable

상식적으로 가비지 컬렉터가 작동된다면 메모리가 늘어나는 것에 대한 걱정은 없어야 하겠지만 실제로는 메모리가 늘어나기도 합니다. 
C#에서는 메모리 해제 명령이 없어서 메모리 릭이 발생하게 되면 당황스러울 수 밖에 없죠.

그러면 어떤 경우에 메모리가 늘어나게 될까요?

가비지는 더 이상 참조가 없는 메모리를 뜻합니다. 
메모리가 어디선가 늘어나고 있다는 뜻은 결국 어디선가 의도하지 않은 참조가 일어나고 있어서 가비지화 되지 못하고 계속 누적되고 있다는 말이 되겠죠.

어떤 경우에 그런 일이 발생할까요?


이렇게 캐릭터 매니져가 세개의 캐릭터를 만들었습니다. 


그리고 캐릭터의 위치를 보여주는 객체가 캐릭터 매니저에 접근해서 캐릭터를 모두 참조하게 됩니다. 


그 후에 필요 없어진 캐릭터를 캐릭터 매니저가 삭제하게 됩니다. 
그러면 가비지가 될것이라 생각하겠지만 여전히 지워진 캐릭터를 디스플레이 캐릭터 포지션 객체가 참조하고 있기때문에 가비지가 되지 않습니다. 
여전히 메모리에 남아서 메모리를 증가시키게 되죠.

이런 일을 만들지 않으려면 위의 경우처럼 의도치 않은 참조를 지워줘야 합니다. 

WeakReference는 가비지 컬렉션에 의한 객체 회수를 허용하면서 객체를 참조하게 됩니다. 
이때, 인스턴스를 참조하려면 WeakReference.Target을 사용하는데 해당 인스턴스가 가비지 컬렉터에게 회수되면 null값을 반환하게 됩니다. 
public class Sample
{
    private class Fruit
    {
        public Fruit(string name) 
        { 
            this.Name = name; 
        }

        public string Name 
        { 
            private set; 
            get; 
        }
    }

    public static void TestWeakRef()
    {
        Fruit apple     = new Fruit("Apple");
        Fruit orange    = new Fruit("Orange");
           
        Fruit fruit1    = apple;   // 강한 참조

        // WeakReference를 이용 
        WeakReference fruit2    = new WeakReference(orange); 

        Fruit target;
           
        target          = fruit2.Target as Fruit;

        // 이 경우 결과는 애플과 오렌지가 나오게 됩니다.
        Console.WriteLine(" (1) Fruit1 = \"{0}\", Fruit2 = \"{1}\"", 
            fruit1.Name, target == null ? "" : target.Name);

        // 모두 참조하지 않도록 null값을 넣어줍니다.
        apple   = null;
        orange  = null;

        // 가비지 컬렉터를 작동시킨다
        System.GC.Collect(0, GCCollectionMode.Forced);
        System.GC.WaitForFullGCComplete();

        // 그 후 같은 방법으로 결과를 확인해보면
        // fruit1과 fruit2의 값을 바꾼 적은 없지만, fruit2의 결과가 달라집니다.
        target          = fruit2.Target as Fruit;

        // 결과는 애플만 나오게 된다.
        // 오렌지는 가비지 컬렉터에게 회수되버렸기때문입니다
        Console.WriteLine(" (2) Fruit1 = \"{0}\", Fruit2 = \"{1}\"", 
            fruit1 == null ? "" : fruit1.Name,
            target == null ? "" : target.Name);
    }
}
매니저처럼 객체를 직접 생성하고 삭제하는 모듈이 아닌 이상 가능하다면 WeakReference를 사용하시는 것이 좋습니다. 
그렇게 해서 의도치 않은 참조로 인해 메모리가 누적되게 되는 실수를 방지할 수 있습니다. 

주의할 것은 WeakReference.Target의 값을 보관하면 안된다는 것입니다. 
이값을 보관하게 되면 강한 참조가 일어나 가비지 컬렉터가 회수를 하지 않게 됩니다. 

C#은 메모리를 원하는 시점에 정확히 해제하는 것이 불가능합니다만 C/C++처럼 원하는 시점에 삭제하기를 희망한다면 IDisposable을 사용할 수 있습니다. 

이것은 관리되지 않는 메모리(리소스)들을 해제할때 사용하는 인터페이스입니다.
서로 다른 type의 객체라 하더라도 모든 type의 메모리를 정리할 수 있는 장점이 있습니다. 

WeakReference와 IDisposable을 같이 사용해서 원하는 시점에 메모리를 해제할 수 있습니다. 
아래의 예제는 Disposable 인터페이스를 상속받아 구현되었습니다.
namespace MyApp
{
    public class SampleChar : IDisposable
    {
        private IRenderObject m_Render = Renderer.CreateRenderObject();

        public void Dispose()
        {
            // 이후에 더이상 업데이트가 되지 않도록 여기서 제거하면
            SampleCharManager.Remove(this);

            m_Render = null;
        }

        public bool isRemoved 
        { 
            get 
            { 
                return m_Render == null; 
            } 
        }

        public void Render()
        {
            // 화면에서 그려지지 않도록 합니다. 
            if (m_Render == null) return;
        }

        public void Update() { }
    }
}
IRenderObject 인터페이스 구현은 아래와 같습니다.
namespace MyApp
{
    public interface IRenderObject
    {
        void Render();
    }

    public static class Renderer
    {
        public static IRenderObject CreateRenderObject()
        {
            // IRenderObject를 상속받은 더미 객체
            return new DumyRenderObject(); 
        }
    }
}
아래의 코드는 캐릭터 매니저가 등록된 캐릭터들을 일괄적으로 업데이트 시키고 렌더링하는 코드입니다. 
namespace MyApp 
{
    static class SampleCharManager
    {
        private static List<samplechar> m_list = new List<samplechar>();

        public static void Update()
        {
            foreach (SampleChar obj in m_list) 
            {
                obj.Update();
            }
        }

        public static void Render()
        {
            foreach (SampleChar obj in m_list)
            {
                obj.Render();
            }
        }

        public static void Add(SampleChar obj)
        {
            m_list.Add(obj); 
        }
        
        public static void Remove(SampleChar obj)
        {
            m_list.Remove(obj);
        }
    }
}
그 다음 디버깅을 위한 캐릭터의 위치를 표시하는 코드입니다. 
namespace MyDebug
{
    static class DisplayCharInfo
    {
        private static List<weakreference> m_list            = new List<weakreference>();
        private static Queue<weakreference> m_removeQueue    = new Queue<weakreference>();

        public static void Update()
        {
            foreach (WeakReference item in m_list)
            {
                MyApp.SampleChar obj = (item.Target != null) ? item.Target as MyApp.SampleChar : null;

                if (obj == null || obj.isRemoved)
                {
                    m_removeQueue.Enqueue(item);
                }
                // 삭제되지 않은 캐릭터의 정보만을 표시합니다.
                else 
                { 
                    /* 캐릭터 정보 표시 */ 
                }
            }

            // 삭제된 캐릭터는 목록에서 지워줍니다
            while(m_removeQueue.Count > 0)
            {
                WeakReference item = m_removeQueue.Dequeue();
                m_list.Remove(item);
            }
        }

        public static void Add(MyApp.SampleChar obj)
        {
            // WeakReference를 이용해 참조하도록 해줍니다.
            // SampleCharManager에서 캐릭터를 삭제하더라도 안전하게 가비지가 회수됩니다.
            m_list.Add(new WeakReference(obj));
        }
    }
}
Unity3D는 모노에서 관리하는 메모리와 엔진에서 관리하는 메모리로 나뉩니다. 
둘 다, 메모리가 부족하면 heap에 메모리를 할당하는데 이렇게 늘어난 메모리는 줄어들지 않습니다. 
heap에서 메모리가 재사용되므로 무작정 늘어날 일은 없지만 가비지가 늘어날수록 최대 메모리 사용량이 늘어나므로 가능하면 가비지가 덜 생성되록 코드를 짜는 것이 중요하겠죠.
메모리는 한번에 잡는 것이 좋고, caching이나 memory pool을 사용하는것이 좋습니다. 



2014년 1월 26일 일요일

C# 메모리 관리 기법 - Boxing

가비지로 인한 부하도 크지만 박싱으로 인한 부하도 무시할 수 없습니다. 

박싱이란 것은 Value type 객체를 Reference type객체로 바꾸는 것을 말하는데요.

C#에서는 모든 객체가 object로부터 상속됩니다. 
int, float같은 Value type조차도 object로부터 상속받은 것처럼 사용할 수 있습니다.

이런것들을 주의하지 않고 코드를 짜게 될경우 의도치 않게 박싱이 일어나는 곳들이 생기게 되고 이것이 누적되면 무시할 수 없는 속도저하가 일어나게 됩니다. 

아래의 예를 보시면, 리스트에 서로 다른 여러 종류의 type 값을 추가하는데 foreach문 안에서 item을 object로만 받아서 처리하고 있는것을 보실 수 있습니다. 
class MyClass
{
    public override string ToString() 
    { 
        return "다섯"; 
    }

    static public void Sample()
    {
        ArrayList list = new ArrayList();

        // 리스트에 아래처럼 여러가지 타입의 값을 추가해 넣어줍니다.
        // int형 float형 string형 등등.....
        list.Add(1);
        list.Add(1.5f);
        list.Add(‘3’);
        list.Add("four");
        list.Add(new MyClass());

        // 리스트에서 값을 꺼낼때 오직 object로 받아서 처리하고 있습니다.
        // 이런 경우 Value type을 Reference type으로 바꿔주면서 시간이 오래 걸리고
        // 변환하면서 heap에 저장이 됩니다. 
        // 시간도 오래 걸리고 가비지도 생기게 되겠네요.
        foreach (object item in list)
        {
            Debug.log(item.ToString());
        }
    }
}
그래서 여러 type을 처리해야만 하는 경우가 아니라면 값의 type을 명시할 수 있는 Generic collection의 사용이 좋겠습니다. 
Generic은 C++의 template과 비슷해서 C++의 STL container와 비슷하게 생겼습니다. 

아래의 예제에서 잘 사용한 경우와 그렇지 않은 경우를 비교해서 보세요.
class Example
{
    // 일단은 여러가지 type을 사용하지 않도록 분리해서 작업하는것이 제일 좋습니다. 
    // 분리가 끝나면 적절한 type을 명시할 수 있는 Generic collection을 사용하구요.

    // 좋지 못한 경우
    static public void BadCase()
    {
        // type에 대한 명시가 없는 어레이 리스트를 사용합니다.
        ArrayList list = new ArrayList();

        // 값은 int형인데
        int evenSum = 0;
        int oddSum  = 0;

        for (int i = 0; i < 1000000; i++)
        {
            list.Add(i);
        }

        // item을 object로 받아서 처리하게 되네요
        // 박싱이 일어납니다.
        foreach (object item in list)
        {
            if (item is int)
            {
                int num = (int)item;

                if(num % 2 ==0)
                {
                    evenSum += num;
                }
                else 
                {
                    oddSum += num;
                }
            }
        }
           
        Console.WriteLine("EvenSum={0}, OddSum={1}", evenSum, oddSum);
    }

    // 적절하게 잘 사용한 경우
    static public void GoodCase()
    {
        // type을 명시해줬습니다.
        List<int> list = new List<int>();

        // int형을 사용했구요
        int evenSum = 0;
        int oddSum  = 0;

        for (int i = 0; i < 1000000; i++)
        {
            list.Add(i);
        }

        // num을 int형으로 받아서 처리합니다. 
        // 박싱이 일어나지 않습니다.
        foreach (int num in list)
        {
            if (num % 2 == 0)
            {
                evenSum += num;
            }
            else
            {
                oddSum += num;
            }
        }
           
        Console.WriteLine("EvenSum={0}, OddSum={1}", evenSum, oddSum);
    }
}





C# 메모리 관리 기법 - instance

C#에서 클래스를 인스턴싱할때는 new를 해서 할 수 있습니다. 그런데 이런 식으로 생성하게 되면 heap에 메모리가 할당됩니다. 

그리고 이런것들이 계속 누적이 되면 가비지가 작동하게 됩니다. 

아래 예를 보세요.
static class TestMyVector
{
    public static void PrintVectorLength(float x, float y, float z)
    {
        // 이런것들이 heap에 메모리가 할당됩니다. 
        Vector3 v = new Vector3(x, y, z);
        Debug.log("v의 값 X = " + v.x  + "V의 값 Y = " + v.y + "V의 값 Z = " + v.z);
    }
}
이런 경우에 Vector 클래스를 구조체로 바꾸면 new를 통해서 생성하게 되도 heap에 메모리가 할당되지 않게 됩니다. 
구조체는 Value type이기때문에 stack에 할당되기 때문입니다. 
이런식으로 해서 가비지를 조금이라도 줄일 수 있습니다. 
아래의 예처럼요.
public class MyVector
{
    // 이런식으로 구조체를 만들고
    public struct MyVector
    {
        public float x, y, z;

        public MyVector(float x, float y, float z) 
        { 
            this.x = x;
            this.y = y; 
            this.z = z;
        }
    }
}

static class TestMyVector
{
    public static void PrintVectorLength(float x, float y, float z)
    {
        // 구조체를 이용해서 인스터스하면 이것은 stack에 할당됩니다.
        MyVector v = new MyVector(x, y, z);
        Debug.log("v의 값 X = " + v.x  + "V의 값 Y = " + v.y + "V의 값 Z = " + v.z);
    }
}
그런데 경우에 따라서는 구조체로 만드는 것이 애매한 경우가 있습니다. 
이런 경우엔 멤버변수로 만들어서 사용하는 방법이 있습니다. 
저도 주로 이 방법을 사용하는데요.. 가장 간편하기도 하고 멤버변수로 만들 경우 메서드 인자를 줄일수도 있고 여기 저기 사용하기 편하다는 장점도 있습니다. 
대신 잘못했을 경우 값이 도중에 바뀌어서 애매한 버그를 만들기도 합니다. ^^;;
하지만 개인적으로는 이 방법을 추천합니다. 
아래와 같이 멤버변수로 만들어서 사용할 수 있습니다. 
static class TestMyVector
{
    // 멤버변수로 만들어 놓습니다. 
    Vector3 tempVector3 = new Vector3(0.0f, 0.0f, 0.0f);

    // 이해를 돕기 위해 위와같이 만들었지만 x, y, z의 값이 모두 0인 경우
    // new를 사용하지 않고 
    // Vector3 tempVector3 = Vector3.zero
    // 이와 같이 쓸수도 있습니다. 

    public static void PrintVectorLength(float x, float y, float z)
    {
        tempVector3.x = x;
        tempVector3.y = y;
        tempVector3.z = z;
        Debug.log("tempVector3의 값 X = " + tempVector3.x  + "tempVector3의 값 Y = " + tempVector3.y + "tempVector3의 값 Z = " + tempVector3.z);
    }
}


2014년 1월 23일 목요일

C# 메모리 관리 기법 - string

유니티는 다양한 언어를 지원해서 개발을 좀더 수월하게 해주는데 저는 주로 Java보다 C#을 애용합니다. 

Java가 주는 간결하고 자유로움이 매력적이긴 하지만 개발이 진행될수록 그 자유스러움이 오히려 독이 되는 경우가 많다고 느껴지기 때문인데요. 

C#을 다루면서 참 코딩이 쉽다라고 느낀적이 참 많을 정도로 좋은 언어인 것은 틀림없습니다.

C#의 장점이라 할수 있는 특징들을 보면

  • 메모리 해제에 신경 쓰지 않아도 됩니다.
  • 이미 삭제된 메모리에 접근하는 실수를 방지해줍니다.
  • 잘못된 캐스팅으로 엉뚱한 메모리에 접근하지 않게 합니다.
  • 배열 크기보다 큰 메모리에 접근하지 못합니다.
  • 메모리 단편화에 대해 신경 쓰지 않아도 됩니다.
등등이 있겠네요. 
이런 장점들때문에 메모리를 무시하고 막 만들다 보면 가비지 컬렉션때문에 랙이 발생하는 경우가 다들 한번씩은 있을거에요.

어쨌든 C#을 사용하면서 알아두면 좋을 메모리 관리 기법에 대해서 써볼께요.
앞으로 하드웨어가 더 발전하면 메모리 관리도 필요 없어지지 않을까 싶네요. ^^;;

주기적으로 랙이 막 생기는데 로직의 이상도 없고 다른 부분에서는 특정 이상이 발견되지 않는다. 


그런데!! 

프로파일러를 돌려보니 System.GC.Collect()에서 호출 시간이 오래걸린다!!!

이거 십중팔구 가비지 컬렉션 문제입니다.


그럼 가비지 컬렉션이 작동하는 횟수를 줄여서 랙 발생 횟수를 줄이면 되겠죠. 

그러려면 가비지가 최대한 발생하지 않도록 해주어야 합니다.

그렇게 하기 위한 방법들중 string에 대해서 살펴보겠습니다.


C#은 문자열 조합하는 방법이 너무 쉽습니다. 


그래서 아래처럼 무턱대고 '+' 연산자를 이용해서 문자열을 만드는 경우가 많은데 이렇게 하면 '+'할때마다 새로운 string 인스턴스가 생성됩니다. 


그리고 이게 곧 가비지가 됩니다.


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);
        }
    }
}
이럴때 아래처럼 System.Text.StringBuilder를 사용하면 이런 문제를 해결 할 수 있습니다.

class NewNames
{
    public string[] name              = new string[100];
    private StringBuilder sb          = new StringBuilder();

    public void Print()
    {
        sb.Clear(); 

        for (int index = 0; index < name.Length; index++)
        {
            sb.Append("[");
            sb.Append(index);
            sb.Append("] ");
            sb.Append(name);
            sb.AppendLine();
        }
        
        Console.WriteLine(sb.ToString());
    }
}
이미 잡아 놓은 메모리 공간에 문자열을 복사해서 한번에 ToString()으로 string 객체를 생성해내기 때문에 가비지가 발생하지 않습니다.

Append()가 너무 많아 코드가 드럽다고 느껴지면 AppendFormat()을 사용할 수 도 있습니다.


class NewNamesOther
{
    public string[] name              = new string[100];
    private StringBuilder sb          = new StringBuilder();

    public void Print()
    {
        sb.Clear(); 

        for (int index = 0; index < name.Length; index++)
        {
            sb.AppendFormat("[{0}] {1}", index, name.ToString());
        }
        
        Console.WriteLine(sb.ToString());
    }
}
string처럼 Immutable pattern을 사용한 객체들의 값은 기존 메모리를 수정하지 않고 새로운 메모리를 만들어 반환하므로 주의해야합니다.