第93章 カスタムヒープアロケーター

 malloc() を使って大きめの記憶域を先に割り当てるアロケーターは、前の項目で可能な事が判明したので、早速アロケーターを設計してみましょうかね。

 まず割り当てには malloc() や new を使わない点においてスタックアロケーターとは何も変わらないです。

 さらに言えばスタックアロケーターと allocate() 関数の実装については特に変更は必要ないです。

 では実装してみましょう。

main.cpp. 

  1 #include <iostream>
  2 #include <cassert>
  3 #include <cstddef>
  4 #include <cstdint>
  5 #include <new>
  6 #include <map>
  7 #include <vector>
  8
  9 #define MAX_OBJECTS 256
 10
 11 struct Foo {
 12   int x_,y_,z_;
 13   Foo(int x, int y, int z) :
 14     x_(x),y_(y),z_(z){}
 15 };
 16
 17 template<typename T>
 18 struct A {
 19   typedef T* pointer;
 20   typedef std::size_t size_type;
 21
 22   const size_t M_object_size_;
 23   unsigned M_count_;
 24   char* M_heap_;
 25
 26   using size_map = std::map<void*,size_type>;
 27   using free_map = std::map<size_type,std::vector<void*>>;
 28
 29   A() : M_count_(0), M_object_size_(sizeof(T) < 8 ? sizeof(long long) : sizeof(T))
 30   {
 31     M_heap_ = (char*)std::malloc(MAX_OBJECTS*M_object_size_);
 32   }
 33
 34   ~A(){
 35     std::free(M_heap_);
 36     M_heap_ = nullptr;
 37   }
 38
 39   pointer allocate(size_type n = 1) {
 40     assert(M_count_ <= MAX_OBJECTS);
 41     auto& free_chunks = free_map_[n*M_object_size_];
 42     if(!free_chunks.empty()){
 43       T* available_block = static_cast<T*>(free_chunks.back());
 44       free_chunks.pop_back();
 45       std::cout << "reuse pointer: " << available_block << " M_count_: " << M_count_ << '\n';
 46       return available_block;
 47     }
 48     M_count_ += n;
 49     void* pblock = static_cast<void*>(M_heap_ + (M_object_size_ * M_count_));
 50     size_map_[pblock] = n * M_object_size_;
 51     std::cout << "new pointer: " << pblock << " M_count_: " << M_count_ << '\n';
 52     return reinterpret_cast<T*>(::new (pblock) char(n*M_object_size_));
 53   }
 54
 55   void deallocate(pointer p) {
 56     auto no_longer_in_use_chunks = size_map_[(void*)p];
 57     free_map_[no_longer_in_use_chunks].push_back((void*)p);
 58     std::printf("deallocate %p with size %lu\n",p,no_longer_in_use_chunks);
 59   }
 60
 61   template<typename U, typename... Args>
 62   void construct(U* p, Args&& ...args) {
 63     ::new ((void*)p) U(std::forward<Args>(args)...);
 64   }
 65
 66   template<typename U>
 67   void destroy(U* p) {
 68     p->~U();
 69   }
 70 private:
 71   size_map size_map_;
 72   free_map free_map_;
 73 };
 74
 75 int main()
 76 {
 77   A<int> a;
 78   int* d[10];
 79   A<Foo> f;
 80   Foo* foo[10];
 81   for(int i = 0; i < 5; ++i) {
 82     auto size = i % 2 + 1;
 83     d[i] = a.allocate(size);
 84     foo[i] = f.allocate(size);
 85     a.construct(d[i],i);
 86     f.construct(foo[i],i,i,i);
 87   }
 88   for(int i = 2; i < 5; ++i){
 89     a.deallocate(d[i]);
 90     f.deallocate(foo[i]);
 91   }
 92   for(int i = 5; i < 10; ++i){
 93     d[i] = a.allocate(2);
 94     foo[i] = f.allocate(2);
 95   }
 96   return 0;
 97 }

ビルドと実行結果. 

