기본 콘텐츠로 건너뛰기

메모리 풀링

출처 탑건매직 | 탑건매직
원문 http://blog.naver.com/topgunmagic/120033903901

메모리 풀링?

: 메모리 풀링이란 간단히 말해서 필요할때마다 new나 malloc을 사용하는것이 아니고, 필요한 만큼에 메모리를 미리 잡아두고 사용하는것을 말한다.
그럼 이것을 어따쓰냐.... 잦은 메모리 할당은 성능을 저하시킬수 있다. 따라서 한번에 미리 메모리를 잡아두고 사용한다면 성능면에서 매우 효율적일때 사용될수 있다.
위 MemPooler.h는 Edited from C8MemPool class (Hitel, gma 7 3) - 1CoinClear - 라는 사람이 만든건데, 메모리 풀링이 어떤식으로 돌아가는지 분석 해보도록 하자.

메모리 풀링은 언제 사용하나??

new와 delete가 빈번하게 반복 발생될때 메모리풀을 사용합니다.
게임에 사용자가 접속할때마다 사용자 객체를 new해주고 접속을 해제할때마다 delete를 해준다면
속도는 물론이고 메모리 단편화의 문제가 발생할 수 있기 때문에 시작할 당시 n개의 사용자 객체를 생성한뒤 접속할때 가져다 쓰고 접속을 해제할때 제거하지 않고 다시 사용할 수 있게 해주는 경우도 해당합니다.
template<class Type>
class MemPooler
템플릿 클래스를 사용하였습니다. 왜일까요?
Type이 쓰이는 부분들을 일단 살펴 보도록 하겠습니다.
m_nListBlockSize=sizeof(BlockNode)+sizeof(Type);
음...보시면 일단 사이즈를 잡는건데요.
그렇습니다. MemPooler 클래스 설계시 우리는 필요한 만큼의 메모리를 잡아야 하는데 이시점에서 이 필요한 만큼의 메모리가 어느정도인지 우리는 알수 없습니다.
이럴때 한번 사용했군요.
Type* Alloc()
한 블럭을 사용할 시 사용가능한 메모리를 가리켜 그 주소를 리턴하는 함수인데요. 여기서 한블럭이라는건 차차 설명 하도록 하지요.
우리는 메모리풀링을 만들시 필요한 사이즈를 sizeof(Type)만큼 잡았습니다. 그럼 역시 사용할때도 그 사이즈 만큼 잡힌 메모리의 시작주소를 리턴 해야 겠지요.
Free(Type* freeBlock()
해제역시 마찬가지 입니다.
이럴때 사용하려고 template를 사용하였습니다.
앞으로 우리도 이와 비슷한 상황이 있을때 사용하면 되겠네요. ㅎㅎ 이러므로써 실력이 느는거 아닌가 싶습니다.
그럼 이제 생성자를 알아보도록 하겠습니다.
MemPooler(int nNumOfBlock): m_nNumofBlock(nNumOfBlock),
m_pFreeList(NULL),
m_pMemBlock(NULL),
m_nAllocCount(0)
{
    assert(nNumOfBlock>0);
    
    m_nListBlockSize=sizeof(BlockNode)+sizeof(Type);
    printf("sizeof(BlockNode) -> %d\n", sizeof(BlockNode) );
    printf("m_nListBlockSize -> %d\n", m_nListBlockSize);

    Create();
}
많에 복잡한것 처럼 보이지만 또 한번 보면 그래 복잡 하지도 않습니다.
먼저 각각의 클래스 맴버변수의 의미를 살펴 보도록 하겠습니다. m_nNumofBlock : 메모리 할당할 블럭 수
여기서 보시면 블럭이라는 말이 자주 나오는데 메모리 풀링은 대부분 다음과 같은 구조를 가집니다.
     1블럭                  2블럭                   3블럭                  4블럭                  5블럭
(블럭사이즈)  ->  (블럭사이즈)  ->  (블럭사이즈)  ->  (블럭사이즈)  ->  (블럭사이즈) 이렇게 필요한 사이즈만큼의 메모리(블럭사이즈)를 필요한 갯수만큼(5개) 만들어 링크드 리스트로 관리를 하게 됩니다.
그럼 MemPooler 클래스의 m_nNumofBlock란 위 예제에서는 5블럭 즉 5개를 의미하는 것입니다.
m_pFreeList : 남아 있는 메모리 블럭 리스트
남아 있는 메모리 블럭 리스트란 말 그대로 사용하고 있는 블럭을 제외한 사용 가능한 리스트를 말하는 것입니다.
먼저 이런 리스트를 관리하는 방법이 여러가지가 있지만, 크게 두가지를 본다면,

                                    3블럭                   4블럭                   5블럭 
사용가능한 리스트    : (블럭사이즈)  ->  (블럭사이즈)  ->  (블럭사이즈)

                                     1블럭                  2블럭
사용하고있는 리스트 : (블럭사이즈)  ->  (블럭사이즈) 

이렇게 사용하고있는 리스트와 사용가능한 리스트를 따로 관리를 하든지.

     1블럭                  2블럭                   3블럭                  4블럭                  5블럭
(블럭사이즈)  ->  (블럭사이즈)  ->  (블럭사이즈)  ->  (블럭사이즈)  ->  (블럭사이즈)
                                                              ▲
                                                       m_pFreeList

이처럼 한개의 리스트에서 사용가능한것만을 가리키고 리스트로 관리하는 방법이 있을수 있겠습니다.
여기선 두번째 방법을 사용하였습니다. 이제 m_pFreeList의 의미를 아시겠지요?
그렇다면 이 m_pFreeList가 Type* m_pFreeList 형일까요? 보시면,BlockNode* m_pFreeList; 즉 BlockNode*형입니다.
왜? BlockNode*는 모야.. 살펴 보도록 하겠습니다.
struct BlockNode
{
    BlockNode* pNext;

    BlockNode()
    {
        pNext=NULL;
    }
};
음..그냥 링크드 리스트 입니다.
이렇습니다. 우리는 일단 링크드 리스트로 관리는 한다는것을 알았는데 이 링크드 리스트 처럼 관리를 위한 노드 타입 입니다.
그럼 다음과 같이 관리가 되는것이지요.

                 1블럭                                                                  2블럭                                                  3블럭
(BlockNode size + Type size)           ▷              (BlockNode size + Type size)     ▷     (BlockNode size + Type size)
                                              BlockNode* pNext                                        BlockNode* pNext;

이렇게 다음을 이어주는 역할이 바로 BlockNode* pNext 구요. 그럼 이제 m_nListBlockSize=sizeof(BlockNode)+sizeof(Type);
이부분도 이해가 되실겁니다.
m_nListBlockSize : 한 블럭 사이즈 즉 한블럭 사이즈는 BlockNode + Type가 되는 것입니다.
또 생성자 부분을 보면 m_pMemBlock 란것이 있네요.
변수명 의미로 보자면 메모리 블럭의 포인터라는것인데...이것은 void* m_pMemBlock; 이렇게 선언되어 있습니다.
아직은 어떤 의미로 사용되는지 모르겠네요. 그럼 이것이 쓰인곳을 보도록 하겠습니다.
m_pMemBlock=VirtualAlloc(NULL,AllocationSize,MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
BlockNode* pNode=reinterpret_cast<BlockNode*>(m_pMemBlock);
음.. 메모리를 VirtualAlloc로 잡고 리턴된 주소를 가리키고있다가 pNode가 이곳을 기점으로 링크드 리스트를 만드는데 임시적으로 사용이 되는거 같습니다.
if(m_pMemBlock)
{
    VirtualFree(m_pMemBlock,0,MEM_RELEASE);
}
또한 삭제할때 메모리가 존재 하는지여부를 판단할때 사용됬군요. 그외 특별한 사용용도는 없는거 같습니다.

m_nAllocCount : 할당된 메모리 블럭 갯수
특별히 설명할필요는 없는거 같습니다. 한개 한개 사용할때마다 그 갯수를 체크하는 맴버 변수 같습니다.
이제 마지막으로,

CRITICAL_SECTION m_cs;
동기화 기법중 크리티컬 섹션이 나왔습니다. 이것을 어따 쓸까요? 조금 생각해보도록 하겠습니다.
먼저 메모리 풀링은 미리 만들어 놓은 메모리를 요청할때마다 Alloc()로 필요한 메모리를 주고 링크드 리스트를 적절히 관리 한느 것입니다.
그렇다면 한개의 쓰레드가 요청할때는 상관이 없지만, 멀티 쓰레드일때 즉 여러개의 쓰레드가 돌면서 필요할때마다 Alloc()를 할경우 중첩될수도 있습니다.
단순히 읽는 작업이라는 상관이 없지만 이렇게 필요한 메모리를 주고 쓰는 작업이라면 반드시 이런 경우를 고려 해야 합니다.

그렇기 때문에 쓸때는 "내가 쓰고있다"라는것을 알리고, 다쓰면 "다 썼다 너가 써라"라는 것을 알려주는 동기화 기법을 생각해야만 하는것입니다.

역시나 우리는 Alloc()안의 소스를 보면,
EnterCriticalSection(&m_cs);
////////////////////////////
    pNode=m_pFreeList;

    if(pNode!=NULL)
    {
        m_pFreeList=m_pFreeList->pNext;
        m_nAllocCount++;
        pRet=reinterpret_cast<Type*>(pNode+1);
    }

////////////////////////////
    LeaveCriticalSection(&m_cs);
이렇게 동기화 기법이 구현되어있는것을 볼수 있습니다. 물론 Free도 마찬가지 겠지요.
EnterCriticalSection(&m_cs);
///////////////////////////
    pNode=( reinterpret_cast<BlockNode*>(freeBlock) )-1;
    if(m_nAllocCount>0)
    {
        pNode->pNext=m_pFreeList;
        m_pFreeList=pNode;
        m_nAllocCount--;

        bRet=TRUE;
    }
///////////////////////////

    LeaveCriticalSection(&m_cs);
이제 클래스 맴버 변수에 대한 분석은 다 마쳤고 이제 함수에 대해서 분석 하도록 하겠습니다.
벌써 반은 마친거 같습니다. ㅎㅎ

생성자에서 하는역할은 객체가 만들어질때 생성자 인자로 nNumOfBlock가 들어옵니다. 즉 몇개 만들것이냐
이것은 클래스 맴버 변수 m_nNumOfBlock와 의미가 같으므로 m_nNumofBlock(nNumOfBlock) 값을 설정해주고
나머지 맴버들도 초기화 해줍니다. 그리고 필요한 만큼의 메모리를 만들고 링크드 리스트로 관리하는 Create()함수가 나옵니다.
void Create()
{
    const int AllocationSize=(m_nListBlockSize) * m_nNumofBlock;
    m_pMemBlock=VirtualAlloc(NULL,AllocationSize,MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
    
    assert(m_pMemBlock);
    
    BlockNode* pNode=reinterpret_cast<BlockNode*>(m_pMemBlock);
    pNode =reinterpret_cast<BlockNode*>((reinterpret_cast<DWORD>(pNode))+(m_nNumofBlock-1)* (m_nListBlockSize) );
    
    for(int i=m_nNumofBlock-1; i>=0; i--)
    {
        pNode->pNext=m_pFreeList;
        m_pFreeList=pNode;
        pNode=reinterpret_cast<BlockNode*>((reinterpret_cast<DWORD>(pNode))-m_nListBlockSize);
    }

    InitializeCriticalSectionAndSpinCount(&m_cs,4000);
  }
  
복잡한거 같지만 역시 별거 없습니다.
AllocationSize=(m_nListBlockSize) * m_nNumofBlock; 여기서 AllocationSize는 전체 사이즈를 뜻하죠.
전체 사이즈는 결국 한개의 노드 사이즈 * 총갯수가 되는것입니다.
전체 사이즈를 알았으니 이만큼의 메모리를 할당해야 합니다.
m_pMemBlock=VirtualAlloc(NULL,AllocationSize,MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
메모리 할당은 흔히 우리가 알고 있는 malloc나 alloc나 혹은 new를 사용안하고 VirtualAlloc을 사용하였습니다.
이것에 대한 논의는 개발시 유용한팁에 올려놨습니다.

메모리를 잡고 그주소를 m_pMemBlock로 받습니다.
for(int i=m_nNumofBlock-1; i>=0; i--)
{
    pNode->pNext=m_pFreeList;
    m_pFreeList=pNode;
    pNode=reinterpret_cast<BlockNode*>((reinterpret_cast<DWORD>(pNode))-m_nListBlockSize);
}
이제 이것을 이렇게 링크드 리스트로 관리를 하게 됩니다.
링크드 리스트가 먼지 잘 모르고 위 코드가 이해가 안되시는 분들은 자료구조 링크드 리스트를 좀더 공부 하시는게 좋을듯 하네요.

그리고 앗, 좀 못본 함수가 나오네요.

InitializeCriticalSectionAndSpinCount(&m_cs,4000); 오~함수 이름도 길다. MSDN을 찾아 보도록 하겠습니다.

BOOL InitializeCriticalSectionAndSpinCount(
LPCRITICAL_SECTION lpCriticalSection,
DWORD dwSpinCount
);
The InitializeCriticalSectionAndSpinCount function initializes a critical section object and sets the spin count for the critical section.
이렇게 나와 있습니다. 즉 이런 기능을 하는 함수 입니다.


InitializeCriticalSectionAndSpinCount()
에서의 SpintCount에 대해서...

Spin - wait 상태로 들어가기 전에 함수의 인자로 주어진 spin 횟수만큼 루프를 돈다는 겁니다.
루프 도는 중간에 critical section을 획득 할 수 있다면 thread context switch가 발생하지 않고 임계영역안에 접근 가능하다는 것입니다.

InitializeCriticalSectionAndSpinCount(&g_cs, 2000); 일반적으로 두번째 파라미터 dwSpinCount에 스핀록 루프 횟수를 2000 정도를 잡는게 효율적입니다.

spin에 대해서 조금 부연설명을 하면...
하나의 공유 변수에 대해서 1과 0값을 사용해서 (사실 어떤 값이던 상관이 없죠) critical section 에 대한 권한을 체크하는 것입니다.
이 변수가 1일때는 사용중이고, 0 일때는 진입 가능이라고 보면, critical section에 진입시에는 이 값을 1로 setting 해서 다른 access를 막고,
다시 critical section을 나갈때는 이 값을 0으로 바꿔주는 것죠.
이때 loop를 돌면서 InterlockedExchange를 사용 합니다. 들어가려고 시도하는 쪽에서는
int permission = InterlockedExchange(&var, 1); 같은 문장을 실행시켜봐서 결과가 0이 나오면 진입, 1이 나오면 계속 루프를 돌게 되는거죠.
역시 나올때는 InterlockedExchange(&var, 0); 를 실행하면 됩니다.

퍼왔습니다. ㅎㅎ

자 이제 우리는 생성자에서 하는 역할을 알았습니다.
그럼 이제 소멸자를 보겠습니다.
~MemPooler()
{
    Destroy();
}
Destroy()함수가 실행이 되네요. 그럼 Destroy함수를 보면,
void Destroy()
{       
    if(m_pMemBlock)
    {
        VirtualFree(m_pMemBlock,0,MEM_RELEASE);
    }

    DeleteCriticalSection(&m_cs);
}
m_pMemBlock가 존재 하면 VirtualFree 메모리를 해제하고 DeleteCriticalSection 동기화 객체를 삭제 하네요.
이해가 바로 되실겁니다.

그럼 이제 메모리 할당 Alloc()부분을 보도록 하겠습니다.
Type* Alloc()
{
    BlockNode* pNode=NULL;
    Type* pRet=NULL;

    EnterCriticalSection(&m_cs);
    ////////////////////////////

    pNode=m_pFreeList;
    if(pNode!=NULL)
    {
        m_pFreeList=m_pFreeList->pNext;
        m_nAllocCount++;
        pRet=reinterpret_cast<Type*>(pNode+1);
    }

    ////////////////////////////
    LeaveCriticalSection(&m_cs);

    return pRet;
}
보시면 말이 메모리 할당이지 여기서 메모리를 할당하는 의미는 이미 만들어 놓은 풀링에서 한개를 던져 준다...라는 의미 입니다.
보시면 m_pFreeList를 한개던져주고 다음 노드를 가리키게 하네요.
이제 리턴값으로 받은 Type*로 사용자는 메모리에 쓰고 할일을 하면 되겠지요.
BOOL Free(Type* freeBlock)
{
    BlockNode* pNode=NULL;
    BOOL bRet=FALSE;
 
    EnterCriticalSection(&m_cs);
    ///////////////////////////
    
    pNode=( reinterpret_cast<BlockNode*>(freeBlock) )-1;
    if(m_nAllocCount>0)
    {
        pNode->pNext=m_pFreeList;
        m_pFreeList=pNode;
        m_nAllocCount--;
        bRet=TRUE;
    }

    ///////////////////////////
    LeaveCriticalSection(&m_cs);
    
    return bRet;
}
프리부분을 보시면 프리하고자 하는 노드가 인자로 들어오면 그것을 사용가능한 링크드 리스트에 추가만 시켜줍니다.
그래서 나중에 다시 요청을 하면 또 그것중의 한개를 나주는 형식이지요.
int GetCount()
{
    return m_nAllocCount;
}
뭐 이것은 현재 사용중인 블럭수를 리턴하는것입니다.
좀더 정교하게 짠다면 단순히 갯수를 리턴하는것이 아닌, 사용중인것역시 링크드 리스트로 관리하여 필요한 블럭의 주소를 리턴시켜줄수도 있겠네요.
이런 부분은 메모리 풀링의 설계차이지요.

이상으로 메모리 풀링에 대한 분석을 마칩니다. 이제 이 클래스를 이용하여 직접 응용 프로그램을 한개 짜보면 완전히 내것이 될듯 싶습니다.

서버를 개발하다 보니 메모리 풀링에 관심이 많아질 수 밖에 없었다.
여러 소스와 문서들을 찾았는데 그 중 가장 맘에 드는 자료는 'Efficient C++' 이라는 서적과 '게임 프로그래머를 위한 C++' 이라는 서적 두 권과
CodeProject의 Cho,Kyung-min(한국분+_+ 그냥 조경민님이라고 하겠다)님이 작성하신 VMemPool 이었다.
특히 'Efficient C++'은 메모리 풀링뿐만 아니라 인라인 트릭, 참조등의 효율적인 코드 작성을 알려준다. 강추한다
-_-d '게임 프로그래머를 위한 C++'도 성능 향상을 위한 테크닉이라는 흥미있는 내용을 담고있다.
두 책 모두 시간날 때 차근히 읽는 다면 코딩의 습관이 달라질지도 모르겠다.
그러나 '게임 프로그래머를 위한 C++'엔 동적 메모리 관리를 위한 내용은 있었지만 풀링에 대해서는 간략하게만 나와있었다.
동봉된 CD 소스에 작성되 있다고 하는데 풀링에 관한 코딩은 되어 있지 않았다.
그에 반해 'Efficient C++'은 풀링의 자세한 설명과 친절하게 성능시험까지 해서 보여준다. 다만 책 안에 소스 CD가 없어서 불편하다-_-
또한 CodeProject에 올려져있는 조경민님이 작성하신 VMemPool은 나에게 좋은 예제였다.
같은 개념이다 보니 비슷비슷하겠지만 'Efficient C++'은 메모리 청크를 linked list로 구현되 있는데 반해 VMemPool은 따로 영역을 할당하여 sequence값을 저장해서 빼내는 환형큐로 구현되어 있었다.
'게임 프로그래머를 위한 C++'은 'Efficient C++'와 마찬가지로 linked list를 권하였다.
다시 말하지만 VMemPool은 개인적으로 정말 재미있게 분석했기 때문에 나 역시 환형큐로 작성해보았다.
먼가 다른 방법으로 해볼까도 했지만(다른 큐라든지 스택이라든지^^;) 별다른 생각이 나오진 않았다-_-
그치만 시간나면 linked list로도 구현해서 어떤 것이 더 성능이 좋은 지 비교는 해봐야겠다.
또한 '게임 프로그래머를 위한 C++'에 자세하게 나와있는 메모리 관리도 나중에 추가할 생각이다.
어쨋든 같은 데이터 구조로 작성하다보니 본의 아니게 VMemPool과 상당히 비슷한 소스가 되어버렸다.
VMemPool과 내가 작성한 소스를 비교해보는 것도 재미있겠다. 그치만 나중에 이것저것 추가하고 수정하면 많이 달라질거라고 생각한다.
어땟든 이 자리를 빌어서-_- 조경민님께 큰 감사를 드린다.

메모리 풀링 소스 다운로드

메모리 풀링(Memory Pooling)

메모리를 자주 할당하고 해지하는 것은 응용 프로그램 성능을 저하시키는 주요한 요소이다.
하지만 특화된 메모리 관리자를 개발하여 이러한 사항을 극복할 수 있다.
특화된 메모리 관리자를 만드려면 new, delete 키워드를 오버로드하여 필요한 일만 하게끔 하면 되겠다.
inline void* operator new(size_t size);
inline void operator delete(void* pDoomed);
VMemPool과 비슷한 부분도 있지만 크게 다른 점들도 있다.
VMemPool은 메모리 풀 클래스를 상속하는 방식이지만 이 쪽은 static 멤버 객체로 가지는 방식이다. 이유는 뒤에 설명하겠다.
일단, 메모리 풀 관리가 어떤 특정한 클래스에 의존적이지 않기 때문에 메모리 풀 클래스를 template로 구현하였다.
메모리 풀로 사용하고 싶다면 이 클래스를 static 멤버 객체로 선언해야 한다.
template <class Type>
class CMemPool
{
메모리 풀 클래스에서 가장 먼저 필요한 것은 역시나 메모리겠다.
사용자가 원하는 사이즈 만큼의 메모리를 할당하고 new를 할당할 때마다 꺼내어 주고, delete를 하면 가져오는 구조이다.
아래는 메모리를 할당하는 메서드이다.
static inline BOOL InitMemPool(const int& nObjSize,
const int& nObjCount)
{
   if(m_spAllocMem != NULL)
      return FALSE;

   m_nMaxObjCount = nObjCount;
   if(nObjSize > 0)
   {
      m_nObjSize = nObjSize;
      if(Allocate(m_nObjSize, nObjCount) == FALSE)
         return FALSE;
   }

   return TRUE;
}
이 함수는 CMemPool의 생성자에서 호출하는 데 메모리 풀을 쓸 클래스를 정의만 하면 알아서 메모리 힙을 할당하는 구조이다.
여타 다른 메모리 풀링 코드에선 new를 할 때 메모리를 할당했는지의 여부에 따라 정해졌지만 (따라서 첫번째 new호출에서 메모리가 할당된다. 미리 할당을 하라는 메서드를 따로 호출하는 경우도 있다.)
난 이 것이 더 낫다고 생각한다.
다만 클래스 정의만 하고 쓰지 않는 경우엔 낭비가 되는데.. 쓰지도 않을 거 왜 정의하고 그래-_-
이 부분이 메모리 풀 클래스를 static 멤버 객체로 선언한 이유 인데, 상속으로 하면
클래스 객체가 생길 때 마다 메모리 힙 클래스 생성자 또한 실행 될 터이고 그럼 위와 같이 미리 할당 하는 구조가 성립될 수 없다.
소멸자 역시 마찬가지이다. 소스를 보면 메모리 힙 클래스의 소멸자에 ReleasePool을 호출하고 있다.
할당한 메모리를 삭제하는 메서드이다. VMemPool은 메모리 삭제를 아예 하지 않고 있다. 안해도 프로그램이 종료될 때 알아서 메모리가 파기된니 상관없다는 것 같다.
그치만 난 책임지고 삭제하고 싶다. 그렇기에 메모리 풀을 사용하기 위한 작업이 좀 더 많아졌다.
static inline BOOL Allocate(const size_t& size, const int& count)
{
   if(m_spAllocMem != NULL)
      return FALSE;

   int nBitSize = (count >> 3) + (int)(count % 8 > 0);
   if(nBitSize % 4 != 0)
      nBitSize += (4 - (nBitSize & 3));
   m_nMaxCountQueue = count + 1;
   int nSeqSize = sizeof(int) * m_nMaxCountQueue;
   m_nTotalSize = nBitSize + nSeqSize + size * count;

   m_spAllocMem = (char*)malloc(m_nTotalSize);
   if(m_spAllocMem == NULL)
      return FALSE;

   m_slpData = m_spAllocMem + nBitSize + nSeqSize;
   m_slpBitSet = m_spAllocMem + nSeqSize;
   memset(m_slpBitSet, 0, nBitSize);

   for(int i = 0; i < count; i++)
      Push(i);

   return TRUE;
}
InitMemPool에서 호출하는 Allocate 함수는 비트를 넣을 공간, 시퀀스 번호를 넣을 공간, 실제 오브젝트 공간을 계산하여 메모리를 할당한다.
또한 저 공간 사이마다 포인터 주소를 받아서 저장해놓는다.
마지막으로 오브젝트 개수만큼 Push를 호출하여 모든 메모리가 사용할 수 있는 상태라고 표시한다.
static inline void* Allocate(const size_t& size)
{
   if(size <= 0)
      return NULL;

   m_sLock.Lock();
   void* pMem = Pop();
   m_sLock.Unlock();

   return pMem;
}
또하나의 Allocate 메서드는 재정의된 new 에서 호출된다. 별 거 없다.
동기화해주고 메모리를 하나 꺼내 리턴하는 메서드이다.
static inline void Free(void* pDoomed)
{
   if(pDoomed == NULL)
      return;

   m_sLock.Lock();
   int seq = (int)((char*)pDoomed - m_slpData) / m_nObjSize;
   Push(seq);
   m_sLock.Unlock();
}
위 Free 메서드는 재정의된 delete 에서 호출한다. 역시 별거 없다.
동기화하고 몇 번째 블럭의 메모리인지 체크하고 그 메모리가 안 쓰고 있다는 비트 체크를 하는 Push함수를 호출한다.
static inline void* Pop()
{
   if(m_nRearMarker == m_nFrontMarker)
      return NULL;

   char* lpSeq = m_spAllocMem;
   int seq = *((int*)lpSeq + m_nFrontMarker);
   m_slpBitSet[seq >> 3] |= (1 << (seq & 7));

   m_nFrontMarker = ++m_nFrontMarker % m_nMaxCountQueue;
   return (Type*)m_slpData + seq;
}
Pop()은 저 위의 Allocate에서 호출했었다.
환형큐의 Front, Rear 변수들을 비교하고 메모리 sequence 값을 메모리 위치를 가리키는 비트에 1을 찍어 이 메모리는 사용중! 이라고 도장찍는 일을 한다.
static inline BOOL Push(const int& seq)
{
   if((m_nRearMarker + 1) % m_nMaxCountQueue == m_nFrontMarker)
      return FALSE;

   char* lpSeq = m_spAllocMem;
   *((int*)lpSeq + m_nRearMarker) = seq;
   m_slpBitSet[seq >> 3] &= ~(1 << (seq % 7));

   m_nRearMarker = ++m_nRearMarker % m_nMaxCountQueue;
   return TRUE;
}
Push는 Free에서 호출하는데 Pop에서 가져간 메모리를 가리키는 비트에 찍혀진 도장을 지워버리는 역할을 한다.
메모리 풀의 개략적인 설명은 여기까지만 하고 이 것을 사용하는 방법을 설명하겠다.
아까도 말했듯이 메모리 풀 클래스를 static 멤버 변수로 가지는 클래스를 만들어야 한다.
게다가 new, delete 연산자도 재정의 해야한다. 메모리 풀을 쓸 때 마다 이 번거로운 작업을 최대한 줄이기 위해 저 선언들을 define으로 묶어 버렸다.
#define DECLARE_HEAP \
public: \
   void* operator new(size_t size) \
   { \
      return m_spMemPool.Allocate(size); \
   } \
   void operator delete(void* pDoomed) \
   { \
      m_spMemPool.Free(pDoomed); \
   } \
private: \
   static CMemPool m_spMemPool;

#define DEFINE_HEAP(className, count) CMemPool
className::m_spMemPool(sizeof(className), count);
이제 define된 것들을 이용해 메모리 풀링을 사용할 클래스를 만드는 일만 남았다. 아래는 사용하는 예이다.
class CObject
{
public:
   CObject() { };
   ~CObject() { };

   DECLARE_HEAP;
private:
   char m_sText[1827];
};

DEFINE_HEAP(CObject, 100000);
DECLARE_HEAP을 넣어 놓으면 new, delete operator와 메모리 풀링 클래스 객체가 static으로 생성되게 된다.
DEFINE_HEAP은 static 객체를 선언하는 부분인데 100000은 메모리 풀을 100000개의 객체 크기만큼 할당한다는 것이다. 적절한 값을 집어넣으면 되겠다.
기존의 new연산자에 비해 성능이 얼마나 좋아졌는지는 스스로 테스트해보는 것이 좋겠다^^
그럼 메모리 풀링 설명은 여기까지... -_-)/

댓글

이 블로그의 인기 게시물

80040154 오류로 인해 CLSID가 {xxxx-...}인 구성 요소의 COM 클래스 팩터리를 검색하지 못했습니다.

원문보기 .NET 으로 만든 응용프로그램에서 com 객체를 호출한 경우 Windows7 64bit 에서 제목과 같은 에러가 발생했다. Win32 COM 과 .NET 프로그램간의 호환성 때문에 생긴 문제였다. 원인은 .NET 실행시 JIT 컴파일러에 의해 최적화된 기계어로 변환되기 때문.. Win32 COM은 컴파일시.. Win32 COM에 맞춰 빌드 속성에서 하위버전으로 맞춰 컴파일을 다시하는 방법도 있지만 메인 프로젝트가 .NET이라면 참조되는 모든 프로젝트를 다 바꿔야할 노릇.. 또 다른 방법은 COM+를 이용하여 독립적으로 만드는 것이다. 분리시키는 방법은 아래 주소해서 확인할 수 있다. http://support.microsoft.com/kb/281335 나의 경우는 Win32 COM DLL을 64비트 .NET 프로그램에서 참조하니 COM 객체를 제대로 호출하지 못하였습니다. 그래서 .NET 프로그램의 Target Machine을 x86으로 설정하니 제대로 COM 객체를 호출하였습니다.

[Pyinstaller] 실행 파일 관리자 권한 획득하기

고객사에서 일부 사용자에게서 프로그램 오류가 발생한다며 아래와 같이 에러 캡처를 보내왔습니다. 프로그램에서 로그를 남기기 위해 로그 파일을 생성하는데 권한의 문제로 로그 파일을 생성하지 못해 프로그램 오류가 발생한 것 같습니다. 처음에는 Python 코드에서 관리자 권한을 요청하는 코드를 넣으려고 했는데, 실제로 Stackoverflow를 찾아보면 이런 내용이 나옵니다. 프로그램이 관리자 권한으로 실행되지 않았다면 관리자 권한으로 다시 프로그램을 실행시키는 코드입니다. import os import sys import win32com.shell.shell as shell ASADMIN = 'asadmin' if sys.argv[-1] != ASADMIN: script = os.path.abspath(sys.argv[0]) params = ' '.join([script] + sys.argv[1:] + [ASADMIN]) shell.ShellExecuteEx(lpVerb='runas', lpFile=sys.executable, lpParameters=params) sys.exit(0) 하지만 개인적으로 이런 방식은 마음에 들지 않았고 조금 더 찾아보니 Pyinstaller로 exe 파일을 만들 때 옵션을 설정하여 관리자 권한을 요청하도록 할 수 있다고 합니다. --uac-admin을 옵션에 추가하면 프로그램 실행 시 관리자 권한을 요청할 수 있습니다. pyinstaller.exe --uac-admin sample.py 하지만 안타깝게도 이 방식은 원하는 대로 동작하지 않았습니다. 마지막으로 manifest 파일을 이용하여 시도해보았습니다. spec 파일을 이용하여 pyinstaller로 빌드하면 <실행 파일 이름>.manifest 라는 파일이 생성됩니다. 파일에서 아랫부분을 찾아볼 수 있습니다. <security> <re

초간단 프로그램 락 걸기

프로그램에 락을 걸 일이 생겨났다. 하드웨어 락을 걸면 쉬울텐데 그 정도는 아니고 프로그램의 실행 날짜를 제한 해 달라고 한다. 그래서 파일(license.lic)을 가지고 락을 걸리고 결정을 했다. 요구 사항은 아래와 같다. 1. license.lic 파일이 없으면 프로그램을 실행 할수 없게 한다. 2. 지정한 날짜를 넘어서는 프로그램을 실행 할수 없게 한다. 3. 사용자가 시스템 날짜를 되돌렸을때 인식하여 프로그램을 실행 할수 없게 한다. 음.... 1.번 문제는 사용자가 프로그램을 실행하기 위해서 license.lic 파일을 받아야만 한다. license.lic 파일에는 최근 실행 날짜/종료날짜 이런식으로 적도록 한다.(물론 내용은 암호화 한다.) 최근 실행날짜는 프로그램이 실행때마다 업데이트 하도록 하고 시스템 날짜와 비교하여 시스템 날짜가 최근 실행 날짜보다 이전의 날짜면 시스템 날짜를 되돌렸다고 인식하도록 한다.(3.번 문제 해결) 시스템 날짜와 종료 날짜를 비교하여 시스템 날짜가 종료 날짜를 넘으면 프로그램을 실행 할수 없도록 한다.(2.번 문제 해결)