현재 적용할 메모리 풀
Project/Win32API로 던그리드 모작

현재 적용할 메모리 풀

Woon2World :: Programming & Art Life

 

기존 오브젝트 풀 개선

https://jartlife.tistory.com/59

 

메모리 풀, 오브젝트 풀 개념

힙 메모리 단편화 힙 단편화( heap fragmentation ), 메모리 단편화( memory fragmentation )이라 부르는 이 현상은 객체를 자주 동적할당할 때 발생한다. 메모리들이 직렬화되어 전부 붙어있으면 좋겠지만,

jartlife.tistory.com

 

이전 게시글의 오브젝트 풀을 개선하여 이번 프로젝트에 쓸 메모리 풀을 만들었다.

커스텀 삭제자를 지정한 유니크 포인터를 통해 자동 반납을 구현했다.

객체 생성자의 호출 시점을 할당 시점으로 옮기고, 소멸자의 호출 시점을 반납 시점으로 옮겼다.

alloc은 생성자로 인자를 전달하므로 기본 생성자가 없거나 생성자 종류가 여러 개인 타입에 대해서도 원하는 만큼 할당할 수 있다.

 

 

 

 


개요

 

pool<Ty>

: Ty 타입 객체를 관리하는 메모리 풀입니다.

객체를 위한 메모리를 미리 확보하여 효율적으로 관리합니다.

동적할당하듯이 pool에서 객체를 할당합니다.

 

  • constructor
    생성자
    • 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 예외를 던집니다.

  • 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* 입니다.

  • available_cnt
    할당 가능한 객체 수
    • 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 + sizeofvoid* ) ] },
        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 ), sizeofvoid* ) );
    }
 
    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-><< ", " << rptr-><< ", " << rptr-><< 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-><< ", " << iter-><< ", " << iter-><< 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{ 123 };
                delete ptr;
            }
        } ) << " ms 소요\n";
 
    std::cout << "풀 동적 할당 : " << timefunc( [&pl]()
        {
            for ( int i = 0; i < 10'000'000++i )
            {
                auto ptr = pl.alloc( 123 );
            }
        } ) << " ms 소요\n";
}
cs

 

 

 

release 모드에선 풀 쪽이 압도적으로 빠르다.

debug 모드로 실행시엔 원시 동적 할당이 워낙 성능이 중구난방이기에 풀이 빠르거나 비슷하다.

 

 

 

 


템플릿, 함수 객체, 람다, 가변 인자, reinterpret_cast 이런 것들을 가지고 놀다 보면 원래 언어로 불가능했어야 할 것들을 억지로 가능하게 만드는 느낌이라 좀 부담스럽다.

 

적어도 학교에서 배운 c++ 과는 전혀 상관 없는 내용이니...

이 풀을 기반으로 다음에 FMOD API 를 이용하는 클래스인 sound 를 만들어 FMOD::Sound 객체와 FMOD::Channel 객체를 관리하도록 할 예정이다.