$ g++ main.cpp
$ ./a.out
new pointer: 0x56342fb05e78 M_count_: 1
new pointer: 0x56342fb0668c M_count_: 1
new pointer: 0x56342fb05e88 M_count_: 3
new pointer: 0x56342fb066a4 M_count_: 3
new pointer: 0x56342fb05e90 M_count_: 4
new pointer: 0x56342fb066b0 M_count_: 4
new pointer: 0x56342fb05ea0 M_count_: 6
new pointer: 0x56342fb066c8 M_count_: 6
new pointer: 0x56342fb05ea8 M_count_: 7
new pointer: 0x56342fb066d4 M_count_: 7
deallocate 0x56342fb05e90 with size 8
deallocate 0x56342fb066b0 with size 12
deallocate 0x56342fb05ea0 with size 16
deallocate 0x56342fb066c8 with size 24
deallocate 0x56342fb05ea8 with size 8
deallocate 0x56342fb066d4 with size 12
reuse pointer: 0x56342fb05ea0 M_count_: 7
reuse pointer: 0x56342fb066c8 M_count_: 7
new pointer: 0x56342fb05eb8 M_count_: 9
new pointer: 0x56342fb066ec M_count_: 9
new pointer: 0x56342fb05ec8 M_count_: 11
new pointer: 0x56342fb06704 M_count_: 11
new pointer: 0x56342fb05ed8 M_count_: 13
new pointer: 0x56342fb0671c M_count_: 13
new pointer: 0x56342fb05ee8 M_count_: 15
new pointer: 0x56342fb06734 M_count_: 15

 アロケーターの状態には、オブジェクト数をキープする M_count_ 変数と、オブジェクトサイズを保持する M_object_size_ があります。

 17 template<typename T>
 18 struct A {
 19   typedef T* pointer;
 20   typedef std::size_t size_type;
 21
 22   const size_t M_object_size_;
 23   unsigned M_count_;
 24   char* M_heap_;
 25
 26   using size_map = std::map<void*,size_type>;
 27   using free_map = std::map<size_type,std::vector<void*>>;

//中略

 70 private:
 71   size_map size_map_;
 72   free_map free_map_;
 73 };

 M_heap_ はヒープで割り当てる記憶域ですが、ヒープはコンストラクターを呼び出す一回に限り malloc() で割り当てます。

 29   A() : M_count_(0), M_object_size_(sizeof(T) < 8 ? sizeof(long long) : sizeof(T))
 30   {
 31     M_heap_ = (char*)std::malloc(MAX_OBJECTS*M_object_size_);
 32   }
 33
 34   ~A(){
 35     std::free(M_heap_);
 36     M_heap_ = nullptr;
 37   }

 オブジェクトサイズについては、テンプレートパラメーター T のバイトサイズが 8 を下回る場合は強制的に 8 に設定します。

 long long は 8 バイトになるはずなので、そのバイトサイズを設定するか、T のバイトサイズをオブジェクトサイズとします。

 後はコンストラクター時に M_heap_ に割り当てたヒープ領域のメモリーはデストラクタ―で解放します。

 allocate() については M_heap_ のアドレスを起点として新たなアドレスを割り当てたり、解放していきます。

 39   pointer allocate(size_type n = 1) {
 40     assert(M_count_ <= MAX_OBJECTS);
 41     auto& free_chunks = free_map_[n*M_object_size_];
 42     if(!free_chunks.empty()){
 43       T* available_block = static_cast<T*>(free_chunks.back());
 44       free_chunks.pop_back();
 45       std::cout << "reuse pointer: " << available_block << " M_count_: " << M_count_ << '\n';
 46       return available_block;
 47     }
 48     M_count_ += n;
 49     void* pblock = static_cast<void*>(M_heap_ + (M_object_size_ * M_count_));
 50     size_map_[pblock] = n * M_object_size_;
 51     std::cout << "new pointer: " << pblock << " M_count_: " << M_count_ << '\n';
 52     return reinterpret_cast<T*>(::new (pblock) char(n*M_object_size_));
 53   }

 まず最初にフリーリスト内にチャンクがないかチェックします。

 41     auto& free_chunks = free_map_[n*M_object_size_];
 42     if(!free_chunks.empty()){

 allocate() の引数である n は要求されている要素数なので、アロケーターへのリクエストサイズは n*M_object_size_ となりますね。

 もしチャンクが空であれば、そのまま新たなアドレスを割り振ります。

 48     M_count_ += n;
 49     void* pblock = static_cast<void*>(M_heap_ + (M_object_size_ * M_count_));
 50     size_map_[pblock] = n * M_object_size_;
 51     std::cout << "new pointer: " << pblock << " M_count_: " << M_count_ << '\n';
 52     return reinterpret_cast<T*>(::new (pblock) char(n*M_object_size_));
 53   }

 M_count_ はアロケーターの新規割り当てに使ったブロックの合計なので、それにブロックサイズである M_object_size_ をかけたものを M_heap_ に加算すれば、新規割り当てに使えるアドレスを算出できます。

 仮にフリーリスト(フリーチャンクのリスト)が空でない場合は、リクエストサイズと同じサイズのリストを検索します。

 42     if(!free_chunks.empty()){
 43       T* available_block = static_cast<T*>(free_chunks.back());
 44       free_chunks.pop_back();
 45       std::cout << "reuse pointer: " << available_block << " M_count_: " << M_count_ << '\n';
 46       return available_block;
 47     }

 検索にマッチするサイズのフリーチャンクがあれば、それを再利用して返します。

 後は deallocate() も軽く説明しておきますね。

 55   void deallocate(pointer p) {
 56     auto no_longer_in_use_chunks = size_map_[(void*)p];
 57     free_map_[no_longer_in_use_chunks].push_back((void*)p);
 58     std::printf("deallocate %p with size %lu\n",p,no_longer_in_use_chunks);
 59   }

 不要になり解放したいポインター p については size_map_ を使って、割り当て時にリクエストされたサイズを照合します。

 照合されたサイズをキーにして、フリーリスト(フリーチャンクのリスト)にポインターを追加します。

 それとアロケーターのテストについては int 型と Foo 型の 2 パターンを用意しました。

 int 型は 4 バイトで Foo 型は 12 バイトのオブジェクトとしてヒープに割り当てをしますが、リクエストサイズは 8 バイトか 16 バイト、12 バイトか 24 バイトのいずれかのパターンとします。

 77   A<int> a;
 78   int* d[10];
 79   A<Foo> f;
 80   Foo* foo[10];

 後は for ループを回してるだけですが、5 回割り当てをし、3 回解放、5 回割り当てというパターンを試すことで、適切な解放領域が再利用されるかチェックしてみます。

 81   for(int i = 0; i < 5; ++i) {
 82     auto size = i % 2 + 1;
 83     d[i] = a.allocate(size);
 84     foo[i] = f.allocate(size);
 85     a.construct(d[i],i);
 86     f.construct(foo[i],i,i,i);
 87   }
 88   for(int i = 2; i < 5; ++i){
 89     a.deallocate(d[i]);
 90     f.deallocate(foo[i]);
 91   }
 92   for(int i = 5; i < 10; ++i){
 93     d[i] = a.allocate(2);
 94     foo[i] = f.allocate(2);
 95   }

 出力結果については以下の部分で再利用が確認されています。

deallocate 0x56342fb05e90 with size 8
deallocate 0x56342fb066b0 with size 12
deallocate 0x56342fb05ea0 with size 16
deallocate 0x56342fb066c8 with size 24
deallocate 0x56342fb05ea8 with size 8
deallocate 0x56342fb066d4 with size 12
reuse pointer: 0x56342fb05ea0 M_count_: 7
reuse pointer: 0x56342fb066c8 M_count_: 7

 つまり解放は各アロケーターに大して 3 回づつ行われてますが、実際に再利用されたのは 16 バイト( 0x56342fb05ea0 ) と 24 バイト( 0x56342fb066c8 )のチャンクだけです。

 93     d[i] = a.allocate(2);
 94     foo[i] = f.allocate(2);

 各 allocate() がリクエストするサイズが 16 バイトと 24 バイトだからです。

Copyright 2018-2019, by Masaki Komatsu