기존 오브젝트 풀 개선
https://jartlife.tistory.com/59
이전 게시글의 오브젝트 풀을 개선하여 이번 프로젝트에 쓸 메모리 풀을 만들었다.
커스텀 삭제자를 지정한 유니크 포인터를 통해 자동 반납을 구현했다.
객체 생성자의 호출 시점을 할당 시점으로 옮기고, 소멸자의 호출 시점을 반납 시점으로 옮겼다.
alloc은 생성자로 인자를 전달하므로 기본 생성자가 없거나 생성자 종류가 여러 개인 타입에 대해서도 원하는 만큼 할당할 수 있다.
개요
pool<Ty>
: Ty 타입 객체를 관리하는 메모리 풀입니다.
객체를 위한 메모리를 미리 확보하여 효율적으로 관리합니다.
동적할당하듯이 pool에서 객체를 할당합니다.
- constructor
생성자
- parameters : capacity
: 총 몇 개의 객체를 할당 가능한 풀인지를 알려야 합니다.
- parameters : capacity
- copy
복사 생성자, 복사 할당 연산자
std::shared_ptr 처럼, 참조 카운트를 통해 pool 객체를 관리합니다.
복사는 새로운 pool을 확보하지 않습니다. 원본 pool을 공유하게 됩니다. (얕은 복사)
공유하고 있는 pool이 아직 남아있다면, pool이 소멸해도 확보한 메모리는 소멸하지 않습니다. - move
이동 생성자, 이동 할당 연산자
이동 의미론(move semantics)을 따릅니다. 원본의 소유권을 가져옵니다.
역시 std::shared_ptr 처럼 동작하여, 이동 시엔 참조 카운트가 변화하지 않습니다. - alloc
객체 할당- parameters : args...
: Ty 객체 생성자가 요구하는 인자들을 받아 그대로 전달합니다. - template parameters : arr_size
: 템플릿 파라미터 지정은 배열 반환 요구의 의미입니다.
전달된 인수는 메모리 풀이 할당할 배열의 크기가 됩니다. - return type : std::unique_ptr< Ty, pool< Ty >::dealloc_func >
( 템플릿 파라미터 미지정 시 )
: 객체를 담은 유니크 포인터를 반환합니다.
할당된 객체의 생존이 끝나면 자동으로 메모리 풀에 반납됩니다.
pool< Ty >::Uptr 혹은 auto 타입의 변수에 할당받으십시오. - return type : std::array< std::unique_ptr< Ty, pool< Ty >::dealloc_func >, arr_size >
( 템플릿 파라미터 지정 시 )
: 객체를 담은 유니크 포인터의, 배열을 반환합니다.
할당된 객체의 생존이 끝나면 자동으로 메모리 풀에 반납됩니다.
std::array< pool< Ty >::Uptr, arr_size > 혹은 auto 타입의 변수에 할당받으십시오. - throw : 할당 가능한 메모리가 없을 때 객체를 할당하려 하면 std::bad_alloc 예외를 던집니다.
- parameters : args...
- salloc, ralloc
객체 할당
alloc과 기능과 매개변수는 같으며, 반환 타입만 다릅니다.
salloc은 std::shared_ptr< Ty > ( pool< Ty >::Sptr 로도 받을 수 있습니다. ) 를 반환합니다.
ralloc은 Ty* 를 반환합니다.
std::shared_ptr, 혹은 Ty*를 이용하는 다른 코드와의 간편한 호환을 위해 만들었습니다.
ralloc의 경우 자동 반납이 되지 않으니 아래의 dealloc 함수를 수동으로 호출해줘야 합니다.
템플릿 파라미터 지정 시 배열을 반환합니다. - dealloc
객체 반납
ralloc으로 할당한 Ty*를 다시 pool에 반납합니다.
- parameters : exhausted
: 반납할 Ty* 입니다.
- parameters : exhausted
- available_cnt
할당 가능한 객체 수- return type : const size_t
: 현재 할당 가능한 객체 수를 반환합니다.
- return type : const size_t
코드
#ifndef _pool
#define _pool
#include <memory>
#include <utility>
#include <array>
#include <type_traits>
#include <algorithm>
template < typename Ty >
class pool
{
public:
class dealloc_func
{
public:
dealloc_func( pool& pl ) : pl{ pl } {}
void operator()( Ty* exhausted ) noexcept
{
pl.dealloc( exhausted );
}
private:
pool& pl;
};
using byte = char;
using byte_ptr = char*;
using byte_pptr = char**;
using Uptr = std::unique_ptr< Ty, dealloc_func >;
using Sptr = std::shared_ptr< Ty >;
// requires constructor args
// returns an unique pointer
// automatically deallocate
template < typename ... Args >
Uptr alloc( Args&& ... args )
{
return alloc_impl< Uptr >( std::forward< Args >( args )... );
}
// requires constructor args
// returns a shared pointer
// automatically deallocate
template < typename ... Args >
Sptr salloc( Args&& ... args )
{
return alloc_impl< Sptr >( std::forward< Args >( args )... );
}
// requires constructor args
// returns a raw pointer
template < typename ... Args >
Ty* ralloc( Args&& ... args )
{
return alloc_impl< Rptr >( std::forward< Args >( args )... );
}
// requires array size template args, constructor args
// returns array of unique pointer
// automatically deallocate
template < size_t arr_size, typename ... Args >
std::array< Uptr, arr_size > alloc( Args&& ... args )
{
return alloc_array_impl< Uptr >( std::make_index_sequence< arr_size >{}, std::forward< Args >( args )... );
}
// requires array size template args, constructor args
// returns array of shared pointer
// automatically deallocate
template < size_t arr_size, typename ... Args >
std::array< Sptr, arr_size > salloc( Args&& ... args )
{
return alloc_array_impl< Sptr >( std::make_index_sequence< arr_size >{}, std::forward< Args >( args )... );
}
// requires array size template args, constructor args
// returns array of raw pointer
template < size_t arr_size, typename ... Args >
std::array< Ty*, arr_size > ralloc( Args&& ... args )
{
return alloc_array_impl< Rptr >( std::make_index_sequence< arr_size >{}, std::forward< Args >( args )... );
}
// requires object's pointer to deallocate
// It doesn't do anything if the pointer was already deallocated.
// only for raw pointer
void dealloc( Ty*& exhausted ) noexcept
{
if ( exhausted )
{
exhausted->~Ty();
putmem( reinterpret_cast< byte_ptr >( exhausted ) );
exhausted = nullptr;
++*avl_cnt;
}
}
const size_t available_cnt() const noexcept
{
return *avl_cnt;
}
pool( const size_t capacity ) : mem{ new byte[ safe_mem() * capacity + sizeof( void* ) ] },
free_ptr{ mem }, avl_cnt{ new size_t{ capacity } }, ref_cnt{ new size_t{ 1 } }
{
// write next memory's address in each memory
byte_pptr cur = reinterpret_cast< byte_pptr >( mem );
byte_ptr next = mem;
for ( size_t i = 0; i < capacity; ++i )
{
next += safe_mem();
*cur = next;
cur = reinterpret_cast< byte_pptr >( next );
}
*cur = nullptr;
}
pool( const pool& other ) noexcept : mem{ other.mem }, free_ptr{ other.free_ptr },
avl_cnt{ other.avl_cnt }, ref_cnt{ other.ref_cnt }
{
++*ref_cnt;
}
pool& operator=( const pool& other ) noexcept
{
if ( this != &other )
{
--*ref_cnt;
mem = other.mem; free_ptr = other.free_ptr;
avl_cnt = other.avl_cnt; ref_cnt = other.ref_cnt;
++*ref_cnt;
}
return *this;
}
pool( pool&& other ) noexcept : mem{ other.mem }, free_ptr{ other.free_ptr },
avl_cnt{ other.avl_cnt }, ref_cnt{ other.ref_cnt }
{
other.mem = nullptr; other.free_ptr = nullptr;
other.avl_cnt = nullptr; other.ref_cnt = nullptr;
}
pool& operator=( pool&& other ) noexcept
{
if ( this != &other )
{
mem = other.mem; free_ptr = other.free_ptr;
avl_cnt = other.avl_cnt; ref_cnt = other.ref_cnt;
other.mem = nullptr; other.free_ptr = nullptr;
other.avl_cnt = nullptr; other.ref_cnt = nullptr;
}
return *this;
}
~pool()
{
if ( mem )
{
if ( !--*ref_cnt )
{
delete[] mem;
delete ref_cnt;
delete avl_cnt;
}
}
}
private:
// for safe memory writing, each memory size must be over sizeof( void* ).
static constexpr const size_t safe_mem() noexcept
{
return std::max( sizeof( Ty ), sizeof( void* ) );
}
template < typename Ptr_t, typename ... Args >
auto alloc_impl( Args ... args )
{
check_avl_cnt();
Ptr_t ret{ create_obj( std::forward< Args >( args )... ), dealloc_func{ *this } };
--*avl_cnt;
return ret;
}
template < typename Ptr_t, typename ... Args, size_t ... Idx >
auto alloc_array_impl( std::index_sequence< Idx... >, Args&& ... args )
{
return std::array<
std::conditional_t< std::is_same_v< Ptr_t, Rptr >, Ty*, Ptr_t >,
sizeof...( Idx )
> { _dummy( alloc_impl< Ptr_t >( std::forward< Args >( args )... ), Idx )... };
}
void check_avl_cnt()
{
if ( !*avl_cnt )
{
throw std::bad_alloc{};
}
}
template < typename ... Args >
Ty* create_obj( Args ... args )
{
return new( getmem() ) Ty{ std::forward< Args >( args )... };
}
byte_ptr getmem() noexcept
{
byte_ptr ret = free_ptr;
free_ptr = *reinterpret_cast< byte_pptr >( free_ptr );
return ret;
}
void putmem( byte_ptr mem ) noexcept
{
*reinterpret_cast< byte_pptr >( mem ) = free_ptr;
free_ptr = mem;
}
template < typename Tx >
decltype(auto) _dummy( Tx&& elem, size_t ) const noexcept
{
return std::forward< Tx >( elem );
}
// for template, raw pointer uselessly require dealloc_func
struct Rptr
{
Rptr( Ty* inst, dealloc_func ) : inst{ inst } {}
operator Ty*() { return inst; }
Ty* inst;
};
byte_ptr mem;
byte_ptr free_ptr;
size_t* avl_cnt;
size_t* ref_cnt;
};
#endif
|
cs |
예시
struct test_pool
{
int x, y, z;
test_pool( int x, int y, int z ) : x{ x }, y{ y }, z{ z } { std::cout << "생성자 호출\n"; }
~test_pool() { std::cout << "소멸자 호출\n"; }
};
|
cs |
#include <iostream>
#include "pool.h"
int main()
{
pool< test_pool > pl1{ 20 };
auto pl = std::move( pl1 );
for ( int i = 0; i < 5; ++i )
{
std::cout << "\n-----------------------------------------\n";
for ( int j = 0; j < 5; ++j )
{
auto rptr = pl.ralloc( j + 1, j + 2, j + 3 );
std::cout << rptr->x << ", " << rptr->y << ", " << rptr->z << std::endl;
pl.dealloc( rptr );
}
}
}
|
cs |
#include <iostream>
#include "pool.h"
int main()
{
pool< test_pool > pl1{ 20 };
auto pl = std::move( pl1 );
for ( int i = 0; i < 5; ++i )
{
std::cout << "\n-----------------------------------------\n";
auto arr = pl.ralloc< 5 >( i + 1, i + 2, i + 3 );
for ( auto& iter : arr )
{
std::cout << iter->x << ", " << iter->y << ", " << iter->z << std::endl;
pl.dealloc( iter );
}
std::cout << pl.available_cnt() << std::endl;
}
}
|
cs |
#include <iostream>
#include "pool.h"
#include "TMP.h"
int main()
{
pool< test_pool > pl{ 10'000'000 };
std::cout << "원시 동적 할당 : " << timefunc([]()
{
for ( int i = 0; i < 10'000'000; ++i )
{
auto ptr = new test_pool{ 1, 2, 3 };
delete ptr;
}
} ) << " ms 소요\n";
std::cout << "풀 동적 할당 : " << timefunc( [&pl]()
{
for ( int i = 0; i < 10'000'000; ++i )
{
auto ptr = pl.alloc( 1, 2, 3 );
}
} ) << " ms 소요\n";
}
|
cs |
release 모드에선 풀 쪽이 압도적으로 빠르다.
debug 모드로 실행시엔 원시 동적 할당이 워낙 성능이 중구난방이기에 풀이 빠르거나 비슷하다.
템플릿, 함수 객체, 람다, 가변 인자, reinterpret_cast 이런 것들을 가지고 놀다 보면 원래 언어로 불가능했어야 할 것들을 억지로 가능하게 만드는 느낌이라 좀 부담스럽다.
적어도 학교에서 배운 c++ 과는 전혀 상관 없는 내용이니...
이 풀을 기반으로 다음에 FMOD API 를 이용하는 클래스인 sound 를 만들어 FMOD::Sound 객체와 FMOD::Channel 객체를 관리하도록 할 예정이다.
'Project > Win32API로 던그리드 모작' 카테고리의 다른 글
게임 타이머 구현 (0) | 2021.09.21 |
---|---|
git repository 생성 (0) | 2021.09.17 |
메모리 풀, 오브젝트 풀 개념 (0) | 2021.08.25 |
게임 저장 구현 - 구조체를 파일로 저장하기 (0) | 2021.08.24 |
데이터베이스 구현 (0) | 2021.08.23 |