黃爸爸狗園

本園只有sanitizer,沒有狗籠

0%

C++ Polymorphic allocator,花式記憶體管理 (三)

本系列文為C++ Weekly PMR Series的筆記文之三

我們在上篇文章中說明了自訂義memory_resource的方法,以及建構pmr容器的一些注意事項 接下來我們將繼續深入探討PMR的細節

還漏掉了一片拼圖

  • 我們先前已經看過了pmr::vector跟pmr::string,還有pmr::vector<pmr::string>
    • 我們使用了vector.emplace_back()來插入pmr::string,使得vector內部的所有string都依靠vector的allocator來建構
    • 但問題來了,世界不是只有美國,C++也不是只有string
  • 假設今天我們有一個自己寫的class S,我們要如何才可以讓S跟pmr::string一樣可以被放進pmr容器中,並且與容器使用相同的allocator呢?
    • 要解決這個問題,我們得先分解問題為兩個部分
      • 如何建構一個支援Polymorphic allocator的class S
      • 如何讓容器裡的instance S與容器共用allocator
  • 先來看一眼pmr這個namespace是何方神聖,我們寫了他很多次,但都沒仔細看過他
    • vector的定義
    • string的定義
  • 我們可以看出無論是vector還是string,所謂的namespace pmr就只是把原本的Allocator<T>換成了polymorphic_allocator<T>而已
    • 咦!?原本就有Allocator了? 那幹嘛我們還要再弄一個進去?

Allocator<T> V.S. polymorphic_allocator<T>

  • 會發生這種奇怪問題的原因是因為原本的allocator在設計上並不是考慮的非常周全,例如以下的情況
    1
    2
    3
    4
    std::vector<int> std_vec {1, 2, 3} ; // same as std::vector<int, std::allocator<int>>
    std::vector<int, custom_allocator> custom_vec;

    custom_vec = std_vec ; // Error!
  • 可以看到的是,在使用allocator的狀況下,不同allocator的vector他們具有不同的型別,一些vector與vector之間的操作就變得不好實作了
    • 如同上面示範的assign,因為std_vec與custom_vec的型別不同了,因此編譯會失敗,你得自己徒手實作operator =才行
    • 更麻煩的是,即便你寫了一個可以處理custom_vec的operator =,萬一我又寫了一個新的allocator(我們叫他bump allocator好了),同樣的事情又要再來一遍,豈不是很惱人?
    • 聰明的小吉肯定會想到要用template來解,的確,template可以解決掉這個問題
    • 但代價是代碼膨脹以及編譯時間增加,就看你覺得值不值得
  • 所以C++17引入了polymorphic_allocator,顧名思義,原本依靠不同的allocator<T>來切換記憶體管理策略的方式,現在一率都變成用繼承的方式來處理
    • 你可以看到無論是先前提過的monotonic_buffer_resource,還是等等我們會介紹的unsynchronized_pool_resource,他們全部都繼承自std::pmr::memory_resource
    • 反過來說,任何pmr容器內部都會有一個polymorphic_allocator<T>,透過其內部不同的memory_resource來實現不同的記憶體管理行為
      • 但我們從外面看,容器型別都是一樣的(Allocator都是polymorphic_allocator<T>)
  • 如此一來,Allcator<T>的問題就能夠被解決掉,但由於polymorphic_allocator背後是仰賴v_table機制運作,因此virtual function call的overhead就必須要被考慮
    • 假設你很在乎的話啦...

