Dans cet article, je voudrais montrer des exemples simples de travail avec des composants de l' espace de noms pmr et les idées de base sous-jacentes aux allocateurs polymorphes.
L'idée principale des allocateurs polymorphes introduits dans c ++ 17 est d'améliorer les allocateurs standards implémentés sur la base du polymorphisme statique ou en d'autres termes des modèles. Ils sont beaucoup plus faciles à utiliser que les allocateurs standard, en outre, ils vous permettent de conserver le type de conteneur lorsque vous utilisez différents allocateurs et, par conséquent, de modifier les allocateurs au moment de l'exécution.
Si vous le souhaitez
std::vector
avec un allocateur de mémoire spécifique, vous pouvez utiliser le paramètre de modèle Allocator:
auto my_vector = std::vector<int, my_allocator>();
Mais il y a un problème - ce vecteur n'est pas du même type qu'un vecteur avec un allocateur différent, y compris celui défini par défaut.
Un tel conteneur ne peut pas être passé à une fonction qui nécessite un vecteur avec un conteneur par défaut, ni deux vecteurs avec des types d'allocateurs différents être affectés à la même variable, par exemple:
auto my_vector = std::vector<int, my_allocator>();
auto my_vector2 = std::vector<int, other_allocator>();
auto vec = my_vector; // ok
vec = my_vector2; // error
Un allocateur polymorphe contient un pointeur vers une interface
memory_resource
afin qu'il puisse utiliser la répartition dynamique.
Pour changer la stratégie de travail avec la mémoire, il suffit de remplacer l'instance
memory_resource
en conservant le type d'allocateur. Cela peut également être fait au moment de l'exécution. Sinon, les allocateurs polymorphes fonctionnent selon les mêmes règles que les standards.
Les types de données spécifiques utilisés par le nouvel allocateur se trouvent dans l'espace de noms
std::pmr
. Il existe également des spécialisations de modèles de conteneurs standard qui peuvent fonctionner avec un allocateur polymorphe.
L'un des principaux problèmes pour le moment est l'incompatibilité des nouvelles versions de conteneurs
std::pmr
avec des analogues de std
.
Composants principaux std::pmr:
std::pmr::memory_resource
— , .- :
virtual void* do_allocate(std::size_t bytes, std::size_t alignment)
,virtual void do_deallocate(void* p, std::size_t bytes, std::size_t alignment)
virtual bool do_is_equal(const std::pmr::memory_resource& other) const noexcept
.
std::pmr::polymorphic_allocator
— ,memory_resource
.new_delete_resource()
null_memory_resource()
«»- :
synchronized_pool_resource
unsynchronized_pool_resource
monotonic_buffer_resource
- ,
std::pmr::vector
,std::pmr::string
,std::pmr::map
. , . -
memory_resource
:
memory_resource* new_delete_resource()
, memory_resource, new delete .memory_resource* null_memory_resource()
La fonction free renvoie un pointeur surmemory_resource
lequel lève une exceptionstd::bad_alloc
à chaque tentative d'allocation.
Cela peut être utile pour garantir que les objets n'allouent pas de mémoire sur le tas ou à des fins de test.
class synchronized_pool_resource : public std::pmr::memory_resource
Une implémentation de memory_resource à usage général et sûre pour les threads consiste en un ensemble de pools avec différentes tailles de blocs de mémoire.
Chaque pool est une collection de blocs de mémoire de même taille.class unsynchronized_pool_resource : public std::pmr::memory_resource
Version à filetage uniquesynchronized_pool_resource
.class monotonic_buffer_resource : public std::pmr::memory_resource
Mono-thread, rapide etmemory_resource
spécial prend la mémoire d'un tampon pré-alloué, mais ne la libère pas, c'est-à-dire qu'elle ne peut que croître.
Exemple d'utilisation
monotonic_buffer_resource
et pmr::vector
:
#include <iostream>
#include <memory_resource> // pmr core types
#include <vector> // pmr::vector
#include <string> // pmr::string
int main() {
char buffer[64] = {}; // a small buffer on the stack
std::fill_n(std::begin(buffer), std::size(buffer) - 1, '_');
std::cout << buffer << '\n';
std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)};
std::pmr::vector<char> vec{ &pool };
for (char ch = 'a'; ch <= 'z'; ++ch)
vec.push_back(ch);
std::cout << buffer << '\n';
}
Sortie programme:
_______________________________________________________________
aababcdabcdefghabcdefghijklmnopabcdefghijklmnopqrstuvwxyz______
Dans l'exemple ci-dessus, nous avons utilisé
monotonic_buffer_resource
, initialisé avec un tampon alloué sur la pile. En utilisant un pointeur vers ce tampon, nous pouvons facilement afficher le contenu de la mémoire.
Le vecteur prend la mémoire du pool, ce qui est très rapide, puisqu'il est sur la pile, s'il manque de mémoire, il la demande à l'aide de l'opérateur global
new
. L'exemple montre une implémentation vectorielle lorsque vous essayez d'insérer plus que le nombre réservé d'éléments. Dans ce cas, l' monotonic_buffer
ancienne mémoire n'est pas libérée, mais ne fait qu'augmenter.
Vous pouvez, bien sûr, faire appel
reserve()
à un vecteur pour minimiser les réallocations, mais le but de l'exemple est précisément de montrer comment il change à monotonic_buffer_resource
mesure que le conteneur se développe.
Espace de rangement pmr::string
Et si nous voulons stocker des chaînes
pmr::vector
?
Une caractéristique importante est que si les objets d'un conteneur utilisent également un allocateur polymorphe, ils demandent l'allocateur du conteneur parent pour la gestion de la mémoire.
Si vous souhaitez profiter de cette fonctionnalité, vous devez utiliser à la
std::pmr::string
place std::string
.
Prenons un exemple avec un tampon pré-alloué sur la pile, que nous passerons comme
memory_resource
pour std::pmr::vector std::pmr::string
:
#include <iostream>
#include <memory_resource> // pmr core types
#include <vector> // pmr::vector
#include <string> // pmr::string
int main() {
std::cout << "sizeof(std::string): " << sizeof(std::string) << '\n';
std::cout << "sizeof(std::pmr::string): " << sizeof(std::pmr::string) << '\n';
char buffer[256] = {}; // a small buffer on the stack
std::fill_n(std::begin(buffer), std::size(buffer) - 1, '_');
const auto BufferPrinter = [](std::string_view buf, std::string_view title) {
std::cout << title << ":\n";
for (auto& ch : buf) {
std::cout << (ch >= ' ' ? ch : '#');
}
std::cout << '\n';
};
BufferPrinter(buffer, "zeroed buffer");
std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)};
std::pmr::vector<std::pmr::string> vec{ &pool };
vec.reserve(5);
vec.push_back("Hello World");
vec.push_back("One Two Three");
BufferPrinter(std::string_view(buffer, std::size(buffer)), "after two short strings");
vec.emplace_back("This is a longer string");
BufferPrinter(std::string_view(buffer, std::size(buffer)), "after longer string strings");
vec.push_back("Four Five Six");
BufferPrinter(std::string_view(buffer, std::size(buffer)), "after the last string");
}
Sortie programme:
sizeof(std::string): 32
sizeof(std::pmr::string): 40
zeroed buffer:
_______________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________
after two short strings:
#m### ###n### ##########Hello World######m### ##@n### ##########One Two Three###_______________________________________________________________________________________________________________________________________________________________________________#
after longer string strings:
#m### ###n### ##########Hello World######m### ##@n### ##########One Two Three####m### ###n### ##################________________________________________________________________________________________This is a longer string#_______________________________#
after the last string:
#m### ###n### ##########Hello World######m### ##@n### ##########One Two Three####m### ###n### ##################________#m### ###n### ##########Four Five Six###________________________________________This is a longer string#_______________________________#
Les principaux points à prendre en compte dans cet exemple:
- La taille est
pmr::string
supérieure àstd::string
. Cela est dû au fait qu'un pointeur versmemory_resource
; - Nous réservons le vecteur pour 5 éléments, donc aucune réallocation ne se produit lors de l'ajout de 4.
- Les 2 premières lignes sont suffisamment courtes pour le bloc de mémoire vectorielle, donc aucune allocation de mémoire supplémentaire ne se produit.
- La troisième ligne est plus longue et nécessite un bloc de mémoire séparé dans notre tampon, et seul le pointeur vers ce bloc est stocké dans le vecteur.
- Comme vous pouvez le voir dans la sortie, «Ceci est une chaîne plus longue» est situé presque à la toute fin du tampon.
- Lorsque nous insérons une autre chaîne courte, elle retombe dans le bloc mémoire du vecteur
À titre de comparaison, faisons la même expérience avec
std::string
au lieu destd::pmr::string
sizeof(std::string): 32
sizeof(std::pmr::string): 40
zeroed buffer:
_______________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________
after two short strings:
###w# ##########Hello World########w# ##########One Two Three###_______________________________________________________________________________________________________________________________________________________________________________________________#
new 24
after longer string strings:
###w# ##########Hello World########w# ##########One Two Three###0#######################_______________________________________________________________________________________________________________________________________________________________________#
after the last string:
###w# ##########Hello World########w# ##########One Two Three###0#######################________@##w# ##########Four Five Six###_______________________________________________________________________________________________________________________________#
Cette fois, les éléments du conteneur occupent moins d'espace car il n'est pas nécessaire de stocker un pointeur vers la source_mémoire.
Les chaînes courtes sont toujours stockées dans le bloc de mémoire vectorielle, mais maintenant la longue chaîne ne fait pas partie de notre tampon. Cette fois, une longue chaîne est allouée à l'aide de l'allocateur par défaut et un
pointeur vers celui-ci est placé dans le bloc de mémoire vectorielle . Par conséquent, nous ne voyons pas cette ligne dans la sortie.
Encore une fois sur l'expansion vectorielle:
Il a été mentionné que lorsque la mémoire du pool est épuisée, l'allocateur le demande à l'aide de l'opérateur
new()
.
En fait, ce n'est pas tout à fait vrai - la mémoire est demandée à
memory_resource
, retournée à l'aide d'une fonction libre
std::pmr::memory_resource* get_default_resource()
Par défaut, cette fonction retourne
std::pmr::new_delete_resource()
, qui à son tour alloue de la mémoire à l'aide d'un opérateur new()
, mais peut être remplacée à l'aide d'une fonction
std::pmr::memory_resource* set_default_resource(std::pmr::memory_resource* r)
Alors, regardons un exemple quand elle
get_default_resource
renvoie une valeur par défaut.
Il convient de garder à l'esprit que les méthodes
do_allocate()
et do_deallocate()
utilisent l'argument "alignement", nous avons donc besoin de la version C ++ 17 new()
avec le support de l'alignement:
void* lastAllocatedPtr = nullptr;
size_t lastSize = 0;
void* operator new(std::size_t size, std::align_val_t align) {
#if defined(_WIN32) || defined(__CYGWIN__)
auto ptr = _aligned_malloc(size, static_cast<std::size_t>(align));
#else
auto ptr = aligned_alloc(static_cast<std::size_t>(align), size);
#endif
if (!ptr)
throw std::bad_alloc{};
std::cout << "new: " << size << ", align: "
<< static_cast<std::size_t>(align)
<< ", ptr: " << ptr << '\n';
lastAllocatedPtr = ptr;
lastSize = size;
return ptr;
}
Revenons maintenant à l'exemple principal:
constexpr auto buf_size = 32;
uint16_t buffer[buf_size] = {}; // a small buffer on the stack
std::fill_n(std::begin(buffer), std::size(buffer) - 1, 0);
std::pmr::monotonic_buffer_resource pool{std::data(buffer), std::size(buffer)*sizeof(uint16_t)};
std::pmr::vector<uint16_t> vec{ &pool };
for (int i = 1; i <= 20; ++i)
vec.push_back(i);
for (int i = 0; i < buf_size; ++i)
std::cout << buffer[i] << " ";
std::cout << std::endl;
auto* bufTemp = (uint16_t *)lastAllocatedPtr;
for (unsigned i = 0; i < lastSize; ++i)
std::cout << bufTemp[i] << " ";
Le programme essaie de mettre 20 nombres dans un vecteur, mais étant donné que le vecteur ne fait que croître, nous avons besoin de plus d'espace que dans le tampon réservé avec 32 entrées.
Par conséquent, à un moment donné, l'allocateur demandera de la mémoire via
get_default_resource
, ce qui conduira à son tour à un appel au global new()
.
Sortie programme:
new: 128, align: 16, ptr: 0xc73b20
1 1 2 1 2 3 4 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 65535 132 0 0 0 0 0 0 0 144 0 0 0 65 0 0 0 16080 199 0 0 16176 199 0 0 16176 199 0 0 15344 199 0 0 15472 199 0 0 15472 199 0 0 0 0 0 0 145 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
À en juger par la sortie vers la console, le tampon alloué est suffisant pour seulement 16 éléments, et lorsque nous insérons le nombre 17, une nouvelle allocation de 128 octets se produit à l'aide de l'opérateur
new()
.
Sur la troisième ligne, nous voyons un bloc de mémoire alloué à l'aide d'un opérateur
new()
.
L'exemple ci-dessus avec remplacement de l'opérateur est
new()
peu susceptible de convenir à une solution produit.
Heureusement, personne ne nous dérange pour faire notre propre implémentation de l'interface
memory_resource
.
Tout ce dont nous avons besoin est
- hériter de
std::pmr::memory_resource
- Mettre en œuvre des méthodes:
do_allocate()
do_deallocate()
do_is_equal()
- Transmettez notre implémentation aux
memory_resource
conteneurs.
C'est tout. Par le lien ci-dessous, vous pouvez regarder le compte rendu de la journée portes ouvertes, où nous parlons en détail du programme de cours, du processus d'apprentissage et répondons aux questions des étudiants potentiels:
Lire la suite