前の項目はデータ構造が少し難しめだったので、この項目では STL コンテナをフル活用してコードを読みやすくすることと、バッファーオーバーフロー時にエラーを投げるのではなく、ヒープへのフォールバックを実装してみたいと思います。
ヒープフォールバック付きのスタックアロケーターの実装はヒープをすこしばかり改造するというアプローチで良いかと思います。
まず最大の違いはヒープのように delete は不要なところです。
それとスタックにはすでにメモリー領域が固定長ではありますが確保されてるので new も不要です。
しかし使用できるアドレスの範囲内にオブジェクトを作るためにプレースメント new を使うことになります。
ではコードを見てみましょう。
main.cpp.
1 #include <memory> 2 #include <vector> 3 #include <iostream> 4 #include <cassert> 5 #include <cstddef> 6 #include <map> 7 8 template<typename T, std::size_t N, typename Allocator = std::allocator<T>> 9 class stack_allocator 10 { 11 public: 12 typedef typename std::allocator_traits<Allocator>::value_type value_type; 13 typedef typename std::allocator_traits<Allocator>::pointer pointer; 14 typedef typename std::allocator_traits<Allocator>::const_pointer const_pointer; 15 typedef typename std::allocator_traits<Allocator>::size_type size_type; 16 typedef typename std::allocator_traits<Allocator>::difference_type difference_type; 17 typedef typename std::allocator_traits<Allocator>::void_pointer void_pointer; 18 typedef typename std::allocator_traits<Allocator>::const_void_pointer const_void_pointer; 19 typedef typename Allocator::reference reference; 20 typedef typename Allocator::const_reference const_reference; 21 22 typedef std::map<void*,size_t> size_map_t; 23 typedef std::map<size_t,std::vector<void*>> free_list_t; 24 typedef std::byte byte; 25 26 typedef Allocator allocator_type; 27 28 explicit stack_allocator(const allocator_type& alloc = allocator_type()) 29 : M_allocator_(alloc), 30 M_block_size_(sizeof(T) < 8 ? sizeof(long long) : sizeof(T)), 31 M_max_objects_(N), 32 M_begin_(M_buffer_), 33 M_end_(M_buffer_+M_block_size_*M_max_objects_), 34 M_stack_pointer_(M_buffer_) 35 {} 36 37 template<typename U> 38 stack_allocator(const stack_allocator<U,N,Allocator>& other) 39 : M_allocator_(other.M_allocator_), 40 M_block_size_(sizeof(T) < 8 ? sizeof(long long) : sizeof(T)), 41 M_max_objects_(N), 42 M_begin_(other.M_begin_), 43 M_end_(other.M_end_), 44 M_stack_pointer_(other.M_stack_pointer_) 45 {} 46 47 template<typename U> 48 struct rebind 49 { 50 typedef stack_allocator<U,N,allocator_type> other; 51 }; 52 53 pointer allocate(size_type n, const_void_pointer hint = const_void_pointer()) 54 { 55 auto& free_chunks = M_free_list_[n*M_block_size_]; 56 if(!free_chunks.empty()){ 57 pointer available_block = static_cast<T*>(free_chunks.back()); 58 free_chunks.pop_back(); 59 std::cout << "reuse pointer: " << available_block << '\n'; 60 return available_block; 61 } else if(M_stack_pointer_ + n * M_block_size_ > M_end_){ 62 std::printf("over the stack capacity, fall back to default allocator\n"); 63 M_stack_pointer_ += n * M_block_size_; 64 return M_allocator_.allocate(n); 65 } 66 67 pointer new_block = reinterpret_cast<pointer>(M_stack_pointer_); 68 M_stack_pointer_ += n*M_block_size_; 69 M_size_map_[new_block] = n*M_block_size_; 70 std::cout << "new pointer: " << new_block << " M_stack_pointer_: " << M_stack_pointer_ << '\n'; 71 return new_block; 72 } 73 74 void deallocate(pointer p, size_type n) 75 { 76 auto freed_chunk_size = M_size_map_[(void*)p]; 77 if(freed_chunk_size != 0){ // value should be initialized to zero if the map is empty 78 M_free_list_[freed_chunk_size].push_back((void*)p); 79 } else if(M_stack_pointer_ > M_end_){ 80 M_allocator_.deallocate(p,n); 81 freed_chunk_size = n * M_block_size_; 82 } 83 std::printf("deallocate: %p with size %lu\n",p,freed_chunk_size); 84 } 85 86 size_type max_size() const noexcept 87 { 88 return (size_type)(~0) / sizeof(T); 89 } 90 91 template<typename U, typename... Args> 92 void construct(U* p, Args&&... args) 93 { 94 ::new ((void*)p) T(args...); 95 } 96 97 template<typename U> 98 void destroy(U* p) 99 { 100 p->~T(); 101 } 102 103 pointer address(reference x) const noexcept 104 { 105 return std::addressof(x); 106 } 107 108 const_pointer address(const_reference x) const noexcept 109 { 110 return addressof(x); 111 } 112 113 pointer buffer() const noexcept 114 { 115 return M_begin_; 116 } 117 118 constexpr static size_type capacity() 119 { 120 return N; 121 } 122 123 private: 124 125 allocator_type M_allocator_; 126 size_map_t M_size_map_; 127 free_list_t M_free_list_; 128 129 byte M_buffer_[N]; 130 const size_type M_block_size_; 131 const size_type M_max_objects_; 132 byte* M_begin_; 133 byte* M_end_; 134 byte* M_stack_pointer_; 135 }; 136 137 template<typename T1, std::size_t N, typename Allocator, typename T2> 138 bool operator==(const stack_allocator<T1,N,Allocator>& lhs, const stack_allocator<T2,N,Allocator>& rhs) noexcept 139 { 140 return lhs.buffer() == rhs.buffer(); 141 } 142 143 template<typename T1, std::size_t N, typename Allocator, typename T2> 144 bool operator!=(const stack_allocator<T1,N,Allocator>& lhs, const stack_allocator<T2,N,Allocator>& rhs) noexcept 145 { 146 return !(lhs == rhs); 147 } 148 149 const std::size_t stack_size = 8; 150 151 int main() 152 { 153 typedef stack_allocator<int,stack_size> alloc_t; 154 155 alloc_t alloc; 156 int* t = alloc.allocate(1); 157 alloc.construct(t,100); 158 std::cout << *t << '\n'; 159 alloc.destroy(t); 160 alloc.deallocate(t,1); 161 162 int* d[16]; 163 for(int i = 0; i < 5; ++i){ 164 d[i] = alloc.allocate(1); 165 } 166 for(int i = 1; i < 5; ++i){ 167 alloc.deallocate(d[i],1); 168 } 169 for(int i = 5; i < 16; ++i){ 170 d[i] = alloc.allocate(1); 171 } 172 for(int i = 5; i < 16; ++i){ 173 alloc.deallocate(d[i],1); 174 } 175 }
このソースコードをビルドして実行すると以下のような出力となります。
$ g++ main.cpp -std=c++17 -g $ ./a.out new pointer: 0x7ffe83fa1e08 M_stack_pointer_: 0x7ffe83fa1e10 100 deallocate: 0x7ffe83fa1e08 with size 8 reuse pointer: 0x7ffe83fa1e08 new pointer: 0x7ffe83fa1e10 M_stack_pointer_: 0x7ffe83fa1e18 new pointer: 0x7ffe83fa1e18 M_stack_pointer_: 0x7ffe83fa1e20 new pointer: 0x7ffe83fa1e20 M_stack_pointer_: 0x7ffe83fa1e28 new pointer: 0x7ffe83fa1e28 M_stack_pointer_: 0x7ffe83fa1e30 deallocate: 0x7ffe83fa1e10 with size 8 deallocate: 0x7ffe83fa1e18 with size 8 deallocate: 0x7ffe83fa1e20 with size 8 deallocate: 0x7ffe83fa1e28 with size 8 reuse pointer: 0x7ffe83fa1e28 reuse pointer: 0x7ffe83fa1e20 reuse pointer: 0x7ffe83fa1e18 reuse pointer: 0x7ffe83fa1e10 new pointer: 0x7ffe83fa1e30 M_stack_pointer_: 0x7ffe83fa1e38 new pointer: 0x7ffe83fa1e38 M_stack_pointer_: 0x7ffe83fa1e40 new pointer: 0x7ffe83fa1e40 M_stack_pointer_: 0x7ffe83fa1e48 over the stack capacity, fall back to default allocator over the stack capacity, fall back to default allocator over the stack capacity, fall back to default allocator over the stack capacity, fall back to default allocator deallocate: 0x7ffe83fa1e28 with size 8 deallocate: 0x7ffe83fa1e20 with size 8 deallocate: 0x7ffe83fa1e18 with size 8 deallocate: 0x7ffe83fa1e10 with size 8 deallocate: 0x7ffe83fa1e30 with size 8 deallocate: 0x7ffe83fa1e38 with size 8 deallocate: 0x7ffe83fa1e40 with size 8 deallocate: 0x55f7af9cf430 with size 8 deallocate: 0x55f7af9cf310 with size 8 deallocate: 0x55f7af9cf540 with size 8 deallocate: 0x55f7af9cf560 with size 8
このコードでは allocate() / deallocate() を使うケースと std::vector を使うケースの 2 パターンをテストしています。
まずはコンストラクターです。
8 template<typename T, std::size_t N, typename Allocator = std::allocator<T>> 9 class stack_allocator 10 { 11 public: // 中略 22 typedef std::map<void*,size_t> size_map_t; 23 typedef std::map<size_t,std::vector<void*>> free_list_t; 24 typedef std::byte byte; // 中略 28 explicit stack_allocator(const allocator_type& alloc = allocator_type()) 29 : M_allocator_(alloc), 30 M_block_size_(sizeof(T) < 8 ? sizeof(long long) : sizeof(T)), 31 M_max_objects_(N), 32 M_begin_(M_buffer_), 33 M_end_(M_buffer_+M_block_size_*M_max_objects_), 34 M_stack_pointer_(M_buffer_) 35 {} 36 37 template<typename U> 38 stack_allocator(const stack_allocator<U,N,Allocator>& other) 39 : M_allocator_(other.M_allocator_), 40 M_block_size_(sizeof(T) < 8 ? sizeof(long long) : sizeof(T)), 41 M_max_objects_(N), 42 M_begin_(other.M_begin_), 43 M_end_(other.M_end_), 44 M_stack_pointer_(other.M_stack_pointer_) 45 {} // 中略 123 private: 124 125 allocator_type M_allocator_; 126 size_map_t M_size_map_; 127 free_list_t M_free_list_; 128 129 byte M_buffer_[N]; 130 const size_type M_block_size_; 131 const size_type M_max_objects_; 132 byte* M_begin_; 133 byte* M_end_; 134 byte* M_stack_pointer_; 135 };
コピーコンストラクターが std::allocator のインスタンスを M_allocator_ にデフォルトでコピーしてます。
M_allocator_ はこのソースコードでフォールバックとして使用しているので無視してもらっても結構ですが、デフォルトアロケーターとして持っておくと便利です。
フォールバック用のデフォルトアロケーターがあると便利なのは、スタックアロケーターでは一定限度以上の割り当てが必要な場合はヒープ領域に割り当てる実装が一般的だからです。
テンプレートパラメーター N はバッファーのバイトサイズを定義するために使われます。
つまり M_buffer_[] 配列は N バイトの記憶域となります。
M_block_size_ はこのアロケーターが割り当てる固定サイズです。
M_begin_ と M_end_ は開始と終端のアドレスです。
M_stack_pointer_ はスタックポインター、つまり現在使用中の最新の位置ってことで、これを割り当て時に加算し、開放時に減算します。
アロケーターのメモリー管理機能は 2 つの型によって行います。
22 typedef std::map<void*,size_t> size_map_t; 23 typedef std::map<size_t,std::vector<void*>> free_list_t;
オブジェクト定義のほうは以下の 2 行です。
126 size_map_t M_size_map_; 127 free_list_t M_free_list_;
M_size_map_ はポインターから、割り当てられたバイトサイズを探すためのマップデータ構造です。
M_free_list_ は割り当てバイトサイズから、フリーリストの std::vector データ構造を探すためのマップです。
この 2 つはメモリー再利用の機能を実装するには不可欠となるのですが、考え方としてはメモリーの
まあ複雑なので実装をみたほうが早いかもしれないです。
次に allocate() 関数を見てみましょう。
53 pointer allocate(size_type n, const_void_pointer hint = const_void_pointer()) 54 { 55 auto& free_chunks = M_free_list_[n*M_block_size_]; 56 if(!free_chunks.empty()){ 57 pointer available_block = static_cast<T*>(free_chunks.back()); 58 free_chunks.pop_back(); 59 std::cout << "reuse pointer: " << available_block << '\n'; 60 return available_block; 61 } else if(M_stack_pointer_ + n * M_block_size_ > M_end_){ 62 std::printf("over the stack capacity, fall back to default allocator\n"); 63 M_stack_pointer_ += n * M_block_size_; 64 return M_allocator_.allocate(n); 65 } 66 67 pointer new_block = reinterpret_cast<pointer>(M_stack_pointer_); 68 M_stack_pointer_ += n*M_block_size_; 69 M_size_map_[new_block] = n*M_block_size_; 70 std::cout << "new pointer: " << new_block << " M_stack_pointer_: " << M_stack_pointer_ << '\n'; 71 return new_block; 72 }
スタックポインターのアドレスを new_block に代入し返しますが、その直前に M_stack_pointer_ のアドレスを n * M_block_size_ 分だけ加算して、次の割り当てに使うアドレスを取得しておきます。
67 pointer new_block = reinterpret_cast<pointer>(M_stack_pointer_); 68 M_stack_pointer_ += n*M_block_size_; 69 M_size_map_[new_block] = n*M_block_size_;
それと終端アドレス超えた場合のセーフガードとしてデフォルトアロケーター( std::allocator )がコールされるようしています。
61 } else if(M_stack_pointer_ + n * M_block_size_ > M_end_){ 62 std::printf("over the stack capacity, fall back to default allocator\n"); 63 M_stack_pointer_ += n * M_block_size_; 64 return M_allocator_.allocate(n); 65 }
後は解放されてフリーな状態のチャンク・ブロックを再利用するロジックです。
55 auto& free_chunks = M_free_list_[n*M_block_size_]; 56 if(!free_chunks.empty()){ 57 pointer available_block = static_cast<T*>(free_chunks.back()); 58 free_chunks.pop_back(); 59 std::cout << "reuse pointer: " << available_block << '\n'; 60 return available_block;
ここでは、リクエストサイズのフリーチャンクが M_free_list_ に存在するかチェックしています。
55 auto& free_chunks = M_free_list_[n*M_block_size_]; 56 if(!free_chunks.empty()){
もし empty() が真を返すならば再利用できるフリーチャンクは存在せず、偽を返すならば再利用できるフリーチャンク・フリーブロックが存在するということです。
もしフリーチャンクがあるなら、後はそれを返すだけですが、取り出しあとにポップしておきます。
57 pointer available_block = static_cast<T*>(free_chunks.back()); 58 free_chunks.pop_back();
次に deallocate() を見てみましょう。
74 void deallocate(pointer p, size_type n) 75 { 76 auto freed_chunk_size = M_size_map_[(void*)p]; 77 if(freed_chunk_size != 0){ // value should be initialized to zero if the map is empty 78 M_free_list_[freed_chunk_size].push_back((void*)p); 79 } else if(M_stack_pointer_ > M_end_){ 80 M_allocator_.deallocate(p,n); 81 freed_chunk_size = n * M_block_size_; 82 } 83 std::printf("deallocate: %p with size %lu\n",p,freed_chunk_size); 84 }
アロケーターではスタックポインターにブロックサイズを加算した状態なので、アロケーターのオーバーフローのチェックは以下の行で行います。
79 } else if(M_stack_pointer_ > M_end_){ 80 M_allocator_.deallocate(p,n); 81 freed_chunk_size = n * M_block_size_; 82 }
このようにデフォルトアロケーター M_allocator_ の deallocate() 関数をコールするだけです。
次にスタック領域内のチャンクについてです。
スタック領域のチャンクは M_size_map_ に登録されています。もとは allocate() 関数で追加されています。
M_size_map_ から p ポインターを照合する場合、返ってきた値が 0 ならデータが存在しないということになります。
76 auto freed_chunk_size = M_size_map_[(void*)p]; 77 if(freed_chunk_size != 0){ // value should be initialized to zero if the map is empty 78 M_free_list_[freed_chunk_size].push_back((void*)p);
該当するポインターが存在する場合はフリーチャンクとしてフリーリストに登録を行います。
それとテストのほうもみておきましょうかね。
まずスタックのサイズは 8 としておきますが、これは 8 バイトではなく、ブロックサイズのことです。
149 const std::size_t stack_size = 8; 150 151 int main() 152 { 153 typedef stack_allocator<int,stack_size> alloc_t; 154 155 alloc_t alloc; 156 int* t = alloc.allocate(1); 157 alloc.construct(t,100); 158 std::cout << *t << '\n'; 159 alloc.destroy(t); 160 alloc.deallocate(t,1); 161 162 int* d[16]; 163 for(int i = 0; i < 5; ++i){ 164 d[i] = alloc.allocate(1); 165 } 166 for(int i = 1; i < 5; ++i){ 167 alloc.deallocate(d[i],1); 168 } 169 for(int i = 5; i < 16; ++i){ 170 d[i] = alloc.allocate(1); 171 } 172 for(int i = 5; i < 16; ++i){ 173 alloc.deallocate(d[i],1); 174 }
最初は int 型を指す領域を 1 個だけ割り当て、すぎに解放しておきます。
155 alloc_t alloc; 156 int* t = alloc.allocate(1); 157 alloc.construct(t,100); 158 std::cout << *t << '\n'; 159 alloc.destroy(t); 160 alloc.deallocate(t,1);
一応 construct() でオブジェクトを初期化し destroy() で破壊もしておいています。
次に解放したポインター再利用のロジックの実装が動作するかチェックしますね。
やってる事は i が 0,1,2,3,4 (合計 5 個)の時に allocate() 関数を呼びます。
reuse pointer: 0x7ffe83fa1e08 new pointer: 0x7ffe83fa1e10 M_stack_pointer_: 0x7ffe83fa1e18 new pointer: 0x7ffe83fa1e18 M_stack_pointer_: 0x7ffe83fa1e20 new pointer: 0x7ffe83fa1e20 M_stack_pointer_: 0x7ffe83fa1e28 new pointer: 0x7ffe83fa1e28 M_stack_pointer_: 0x7ffe83fa1e30
さらに i が 1,2,3,4 (合計 4 個)の時に deallocate() 関数をコールします。
deallocate: 0x7ffe83fa1e10 with size 8 deallocate: 0x7ffe83fa1e18 with size 8 deallocate: 0x7ffe83fa1e20 with size 8 deallocate: 0x7ffe83fa1e28 with size 8
そして i が 5,6,7,8,9,10,11,12,13,14,15 (合計 11 個)の時に allocate() 関数と呼びます。
reuse pointer: 0x7ffe83fa1e28 reuse pointer: 0x7ffe83fa1e20 reuse pointer: 0x7ffe83fa1e18 reuse pointer: 0x7ffe83fa1e10 new pointer: 0x7ffe83fa1e30 M_stack_pointer_: 0x7ffe83fa1e38 new pointer: 0x7ffe83fa1e38 M_stack_pointer_: 0x7ffe83fa1e40 new pointer: 0x7ffe83fa1e40 M_stack_pointer_: 0x7ffe83fa1e48 over the stack capacity, fall back to default allocator over the stack capacity, fall back to default allocator over the stack capacity, fall back to default allocator over the stack capacity, fall back to default allocator
この際に前もって deallocate() でフリーチャンクが 4 個分 M_free_list_ に登録されているので、その分を再利用します。
すると 7 個分までがスタック領域に割り当てられ、残りの 4 個分がヒープ領域に割り当てられます。
最後に最初に作った分と合わせ合計 12 個分のポインターに対して deallocate() 関数をコールします。
deallocate: 0x7ffe83fa1e28 with size 8 deallocate: 0x7ffe83fa1e20 with size 8 deallocate: 0x7ffe83fa1e18 with size 8 deallocate: 0x7ffe83fa1e10 with size 8 deallocate: 0x7ffe83fa1e30 with size 8 deallocate: 0x7ffe83fa1e38 with size 8 deallocate: 0x7ffe83fa1e40 with size 8 deallocate: 0x55f7af9cf430 with size 8 deallocate: 0x55f7af9cf310 with size 8 deallocate: 0x55f7af9cf540 with size 8 deallocate: 0x55f7af9cf560 with size 8
スタック領域のポインターらしきものが 8 個、ヒープ領域のポインターらしきものが 4 個解放されています。
Copyright 2018-2019, by Masaki Komatsu