allocator、polymorphic allocator 與 memory_resource

ticlab發表於2024-07-05

allocator、polymorphic allocator 與 memory_resource
cpp allocator

Created: 2024-07-04T10:59+08:00
Published: 2024-07-05T11:27+08:00
Categories: C-CPP

custom allocator

std::allocator 是無狀態的,實測最簡單的 allocator 只需要:

  1. value_type
  2. allocate
  3. deallocate

rebind 目的是實現 rebind(allocator<TypeA>, TypeB) == allocator<TypeB>
C++11 已經使用 allocator_traits 實現了這種想法[1],並且 C++17 就拋棄了以前把 rebind 放到 allocator 內部實現的方法。

還有兩個函式 constructdestroy,如果提供,就會使用我們自定義的,不提供也沒有問題,allocator_traits 提供了預設的實現,
container 本身就是透過 allocator_traits 來使用 construct 和 destroy。

  1. construct:在 allocate 後呼叫,在分配的記憶體上初始化物件
  2. destroy: 在 deallocate 前呼叫,在記憶體上析構分配的物件

以下是一個非常簡單的 allocator 實現。

#include <vector>
#include <iostream>
#include <memory>

using std::vector;

struct point
{
    int x{1};

    point(int x_) : x(x_) {}

    ~point()
    {
        std::cout << "detor, x:" << x << std::endl;
    }
};

template <typename T>
class MyAllocator
{
public:
    using value_type = T;
    T *allocate(size_t n)
    {
        std::cout << "myallocator.allocate: " << n << std::endl;
        return (T *)::operator new(n * sizeof(T));
    }

    void deallocate(T *p, size_t n)
    {
        std::cout << "myallocator.deallocate: " << n << std::endl;
        return ::operator delete(p);
    }

    // template <typename _Up, typename... _Args>
    // void construct(_Up *__p, _Args &&...__args) noexcept(noexcept(::new((void *)__p) _Up(std::forward<_Args>(__args)...)))
    // {
    //     std::cout << "construct called" << std::endl;
    //     // 表示在 地址 _p 上呼叫物件 _Up的建構函式
    //     // 其中,__args是建構函式的引數
    //     ::new ((void *)__p) _Up(std::forward<_Args>(__args)...);
    // }

    // template <typename _Up>
    // void destroy(_Up *__p) noexcept(noexcept(__p->~_Up()))
    // {
    //     std::cout << "destroy called" << std::endl;
    //     __p->~_Up();
    // }
};

int main()
{
    point a{2};
    vector<point, MyAllocator<point>> vp{3,4,5};
    vp[0].x = 100;

    for (auto& i: vp) {
        std::cout << i.x << std::endl;
    }

    return 0;
}

多型記憶體資源(PMR, polymorphic memory resource)

使用 PMR 原因

為什麼需要 PMR 呢,因為[2]

  1. allocator 是模板簽名的一部分。不同 allocator 的容器,無法混用。
  2. c++11 以前,allocator 無狀態;c++11 以後,可以有狀態,然而 allocator 型別複雜難用。
  3. allocator 記憶體對齊無法控制,需要傳入自定義 allocator。

以上三點、特別是第一點,造成 stl 無法成為軟體介面 (interface) 的一部分。
難以將 memory arena、memory pool 用於 stl 容器。

比如自定義的 allocator 沒法和 stl 預設的 allocator 通用:

vector<int, MyAllocator<int>> vi {1,2,3};
vector<int, std::allocator<int>> vi_copy = vi; // error!

memory_resource 和 polymorphic_allocator

PMR 就說,好吧,那我們把 allocator 固定下來,全都使用 polymorphic_allocator<T>polymorphic_allocator<T> 持有一根 memory_resource 的指標[3],分配策略透過 memory_resource 實現。

The class template std::pmr::polymorphic_allocator is an Allocator which exhibits different allocation behavior depending upon the std::pmr::memory_resource from which it is constructed. Since memory_resource uses runtime polymorphism to manage allocations, different container instances with polymorphic_allocator as their static allocator type are interoperable, but can behave as if they had different allocator types.
All specializations of polymorphic_allocator meet the allocator completeness requirements.
std::pmr::polymorphic_allocator - cppreference.com

