본문 바로가기
컴퓨터/GPG Study

GPG Study 1.7 자원과 메모리 관리

by 김짱쌤 2015. 7. 1.

GPG Study 1.7

자원과 메모리 관리

그래픽, 사운드, 음악, 비디오, 애니메이션등을 사용하는 종합예술인 게임 프로그래밍은 그 목적을 달성하기 위하여 정말 질적 양적으로 많은 자원을 사용한다. 그리고 동시에 유저의 기기에서 쌩쌩 잘 돌아가야하기 때문에 자원을 최대한 효율적으로 다룰 수 있어야한다. 최대한 잘 돌아가게 때려박는것, 이것이 게임 최적화라 할 수 있다.

따라서 우리는 자원들을 효과적으로 다루기 위하여 자원과 자원 관리 클래스를 만들 필요가 있다. 자원은 자신의 데이터를 쉽게 불러오고, 폐기할 수 있어야하고, 자원 관리자는 자원들을 효과적으로 관리, 분배, 접근제어할 수 있아야 한다. 앞서 이야기 했던 핸들 개념을 사용하여 관리작업을 수행한다면 더욱 훌륭할 것이다.

자원 클래스 BaseResource

class BaseResource
{
pubLic:
    enum class Priority
    {
        LOW,
        MIDIUM,
        HIGH,
    };

    BaseResource() { Clear(); }
    virtual -BaseResource(){ Destroy(); }

    // 클래스 데이터를 비운다.
    virtual void Clear();

    // 생성과 파괴 함수들. 파생된 클래스의 Create()가 기반 클래스와
    // 정확히 일치할 멸요는 없다는 점을 주의하기 바란다. 
    // 파생 클래스의 Create() 에서 인자들의 구성은 얼마든지 달라질 수 있다.
    virtual bool Create() { return false; }
    virtual void Destroy() {}

    // Dispose()와 Recreate()는 클래스에 담긴 데이터를 폐기하거나
    // 추가적인 인자들 없이 완전히 다시 생성힐 수 있어야 한다
    virtual bool Recreate() = 0;
    virtual void Dispose() = 0;

    // GetSize()는 클래스에 담긴 데이터의 크기를 돌려줘야 하며/
    // IsDisposed()는 데이터의 존재 여부를 알려줘야 한다.
    virtual size_t GetSize() = 0;
    virtual bool IsDisposed() = 0;

    // 이 함수들은 우선 순위를 설정하거나 돌려준다.
    // 우선 순위는 < 연산자로 자원들의 폐기 순서를
    // 결정할때 쓰인다.
    inline void SetPriority(Priority priority)
    { mPriority = priority; }
    inline Priority GetPriority()
    { return mPriority; }

    inline void SetReferenceCount(UINT count)
    { mRefCount = count; }
    inline UINT GetReferenceCount()
    { return mRefCount; }

    inline bool IsLocked()
    { return (mRefCount > 0) ? true : false; }

    inline void SetLastAccess(time_t LastAccess)
    { mLastAccess = LastAccess; }
    inline time_t GetLastAccess()
    { return mLastAccess; }

    // 자원들의 폐기 순서를 정렬할 때 쓰이는 미만 연산자
    virtual bool operator < (BaseResource& container);

protected:
    Priority mPriority; 
    UINT mRefCount;
    time_t mLastAccess;
} ;

이제 이 BaseResource를 상속받아서 다른 자원 클래스들을 만들게 될 것이다. 각자 사정에 맞게 가상함수들을 재정의하여 사용할 것이다.

  • Create() 함수는 시스템으로부터 필요한 자원 데이터를 할당받는 형태로 재정의 될 것이다. 재생성을 고려하면 고정적으로 사용되는 데이터들을 static등을 활용하여 킵해둘 필요도 있을 것이다.

  • Dispose()와 Recreate()는 데이터를 싹 비워주는 기능을 구현해야할 것이다. 전체 메모리를 다 해제하지 않고 이미지, 사운드 버퍼 등의 데이터만 쾌척하여 클래스를 그대로 재사용 가능하도록 구현해야한다.

  • GetSize()는 현재 사용가능한 데이터의 크기를 돌려줘야한다. 자원이 전부 사용중이라면 0을 리턴해야겠지요.

  • IsDisposed() 는 데이터의 Dispose여부를 체크한다. 폐기된 데이터는 재사용 가능하다는 의미이기도 하고, 사용가능한 자원이 아니라는 의미이기도 하다.

  • mPriority는 이름 그대로 자원의 우선순위를 표현하는 지표이다. 우선순위가 높으면 메모리에 더 오래동안 남아있을 수 있고, 낮은 항목은 사용되지 않는다면 다른 것들보다 먼저 해제될 것이다.

  • mRefCount는 자원의 참조횟수이다. 참조횟수가 0이라면 해제대상이 되지 않을까? 뒤에서 더 자세히 설명하게 될 것이다.

  • mLastAccess는 자원이 마지막으로 접근된 시각을 의미한다. 오래되면 될수록 해제될 가능성이 높아진다.

  • operator<는 자원의 우선순위를 체크하는데 쓰이는 연산자이다. 폐기순서를 결정할 때 자원들을 소팅하기 위하여 쓰일 것이다. 구현부는 다음과 같다.