Allocator-Aware Types

  • 在釐清完allocator的問題後,我們就要來實際來解決第一個問題: 建構一個支援Polymorphic allocator的class S
    • 由於我們只打算讓class支援polymorphic allocator,所以可以直接一點,直接塞進constructor裏頭,就不用那麼麻煩搞一個namespace pmr了
    • 我們沿用上一篇文章的範例,稍微修改一下(code在這)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      class S {
      public:
      S(const char* str) :
      _str(str)
      {

      }

      S(const char* str, const std::pmr::polymorphic_allocator<>& allocator) :
      _str(str, allocator), _alloc(allocator)
      {

      }

      private:
      std::pmr::string _str = "long long long long long long";
      std::pmr::polymorphic_allocator<> _alloc;
      };
    • 我們試著弄了一個class S,然後給他兩個constructor
      • 一個普通的
      • 跟一個可以塞pmr allocator的
  • 看起來也沒有很難嘛,但是要接著解決下面的另一個問題: 讓容器裡的instance S與容器共用allocator
    • 我們試著做一次之後就發現事情沒有想像的那麼容易...
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      template <typename Container, typename... Values>
      auto create_container(auto *resource, Values&&... values) {
      Container result{resource};
      result.reserve(sizeof...(values));
      (result.emplace_back(std::forward<Values>(values)), ...);
      return result;
      }

      int main() {
      print_alloc mem;
      std::pmr::set_default_resource(&mem);

      std::array<std::uint8_t, 1024> buffer{};
      std::pmr::monotonic_buffer_resource mem_resource(
      buffer.data(),
      buffer.size()
      );

      std::cout << "initializing vec<S>\n";
      auto container = create_container<std::pmr::vector<S>> (
      &mem_resource,
      "short string",
      "A really long string here",
      "Another really long string here"
      );
      std::cout << "exiting main\n";
      } // main()
    • 對應的輸出為
      1
      2
      3
      4
      5
      6
      initializing vec<S>
      Allocating 26
      Allocating 32
      exiting main
      Deallocating 26: 'A really long string here'
      Deallocating 32: 'Another really long string here'
    • vector內的所有具有long string的S通通都被print_alloc抓到了
    • 我們在上篇文章中建立std::pmr::vector<std::pmr::string>還好端端的阿?
    • 很明顯地這次的問題並不是出在initilizer_list上頭
    • 那肯定跟我們的S有關
  • 問題在於,我們的std::pmr::vector在初始化每一個內部的S物件時,並沒有順利的把mem_resource轉送給S的constructor
    • 換言之,S沒有認到mem_resource,我們可以透過對make_container做一些修改來佐證我們的推論
      1
      2
      3
      4
      5
      6
      7
      8
      template <typename Container, typename... Values>
      auto create_container(auto *resource, Values&&... values) {
      Container result{resource};
      result.reserve(sizeof...(values));
      (result.emplace_back(std::forward<Values>(values), resource), ...);
      // ^ here
      return result;
      }
    • 在修改後,我們就可以看到原本被print_alloc捕捉到的alloc & dealloc行為消失了,所以的確問題就是S沒有成功的認到vector內的mem_resource
    • 那為何std::pmr::string就可以在原本的make_container下被認到?
  • 這...說來話長
    • 簡單的來說,任何帶有using allocator_type的class,在allocation的過程中(uses_allocator_args.h::uses_allocator_construction_args()這一步)uses_allocator_v這個compile time check會是true
    • 後面還有一個is_constructible_v檢查在確保給定的args真的可以建構出class
      • 一定要通過這些檢查,我們給的allocator才會真的被拿起來用
    • 否則就會被忽略,改用default_constructor
    • 所以我們的目標就是要讓S上也有using allocator_type
  • OK,事不宜遲,我們來看影片中改過的版本長怎樣
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    struct S {
    std::pmr::string str;

    using allocator_type = std::pmr::polymorphic_allocator<>;

    // default constructor, delegate to aa constructor
    S(const char* sstr) : S(sstr, allocator_type{}) {}

    explicit S(const char* sstr, allocator_type alloc)
    : str(sstr, alloc)
    {
    }

    S(const S &other, allocator_type alloc = {})
    : str(other.str, alloc)
    {
    }

    S(S &&) = default;

    S(S &&other, allocator_type alloc)
    : str(std::move(other.str), alloc)
    {}

    S &operator=(const S &rhs) = default;
    S &operator=(S &&rhs) = default;

    ~S() = default;

    allocator_type get_allocator() const {
    return str.get_allocator();
    }
    };
  • 我們總共做了以下幾點修改
    • 補上using allocator_type
    • 根據rule of five,把該給的constructor跟destructor寫出來
  • Cool, 完整程式碼在此,來看看輸出
    1
    2
    3
    4
    initializing vec<S>
    A really long string here
    Another really long string here
    exiting main
  • 現在所有的pmr::string都有乖乖地在local buffer上做allocation了
    • 到這裡,我們就正式的完成了Allocator-Aware Types的建構

小結

  • 我們在本篇文章中探討了Allocator-Aware Types的建構方式,並且實際利用一個class S來實驗結果是否符合預期
  • 我們還探討了polymorphic_allocator與傳統allocator的不同,以及他們各自的優缺點
  • 我們也簡單的研究了一下using allocator_type背後的機轉為何
  • 下一篇文章我們就會正式開始探討不同的記憶體管理策略

有的時候我都在想,搞那麼複雜幹嘛呢...