cpp-ru / ideas

Идеи по улучшению языка C++ для обсуждения
https://cpp-ru.github.io/proposals
Creative Commons Zero v1.0 Universal
90 stars 0 forks source link

memory_traits спецификатор для allocator #231

Open apolukhin opened 3 years ago

apolukhin commented 3 years ago

Перенос предложения: голоса +0, -3 Автор идеи: Victor Gubin

Мотивация. STL контейнеры позволяют определить собственный распределитель памяти, и принимают std::allocator в качестве параметра по умолчанию. Если нужно использовать распределитель памяти, отличный от используемого std::allocator - следует реализовать собственный шаблон Allocator, как правило путем копирования кода std::allocator и внесением незначительных (в объеме кода) изменений.

В большинстве случаев создание не стандартной реализации шаблона allocator, связанно с необходимостью заменить вызовы к базовом операторам new и delete. Это необходимо для замены универсального алгоритма распределения памяти используемого стандартной библиотекой, и не подходящего для конкретного случая. При этом глобально переопределять std::new и std::delete нежелательно.

В тоже время, шаблон allocator весьма сложный класс, и все STL контейнеры требуют четкого соблюдения интерфейса Allocator от любой не стандартной реализации. Таким образом, для замены обращений к стандартному распределителю памяти, на более подходящий для конкретного контейнера, нужно посути продублировать код std::allocator в собственной реализации, изменив всего несколько строк кода.

Концепция memory_traits

memory_traits это небольшой proxy интерфейс над базовым распределителем памяти, который просто реализуем и может быть передан базовому шаблону allocator.

Например для обращения к глобальным операторам new и delete memory_traits будет выглядеть следующим образом

struct memory_traits {

    static void* allocate(std::size_t count) {
        return ::operator new( count );
    }

    static void* allocate(std::size_t size, const std::nothrow_t&) noexcept
    {
        return ::operator new( size, std::nothrow );
    }

    static void release(void* const ptr) noexcept
    {
        ::operator delete( ptr );
    }

};

Тогда в стандартной библиотеке можно ввести следующий шаблон

template<typename T, typename MemoryTraits >
class basic_allocator {
    typedef MemoryTraits memory_traits_type;
....
    pointer allocate(size_type __n, const void* = 0) {
        ....
        pointer *ptr = memory_traits_type::allocate( _n );
        ....
    }
...
    // rest 
};

И спецификацию std::allocator можно определить как ( если это нужно ).

template<typename T>
class allocator: public basic_allocator <T, memory_traits> {
    typedef std::size_t size_type;
    typedef ptrdiff_t difference_type;
    typedef T* pointer;
    typedef const T* const_pointer;
    typedef T&  reference;
    typedef const T& const_reference;
    typedef T value_type;

    template<typename T1>
    struct rebind {
        typedef allocator<T1> other;
    };

    constexpr allocator() noexcept:
        basic_allocator<T, memory_traits>()
    {}

    constexpr allocator(const allocator& other) noexcept:
        heap_allocator_base<T, memory_traits>( other )
    {}

    template<typename _Tp1>
    constexpr allocator(const allocator<_Tp1>& other) noexcept
    {}

    ~allocator() noexcept = default;
};

Таким образом, можно легко заменять std::allocator для любого контейнера, потребуется только реализовать memory_traits и передать его шаблону std::basic_allocator.

Например:

Введение memory_traits и std::basic_allocator затронут уже существующий код. Но определить Allocator для конкретного контейнера станет намного проще.

apolukhin commented 3 years ago

Victor Gubin, 9 октября 2017, 13:32 То что "вылетело" из секции например, и в чем был весь смысл:

#include <jemalloc/jemalloc.h>
#include <windows.h>

namespace jemalloc {

struct memory_traits {

static void* allocate(std::size_t size) {
    void *ret = nullptr;
    // some while(nullptr == __builtin_expect( (ret == std::malloc) ,false) ) {
    while ( nullptr == (ret = je_malloc(size)) ) {
        std::new_handler handler = std::get_new_handler();
        if(nullptr == handler)
            throw std::bad_alloc();
        handler();
    }
    return ret;
}

static void* allocate(std::size_t size, const std::nothrow_t&) noexcept {
    void *ret = nullptr;
    // some while(nullptr == __builtin_expect( (ret == std::malloc) ,false) ) {
    while ( nullptr == (ret = ::je_malloc(size)) ) {
        std::new_handler handler = std::get_new_handler();
        if(nullptr == handler)
            return nullptr;
        handler();
    }
    return ret;
}

static void release(void* const ptr) noexcept {
    ::je_free( ptr );
}

};

} // jemalloc

namespace windows {

class memory_traits {
public:
    static void* allocate(std::size_t size) {
        void *ret = nullptr;
        // use some __assume
        while ( nullptr == (ret = ::HeapAlloc( instance()->heap_, 0, size) ) ) {
            std::new_handler handler = std::get_new_handler();
            if(nullptr == handler)
                throw std::bad_alloc();
            handler();
        }
    }

    static void* allocate(std::size_t size, const std::nothrow_t&) noexcept {
        void *ret = nullptr;
        // use some __assume
        while ( nullptr == (ret = ::HeapAlloc( instance()->heap_, 0 , size ) ) ) {
            std::new_handler handler = std::get_new_handler();
            if(nullptr == handler) {
                errno = ENOMEM;
                return nullptr;
            }
            handler();
        }
        return ret;
    }

    static void release(void* const ptr) noexcept {
        ::HeapFree(instance()->heap_, 0, const_cast<LPVOID>(ptr) );
    }
private:

    static const memory_traits* instance() noexcept {
        static memory_traits _instance( ::HeapCreate(0,0,0) );
        return &_instance;
    }

    constexpr explicit memory_traits(::HANDLE heap) noexcept:
        heap_(heap)
    {}

    ::HANDLE heap_;
};

} // namesapce windows

...
void main() {

    // std::allocator ( new/delete in most cases )
    std::vector<int> v;

    // red-black map with je_malloc nodes
    std::set< int, std::less<int>, std::basic_allocator<int, jemalloc::memory_traits > > s;

    // hash table with windows private heap allocator for buckets
    typedef std::unordered_map<
        int, std::string
        std::hash<int>,
        std::equal_to<int>,
        std::basic_allocator< std::pair<int,std::string>, windows::memory_traits>
    > int_hash_map;

}

Will Code For Food, 25 октября 2017, 14:08 Это же std::pmr::polymorphic_allocator и std::pmr::memory_resource, не?

Виктор Губин, 18 марта 2019, 17:31 Will Code For Food,

Несовсем, std::pmr::polymorphic_allocator/std::pmr::memory_resource - обладают очень нежелательным свойством. get_default_resource/set_default_resource

Теперь положим, у меня в программе есть пул из N потоков. Есть контейнер с бинарной кучей - priority queue, он разделятеся между всеми потоками и синхронизируется. Остальные потоки имеют множество других контейнеров, которые с специфичными аллокаторами (отдельный для строк, деревьев и хеш таблиц). Memory resource при этом глобальный (даже если он будет thread_local это не сильно поможет, если контейнеров с "хитрыми" аллокаторами более одного).