bool BaseResource::operator < (BaseResource& conta;ner)
{
    //1. Priority Check
    if(GetPriority() < container.GetPriority())
        return true;
    else if(GetPriority() > container.GetPriority())
        return false;
    else
    {
        //2. LastAccess Check
        if (m LastAccess < container.GetLastAccess())
            return true;
        else if(m LastAccess > container.GetLastAccess())
            return false;
        else
        {
            //3. Size Check
            if(GetSize() < container.GetSize())
                return true;
            else
                return false;
        }
    }
    return false;
}

자원 관리자 클래스 ResourceManager

class ResourceManager
{
public:
    ResourceManager(){ Clear(); }
    virtual ~ResourceManager() { Destroy(); }

    void Clear();
    bool Create(UINT maxSize);
    void Destroy();

    // 자원 맵 탐색
    // 자윈들율 차례로 탐색해 나가는 데 쓰이는 접근 합수들
    // DLL 경계를 넘어서 맵에 접근하려 하면 스택 포인터 폴트가 생길 수 있다. 
    // (effective c++ 참조)그러나 이처럼 rapper함수들을 거치게 하면 안전하다.

    inline void GotoBegin()
    { mCurrentResource = mResourceMap.begin(); }

    inline bool GotoNext()
    { mCurrentResource++; return IsValid(); }

    inline BaseResource* GetCurrentResource()
    { return (*mCurrentResource).second; }

    inline bool IsValid()
    { 
        return (mCurrentResource != mResourceMap.end())? 
        true : false; 
    }

    // 일반화된 자원 접근

    // 삽입된 자원이 메모리의 최대 허용량을 넘는 일이 일어나지
    // 않도록 미리 메모리를 확보하는 대 쓰인다.
    bool ReserveMemory(size_t mem);

    // 자원의 메모리 주소를 넘겨주변 고유한
    // 핸들을돌려준다.
    bool InsertResource(RHANDLE* hUniqueID, BaseResource* pResource);

    // 자원 관리자로부터 객체를 완전히 제거한다.
    bool RemoveResource(RHANDLE hUniqueID);

    // 객체를 따괴하고 메모리를 해제한다.
    bool DestroyResource(RHANDLE hUniqueID);

    // 해당 핸들에 해당하는 자원에 대한 포인터를 돌려준다.
    // 이미 폐기된 지원인 경우에는 다시 생성한 후 돌려준다.
    BaseResource* GetResource(RHANDLE hUniqueID);

    // 지원 관리자가 건드리지 못하도록 자원을 잠근다.
    // 그래픽 표면에 대해 어떠한 작업을 하는 도중 
    // 자원관리자가 그것을 폐기하지 않도록 하는 둥의 용도로 쓰인다.
    // 지원 객체에는 여러 번의 잠금이 안전하게 일어나게 
    // 하기 위한 잠조 카운터가 있다.
    BaseResource* Lock(RHANDLE hUniqueID);

    // 자원에 대한 잠금을 풀고 관리를 다시 자원 관리자에게 맡긴다. 
    // 모든 잠금들이 풀리면(참조 횟수가 0이 되면) 객체는
    // 관리 가능한 상태가 되며, 
    // 자원 관리자는 필요에 따라 객체를 폐기할 수 있게 된다. 
    int UnLock(RHANDLE hUniqueID);

