본문 바로가기
컴퓨터/Server

Pooling

by 김짱쌤 2015. 4. 17.

메모리 풀

메모리 풀을 쓰는 이유

메모리 Pool은 메모리 할당/해제/최적화 작업등을 유저가 직접 다룰 수 있도록 만든 객체이다. OS에게 메모리 할당/해제을 요청하면 시스템 콜이 발생한다. 빈번한 메모리 요청이 발생할 때, 하나하나 OS에게 작업을 맞긴다면 지나치게 많은 비용이 발생할수도 있다. 그리고 LFH파트에서 본것처럼 시스템이 유저에게 메모리를 할당할때, 데이터 뿐만 아니라 기타 관리 정보까지 포함해서 메모리를 할당하기 때문에, 작은 메모리를 자주 요청하는 경우 필요 이상의 메모리를 사용하게 된다. 그리고 LFH같은 훌륭한 관리체계가 없는 OS의 경우 빈번한 할당/해제는 파편화 문제를 발생시킬 것이다.

메모리 풀은 이런 문제들을 해결할 수 있는 좋은 방법이다. 미리 유저가 큰 메모리를 할당 받은뒤에 풀에 저장하고 필요한 경우 그 메모리를 분할하여 사용하고 반납하는 것으로 유저레벨 메모리 할당/해제를 수행할 수 있다. 이런 방법으로 시스템 콜을 줄이고, 불필요한 메모리 할당을 줄일 수 있다. 그리고 실제 사용하는 패턴에 맞춰 메모리 공간을 빈틈없이 사용할 수 있다면, 파편화 이슈를 잡는 것은 물론 Locality를 높여 성능을 향상시킬 수 도 있다.

메모리 풀 예제 코드

코드참조 https://github.com/zeliard/GSP

  • 메모리 풀 생성

