Allocateurs polymorphes C ++ 17

Très prochainement, un nouveau flux du cours «Développeur C ++. Professionnel " . A la veille du début du cours, notre expert Alexander Klyuchev a préparé un matériel intéressant sur les allocateurs polymorphes. Nous donnons la parole à Alexandre:










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::vectoravec 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_resourceafin 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_resourceen 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::pmravec 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 sur memory_resourcelequel lève une exception std::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 unique synchronized_pool_resource.
  • class monotonic_buffer_resource : public std::pmr::memory_resource

    Mono-thread, rapide et memory_resourcespé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_resourceet 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_bufferancienne 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_resourcemesure 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::stringplace std::string.



Prenons un exemple avec un tampon pré-alloué sur la pile, que nous passerons comme memory_resourcepour 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::stringsupérieure à std::string. Cela est dû au fait qu'un pointeur vers memory_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::stringau 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_resourcerenvoie 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_resourceconteneurs.


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






All Articles