    // 지원 객체에 대한 포인터를 지정해서 그에 해당하는 핸들을
    // 얻는다. 중복된 포인터들이 없다는 가정하에서 작동하며,
    // 중복된 포인터가 있는 경우 첫번째 것을 돌려준다.
    RHANDLE FindResourceHandLe(BaseResource* pResource);

protected:
    // 내부함수들
    inLine void AddMemory(UINT mem)
    { mCurrentUsedMemory += mem; }
    inline void RemoveMemory(UINT mem)
    { mCurrentUsedMemory -= mem; }
    UINT GetNextResHandLe()
    { return mHNextResHandLe; }

    // 폐기 가능한 자원들을 점검힐 때 쓰이는 함수. 자원들은
    // 허용 가능한 최대 용량에 도달한 경우에만 교체되어 나가며,
    // 우선 순위가 낮은 것에서부터 높은 것의 순서로
    // (자원 클래스의 < 연산자에 의해 결정된다) 폐기된다.
    // 이 함수는 요청된 메모리가 해제될 수 없으면 실패한다.
    bool CheckForOverallocation();

protected:
    RHANDLE    mHNextResHandLe;
    UINT       mCurrentUsedMemory;
    UINT       mMaximumMemory;
    ResMapltor mCurrentResource;
    ResMap     mResourceMap;
};

자원관리자는 using ResMap = std::map<RHANDLE, BaseResource*>;타입을 사용하는 자원 맵을 사용하여 자원을 관리한다. 핸들-자원 페어는 스크립트나 config파일등으로 배정될 수도 있고, 자원관리자의 함수들로 동적으로 배정될 수 도 있다.

자원관리자 객체를 생성한뒤 Create()로 초기화를 해준다. 자원을 동적으로 집어넣을 때는 InsertResource()를 활용하여 자원을 넣고 핸들을 발급 받는다. 가용 메모리보다 더 큰 자원을 집어 넣으면 현재 가지고 있는 자원들중 우선순위가 낮은 순서로 해제를 시키고 자리를 마련한다.

기존 메모리를 동적으로 운영하는 방식은 고정된 용량을 지속적으로 사용하는 자원에 대해서는 문제를 일으킬 수 있다. 이때ReserveMemory()함수를 사용하여 미리 메모리 공간을 체크한다면 보다 안전하게 insert작업을 수행할 수있다.

핸들의 작동 방식

핸들을 쓰는 이유는 이전 챕터에서 설명했기 때문에 패스한다. 자원 관리자에게 핸들로 자원을 빌려다 쓴다.

SomeResource* resource = 
    (SomeResource*)gResourceManager->GetResource(hResHandle);

자원을 사용하는 사람들이 주의해야하는 것은 자원 관리자가 허락하는 시점에만 이 자원포인터가 유효하다는 점이다. 자원을 반납한뒤의 자원 포인터 resource는 다른 자원이 들어오거나하면 메모리 관리 로직에 의해서 다른 자원의 메모리 공간으로 변경될 수 있기 때문이다.

자원포인터를 지속적으로 유지하고 싶다면 Lock()함수를 통해 참조횟수를 증가시켜 해제되지 않는것을 보장받아야한다. 참조횟수가 0이 아니면 해제되지 않음이 보장되기 때문에 Unlock()을 호출할 떄까지 자원이 폐기되지 않는다.

개선방향

전체 데이터 집합이 자원관리자에서 index화 되어있는 경우, 자원 관리 시스템을 통해 캐싱을 할 수 있다. 자주 사용되는 자원들에 대해서는 미리 로드시켜서 우선순위를 높여주면 메모리에 계속 남아서 다시 I/O작업등을 통해서 불러올 필요가 없어진다. 자주 사용되지 않는 자원은 우선순위를 낮추어 다른 자원들에게 빠르게 자리를 내줄 수 있도록 만들어주면 된다.

그리고 클라이언트 쪽에서 사용 경향을 집계하여 통계수치를 낸뒤 우선순위에 반영하면 동적으로 자원들의 이용현황에 따라 최적화된 캐시시스템을 만들 수 있을 것이다.

'컴퓨터 > GPG Study' 카테고리의 다른 글

GPG Study 3.8 Fuzzy 논리  (0) 2015.07.17
GPG Study 1.2 TMP를 이용한 수학연산  (0) 2015.06.22