memory_resource 提供對原始記憶體的管理介面,類似 malloc 和 free:

memory_resource:
+ allocate: 提供給 polymorphic_allocator,呼叫 do_allocate
+ deallocate:提供給 polymorphic_allocator,呼叫 do_deallocate
# do_allocate: 內部分配記憶體的方法,像 malloc
# do_deallocate: 內部回收記憶體的方法,像 free

polymorphic_allocator<T> 只是在原始記憶體 memory_resource 上提供具體型別的抽象,比如需要 n 個型別為 T 的物件,底層呼叫 memory_resource 獲取原始記憶體。

std::pmr::polymorphic_allocator<T>::allocate:
Allocates storage for n objects of type T using the underlying memory resource. Equivalent to return static_cast<T*>(resource()->allocate(n * sizeof(T), alignof(T)));.
std::pmr::polymorphic_allocator::allocate - cppreference.com

construct 和 destroy 透過 allocator_traits 實現:

/// Partial specialization for std::pmr::polymorphic_allocator
  template<typename _Tp>
    struct allocator_traits<pmr::polymorphic_allocator<_Tp>>

現成的 memory_resource

上面提到的分離介面可以實現不同 allocator 之間的通用,但是具體要讓記憶體分配快起來需要高效的 memory_resource 實現,C++17 提供了五種[4]:

記憶體資源 行為
new_delete_resource() 返回一個呼叫newdelete的記憶體資源的指標
synchronized_pool_resource 建立更少碎片化的、執行緒安全的記憶體資源的類
unsynchronized_pool_resource 建立更少碎片化的、執行緒不安全的記憶體資源的類
monotonic_buffer_resource 建立從不釋放、可以傳遞一個可選的緩衝區、執行緒不安全的類
null_memory_resource() 返回一個每次分配都會失敗的記憶體資源的指標

這三種比較重要:

  1. std::pmr::monotonic_buffer_resource - cppreference.com
  2. std::pmr::synchronized_pool_resource - cppreference.com
  3. std::pmr::unsynchronized_pool_resource - cppreference.com

在介面呼叫中提到了 upstream 的概念:如果當前 memory_resource 記憶體不足,則呼叫 upstream memory_resource 的 allocate 方法[5]
其實是有一個預設的 memory_resource 的,就是預設的 ::operator new::operator delete 管理記憶體。
而且 memory_resource 必須要有 upstream,看 monotonic_buffer_resource 的原始碼:

    monotonic_buffer_resource(memory_resource* __upstream) noexcept
    __attribute__((__nonnull__))
    : _M_upstream(__upstream)
    { _GLIBCXX_DEBUG_ASSERT(__upstream != nullptr); }

用法

std::pmr::monotonic_buffer_resource - cppreference.com
Cpp17/markdown/src/ch29.md at master · MeouSker77/Cpp17

總結

  1. stl 容器需要 allocator<T>
  2. allocator_traits 規定訪問 allocator 成員的標準介面[6]。可以實現 rebind 等操作,以及物件的 construct 和 destroy 也在 traits 中有預設實現
  3. 但是不同的 allocator 沒法通用、無狀態、難以實現自定義的分配策略,所以用 polymorphic_allocator 和 memory_resource 出現了
  4. polymorphic_allocator<T> 內部持有 memory_resource 指標,統一了 allocator 介面,只封裝了一層要管理的型別 T
  5. memory_resource 內部來實現分配策略,和分配的具體物件無關
  6. 提供了五種特別的 memory_resource 實現

  1. std::allocator_traits - cppreference.com ↩︎

  2. 遊戲引擎開發新感覺!(6) c++17 記憶體管理 - 知乎 ↩︎

  3. std::pmr::memory_resource - cppreference.com ↩︎

  4. Cpp17/markdown/src/ch29.md at master · MeouSker77/Cpp17 ↩︎

  5. std::pmr::monotonic_buffer_resource - cppreference.com ↩︎

  6. std::allocator_traits - cppreference.com ↩︎

相關文章