std::allocator によるメモリ管理

標準テンプレート ライブラリのすべてのコンテナに共通するものは何ですか?これらには、デフォルトで std::allocator である型パラメーター Allocator があります。アロケーターの仕事は、その要素の寿命を管理することです。これは、その要素のメモリを割り当ておよび割り当て解除し、それらを初期化および破棄することを意味します。

この投稿では、標準テンプレート ライブラリのコンテナーについて書いていますが、これには std::string が含まれます。簡単にするために、コンテナという用語を両方に使用します。

std::allocator の何が特別なのですか?

一方では、std::allocator が std::vector または std::map のペアに要素を割り当てる場合、違いが生じます。

template<
 class T,
 class Allocator = std::allocator<T>
> class vector;


template<
 class Key,
 class T,
 class Compare = std::less<Key>,
 class Allocator = std::allocator<std::pair<const Key, T> >
> class map;

一方、アロケーターは、その仕事を行うために一連の属性、メソッド、および関数を必要とします。

インターフェース

 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
// Attributes
value_type T
pointer T*
const_pointer const T*
reference T&
const_reference const T&
size_type std::size_t
difference_type std::ptrdiff_t
propagate_on_container_move_assignment std::true_ty
rebind template< class U > struct rebind { typedef allocator<U> other; };
is_always_equal std::true_type

// Methods
constructor
destructor
address
allocate
deallocate
max_size
construct
destroy

// Functions
operator==
operator!=

つまり、std::allocator の最も重要なメンバーは次のとおりです。

内部クラス テンプレートの rebind (10 行目) は、これらの重要なメンバーの 1 つです。クラス テンプレートのおかげで、T 型の std::allocator を U 型に再バインドできます。std::allocate の中心は、allocate (17 行目) と deallocate (18 行目) の 2 つのメソッドです。どちらのメソッドも、オブジェクトがコンストラクト (20 行目) で初期化され、destroy (21 行目) で破棄されるメモリを管理します。メソッド max_size (19 行目) は、std::allocate がメモリを割り当てることができる型 T のオブジェクトの最大数を返します。

もちろん、std::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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// allocate.cpp

#include <memory>
#include <iostream>
#include <string>
 
int main(){
 
 std::cout << std::endl;

 std::allocator<int> intAlloc; 

 std::cout << "intAlloc.max_size(): " << intAlloc.max_size() << std::endl;
 int* intArray = intAlloc.allocate(100);

 std::cout << "intArray[4]: " << intArray[4] << std::endl;
 
 intArray[4] = 2011;

 std::cout << "intArray[4]: " << intArray[4] << std::endl;
 
 intAlloc.deallocate(intArray, 100);

 std::cout << std::endl;
 
 std::allocator<double> doubleAlloc;
 std::cout << "doubleAlloc.max_size(): " << doubleAlloc.max_size() << std::endl;
 
 std::cout << std::endl;

 std::allocator<std::string> stringAlloc;
 std::cout << "stringAlloc.max_size(): " << stringAlloc.max_size() << std::endl;
 
 std::string* myString = stringAlloc.allocate(3); 
 
 stringAlloc.construct(myString, "Hello");
 stringAlloc.construct(myString + 1, "World");
 stringAlloc.construct(myString + 2, "!");
 
 std::cout << myString[0] << " " << myString[1] << " " << myString[2] << std::endl;
 
 stringAlloc.destroy(myString);
 stringAlloc.destroy(myString + 1);
 stringAlloc.destroy(myString + 2);
 stringAlloc.deallocate(myString, 3);
 
 std::cout << std::endl;
 
}

プログラムで 3 つのアロケータを使用しました。 1 つは int 用 (11 行目)、もう 1 つは double 用 (26 行目)、もう 1 つは std::string 用 (31 行目) です。これらのアロケータはそれぞれ、割り当てることができる要素の最大数を認識しています (14、27、および 32 行目)。

次に、int のアロケーター:std::allocator intAlloc (11 行目) に進みます。 intAlloc を使用すると、100 要素の int 配列を割り当てることができます (14 行目)。最初に初期化する必要があるため、5 番目の要素へのアクセスは定義されていません。これは 20 行で変更されます。呼び出し intAlloc.deallocate(intArray, 100) のおかげで (22 行)、メモリの割り当てを解除します。

std::string アロケータの処理はより複雑です。 36 ~ 38 行目の stringAlloc.construct 呼び出しは、std::string の 3 つのコンストラクター呼び出しをトリガーします。 3 つの stringAlloc.destroy 呼び出し (42 行目から 44 行目) は反対のことを行います。最後 (34 行目) で myString のメモリが解放されます。

次に、プログラムの出力です。

C++17

C++17 では、std::allocator のインターフェースが非常に扱いやすくなっています。そのメンバーの多くは非推奨です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Attributes
value_type T
propagate_on_container_move_assignment std::true_ty
is_always_equal std::true_type

// Methods
constructor
destructor
allocate
deallocate

// Functions
operator==
operator!=

しかし、重要な答えは、この投稿がまだ見つからないということです。

コンテナにアロケータが必要なのはなぜですか?

答えは 3 つあります。

<オール>
  • コンテナは基礎となるメモリ モデルから独立する必要があります .たとえば、x86 アーキテクチャの Intel メモリ モデルでは、次の 6 つの異なるバリアントが使用されます。tiny、small、medium、compact、large、 そして巨大 .その点をはっきりと強調したい。マルチスレッドのベースとしてのメモリ モデルではなく、Intel メモリ モデルから話します。
  • コンテナは、メモリの割り当てと割り当て解除を、要素の初期化と破棄から分離できます .したがって、std::vector vec の vec.reserve(n) の呼び出しは、少なくとも n 要素のメモリのみを割り当てます。各要素のコンストラクターは実行されません。 (スヴェン・ヨハンセン )
  • コンテナのアロケータを必要に応じて正確に調整できます。 したがって、デフォルトのアロケータは、それほど頻繁ではないメモリ呼び出しと大きなメモリ領域向けに最適化されています。内部では、通常、C 関数 std::malloc が使用されます。したがって、事前に割り当てられたメモリを使用するアロケーターは、パフォーマンスを大幅に向上させることができます。プログラムの決定論的なタイミング動作が必要な場合、調整されたアロケーターも非常に理にかなっています。コンテナーの既定のアロケーターでは、メモリ割り当てにかかる時間が保証されません。もちろん、調整されたアロケーターを使用して、充実したデバッグ情報を提供できます。
  • 次は?

    メモリを要求するための戦略はどれですか?それが次の投稿で答えたい質問です。