MemoryPool::MemoryPool() { //32 바이트 증가순으로 사이즈잘라서 메모리 할당 (bucket 형식) int recent = 0; for (int i = 32; i < 1024; i+=32) { SmallSizeMemoryPool* pool = new SmallSizeMemoryPool(i); //메모리 크기별 적합한 풀을 해싱하는 코드 for (int j = recent+1; j <= i; ++j) { mSmallSizeMemoryPoolTable[j] = pool; } recent = i; } //128 바이트 증가순으로 사이즈잘라서 메모리 할당 for (int i = 1024; i < 2048; i += 128) { SmallSizeMemoryPool* pool = new SmallSizeMemoryPool(i); //메모리 크기별 적합한 풀을 해싱하는 코드 for (int j = recent + 1; j <= i; ++j) { mSmallSizeMemoryPoolTable[j] = pool; } recent = i; } //256 바이트 증가순으로 사이즈잘라서 메모리 할당 for(int i = 2048; i <= 4096; i += 256) { SmallSizeMemoryPool* pool = new SmallSizeMemoryPool(i); //메모리 크기별 적합한 풀을 해싱하는 코드 for(int j = recent + 1; j <= i; ++j) { mSmallSizeMemoryPoolTable[j] = pool; } recent = i; } }

  • 메모리 할당
void* MemoryPool::Allocate(int size)
{
   //메모리 앞에 들어갈 헤더정보
   MemAllocInfo* header = nullptr;
   int realAllocSize = size + sizeof(MemAllocInfo);
   // front manager로 관리할 수 있는 사이즈인지 체크
   if (realAllocSize > MAX_ALLOC_SIZE)
   {
      //안들어가면 일반 할당
      header = reinterpret_cast<MemAllocInfo*>(_aligned_malloc(realAllocSize, MEMORY_ALLOCATION_ALIGNMENT));
   }
   else
   {
      //SmallSizeMemoryPool에서 할당
      header = mSmallSizeMemoryPoolTable[realAllocSize]->Pop();
   }
   //헤더에 메모리를 추가하여 반환
   return AttachMemAllocInfo(header, realAllocSize);
}
  • 메모리 해제
void MemoryPool::Deallocate(void* ptr, long extraInfo)
{
   //헤더정보 추출
   MemAllocInfo* header = DetachMemAllocInfo(ptr);
   ///< 최근 할당에 관련된 정보 힌트
   header->mExtraInfo = extraInfo;
   ///< 두번 해제 체크 위해
   long realAllocSize = InterlockedExchange(&header->mAllocSize, 0);

   CRASH_ASSERT(realAllocSize> 0);
   // front manager로 관리할 수 있는 사이즈인지 체크
   if (realAllocSize > MAX_ALLOC_SIZE)
   {
      //back end 해제 (일반 반환)
      _aligned_free(header); 
   }
   else
   {
      //front end 해제 (메모리 풀에 반환)
      mSmallSizeMemoryPoolTable[realAllocSize]->Push(header);
   }
}

오브젝트 풀

오브젝트 풀을 쓰는 이유

오브젝트 풀은 특정 개체(object)를 미리 생성해두고 필요할 때 풀에서 받아 사용하고, 사용을 다하면 반납하여 풀에 다시 저장하는 방식을 말한다. 빈번하게 생성/소멸되는데 비해 생성자/소멸자 호출시 비용이 많이 드는 객체에 대해서 오브젝트 풀을 사용하면 큰 성능 향상을 기대할 수 있을 것이다.(..?)

또한 오브젝트 풀은 메모리 풀의 하위개념으로도 동작할 수 있다. 같은 객체들을 사용하는 메모리 공간을 미리 할당받아서 객체당 필요한 사이즈로 메모리를 분할해놓는다. 이렇게 되면 주어진 연속된 메모리 공간을 또다른 풀로 사용하기 때문에, 전체 영역을 따로 해제할 일이 적기에 빈틈없이 메모리를 사용할 수 있다는 장점이 있고, 같은 종류의 오브젝트를 순회하는 경우 빠른 접근 속도를 기대할 수 있다.

오브젝트 풀 예제 코드

코드참조 https://github.com/zeliard/GSP

  • 오브젝트 생성시 사용하는 new operator 오버라이딩
static void* operator new( size_t objSize )
{
   //lock 잠금
   ClassTypeLock<TOBJECT>::LockGuard();
   //freeList 체크
   if(mFreeList == nullptr)
   {
      //필요한 바이트 개수만큼 오브젝트 freeList의 chunk들을 할당
      mFreeList = new uint8_t[sizeof(TOBJECT)*ALLOC_COUNT];

      //다음번 오브젝트의 주소
      uint8_t* pNext = mFreeList;
      //FreeList의 FLink처럼 다음번 주소를 메모리 공간에 저장한다.
      uint8_t** ppCurr = reinterpret_cast<uint8_t**>( mFreeList );

      for(int i = 0; i < ALLOC_COUNT - 1; ++i)
      {
         //오브젝트 할당 공간 맨 앞(unint8_t사이즈만큼)에 FLink를 저장한다.
         pNext += sizeof(TOBJECT);
    *ppCurr = pNext;
    ppCurr = reinterpret_cast<uint8_t**>( pNext );
      }
      //할당할 수 있는 토탈 카운트 ++
      mTotalAllocCount += ALLOC_COUNT;
   }

   //pAvailable은 사용가능한 공간의 포인터
   //현재 사용가능한 공간은 mFreeList(맨 앞)에 있는 오브젝트 공간
   uint8_t* pAvailable = mFreeList;
   //mFreeList는 FLink를 따라 다음번 오브젝트 메모리로 변경
   mFreeList = *reinterpret_cast<uint8_t**>( pAvailable );

   //현재 사용하는 카운트 ++ 다쓰면 프리리스트 null처리
   if(++mCurrentUseCount > mTotalAllocCount)
   {
      mFreeList = nullptr;
   }

   return pAvailable;
}
  • 오브젝트 생성시 사용하는 delete operator 오버라이딩
static void operator delete( void* obj )
{
   //lock 잠금
   ClassTypeLock<TOBJECT>::LockGuard();
   CRASH_ASSERT(mCurrentUseCount > 0);

   //반환하기 전에 카운트 --
   --mCurrentUseCount;

   //반환된 free chunk에 FLink입력
   *reinterpret_cast<uint8_t**>( obj ) = mFreeList;
   //새로운 freeList의 최초 공간은 반환된 오브젝트
   mFreeList = static_cast<uint8_t*>( obj );
}

STL에서 풀링

STL은 객체들을 관리할 수 있는 컨테이너 객체를 제공한다. 우리가 오브젝트 풀을 사용할때, STL의 도움을 받을 수 있다면 더 사용성이 높고 관리하기 편한 풀을 사용할 수 있을 것이다. STL은 일반적으로 새로운 객체를 받아서 저장할 때, 대상 객체를 복사해서 저장하고, 해제할때는 객체의 소멸자를 호출하는 방식으로 동작한다. 이 동작을 우리의 풀에 맞는 동작으로 변경할 수 있다면 STL에 풀을 적용하는 것이 꿈은 아닐 것이다. 하지만 STL내부 동작을 변경하려면 라이브러리를 건드려야할텐데, 뭔가 마음 한켠에서 '괜찮을까?'하는 울림이 전해져 온다. 이런 우리의 고민을 알아주기라도 하듯 STL은 객체를 받아서 할당하고 지울때 해제하는 동작을 커스터마이징 할 수 있는 API를 제공한다. 이것이 STL Custom Allocator이다.

STL Allocator 사용법

Custom Allocator를 구현하는데 있어 반드시 지켜야할 사항들이 있다.

  • CustomAllocator는 template으로 구현되어야 한다.
  • pointer, reference 타입을 typedef로 정의해야한다.
  • allocator는 멤버 변수를 가질 수 없다. (stateless functor)
  • rebind 템플릿이 필수적이다. 재할당이 필요한 경우 rebind를 allocator 대신 사용하기 때문인듯...

STL Allocator 예제 코드

코드참조 https://github.com/zeliard/GSP

  • 메모리 풀 사용하는 Allocator Template
//템플릿으로 작성할 것
template <class T>
class STLAllocator
{
public:
   STLAllocator() = default;

   //필요한 타입 정의들...
   typedef T value_type;
   typedef value_type* pointer;
   typedef const value_type* const_pointer;
   typedef value_type& reference;
   typedef const value_type& const_reference;
   typedef std::size_t size_type;
   typedef std::ptrdiff_t difference_type;

   //복사 생성자
   template <class U>
   STLAllocator(const STLAllocator<U>&)
   {}

   //rebind 템플릿
   template <class U>
   struct rebind
   {
      typedef STLAllocator<U> other;
   };

   void construct(pointer p, const T& t)
   {
      //replace new (할당하지 않고 생성자만 호출)
      new(p)T(t);
   }

   void destroy(pointer p)
   {
      //replace delete (해제하지 않고 소멸자만 호출)
      p->~T();
   }

   //할당시 호출되는 함수
   T* allocate(size_t n)
   {
      //메모리 풀에서 할당요청
      return GMemoryPool->Allocate(n);
   }
   //해제시 호출되는 함수
   void deallocate(T* ptr, size_t n)
   {
      //메모리풀에 반납요청
      GMemoryPool->Deallocate(ptr, n);
   }
};
  • Allocator STL에 적용하기
//Allocator를 적용한 새로운 X-STL 
template <class T>
struct xvector
{
   typedef std::vector<T, STLAllocator<T>> type;
};

template <class T>
struct xdeque
{
   typedef std::deque<T, STLAllocator<T>> type;
};

template <class T>
struct xlist
{
   typedef std::list<T, STLAllocator<T>> type;
};

template <class K, class T, class C = std::less<K> >
struct xmap
{
   typedef std::map<K, T, C, STLAllocator<std::pair<K,T>>> type;
};

template <class T, class C = std::less<T> >
struct xset
{
   typedef std::set<T, C , STLAllocator<T>> type;
};

template <class K, class T, class C = std::hash_compare<K, std::less<K>> >
struct xhash_map
{
   typedef std::hash_map<K, T, C, STLAllocator<std::pair<K, T>> > type;
};

template <class T, class C = std::hash_compare<T, std::less<T>> >
struct xhash_set
{
   typedef std::hash_set<T, C, STLAllocator<T> > type;
};

template <class T, class C = std::less<std::vector<T>::value_type> >
struct xpriority_queue
{
   typedef std::priority_queue<T, std::vector<T, STLAllocator<T>>, C > type;
};

typedef std::basic_string<wchar_t, std::char_traits<wchar_t>, STLAllocator<wchar_t>> xstring;


//실제 적용사례
//typedef ::type으로 X-STL의 타입을 받아 사용한다.
typedef xlist<ClientSession*>::type ClientList;
ClientList  mFreeSessionList;


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

read-write lock  (2) 2015.04.26
DeadLock  (0) 2015.04.25
Windows Low Fragmentation Heap  (0) 2015.04.17
패킷 여행 in TCP/IP 네트워크 스택  (0) 2015.04.12
PAGE_LOCKING  (1) 2015.04.12