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