Alocadores polimórficos C ++ 17

Em breve, um novo fluxo do curso “Desenvolvedor C ++. Profissional " . Na véspera do início do curso, nosso especialista Alexander Klyuchev preparou um material interessante sobre alocadores polimórficos. Passamos a palavra ao Alexandre:










Neste artigo, gostaria de mostrar exemplos simples de como trabalhar com componentes do namespace pmr e as ideias básicas subjacentes aos alocadores polimórficos.



A ideia principal dos alocadores polimórficos introduzidos em c ++ 17 é melhorar os alocadores padrão implementados com base no polimorfismo estático ou em outras palavras, modelos. Eles são muito mais fáceis de usar do que os alocadores padrão, além disso, eles permitem que você mantenha o tipo de contêiner ao usar diferentes alocadores e, portanto, altere os alocadores em tempo de execução.



Se você quiser std::vectorum alocador de memória específico, pode usar o parâmetro do modelo Allocator:



auto my_vector = std::vector<int, my_allocator>();




Mas há um problema - esse vetor não é do mesmo tipo que um vetor com um alocador diferente, incluindo um definido por padrão.

Tal contêiner não pode ser passado para uma função que requer um vetor com um contêiner padrão, nem dois vetores com diferentes tipos de alocadores podem ser atribuídos à mesma variável, por exemplo:



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


Um alocador polimórfico contém um ponteiro para uma interface memory_resourcepara que possa usar o despacho dinâmico.



Para mudar a estratégia de trabalhar com memória, basta substituir a instância memory_resource, mantendo o tipo do alocador. Isso também pode ser feito em tempo de execução. Caso contrário, os alocadores polimórficos funcionam de acordo com as mesmas regras que os padrão.



Os tipos de dados específicos usados ​​pelo novo alocador estão no namespace std::pmr. Também há especializações de modelo de contêineres padrão que podem funcionar com um alocador polimórfico.



Um dos principais problemas no momento é a incompatibilidade de novas versões de recipientes de std::pmrcom análogos de std.



Componentes principais 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()

      A função free retorna um ponteiro para o memory_resourcequal lança uma exceção std::bad_allocem cada tentativa de alocação.

      Isso pode ser útil para garantir que os objetos não aloquem memória no heap ou para fins de teste.




  • class synchronized_pool_resource : public std::pmr::memory_resource

    Uma implementação de memory_resource de propósito geral segura para thread consiste em um conjunto de pools com diferentes tamanhos de blocos de memória.

    Cada pool é uma coleção de blocos de memória do mesmo tamanho.
  • class unsynchronized_pool_resource : public std::pmr::memory_resource

    Versão de encadeamento único synchronized_pool_resource.
  • class monotonic_buffer_resource : public std::pmr::memory_resource

    Single-threaded, fast, memory_resourcespecial-purpose take memória de um buffer pré-alocado, mas não o libera, isto é, ele só pode crescer.


Exemplo de uso monotonic_buffer_resourcee 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';
}


Resultado do programa:




_______________________________________________________________
aababcdabcdefghabcdefghijklmnopabcdefghijklmnopqrstuvwxyz______


No exemplo acima, usamos monotonic_buffer_resourceinicializado com um buffer alocado na pilha. Usando um ponteiro para este buffer, podemos facilmente exibir o conteúdo da memória.



O vetor tira memória do pool, o que é muito rápido, pois está na pilha, se ficar sem memória, ele a solicita usando o operador global new. O exemplo demonstra uma implementação de vetor ao tentar inserir mais do que o número reservado de elementos. Nesse caso, a monotonic_buffervelha memória não é liberada, mas apenas cresce.



Você pode, é claro, chamar reserve()um vetor para minimizar as realocações, mas o objetivo do exemplo é precisamente demonstrar como ele muda monotonic_buffer_resourceconforme o contêiner se expande.



Armazenamento pmr::string



E se quisermos armazenar strings pmr::vector?



Um recurso importante é que, se os objetos em um contêiner também usarem um alocador polimórfico, eles solicitarão o alocador do contêiner pai para gerenciamento de memória.



Se você quiser aproveitar este recurso, você deve usar std::pmr::stringem vez std::string.



Considere um exemplo com um buffer pré-alocado na pilha, que passaremos memory_resourcepor 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");   
}


Resultado do programa:



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#_______________________________#


Os principais pontos a serem observados neste exemplo:



  • O tamanho é pmr::stringmaior do que std::string. Isso se deve ao fato de que um ponteiro para memory_resource;
  • Reservamos o vetor para 5 elementos, de forma que nenhuma realocação ocorra ao adicionar 4.
  • As primeiras 2 linhas são curtas o suficiente para o bloco de memória vetorial, portanto, nenhuma alocação de memória adicional ocorre.
  • A terceira linha é mais longa e requer um pedaço separado de memória dentro de nosso buffer, e apenas o ponteiro para este bloco é armazenado no vetor.
  • Como você pode ver na saída, "Esta é uma string mais longa" está localizada quase no final do buffer.
  • Quando inserimos outra string curta, ela volta para o bloco de memória do vetor


Para comparação, vamos fazer o mesmo experimento com em std::stringvez 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###_______________________________________________________________________________________________________________________________#




Desta vez, os itens no contêiner ocupam menos espaço porque não há necessidade de armazenar um ponteiro para o memory_resource.

As strings curtas ainda são armazenadas dentro do bloco de memória vetorial, mas agora a string longa não entrou em nosso buffer. Desta vez, uma longa string é alocada usando o alocador padrão e um

ponteiro para ela é colocado no bloco de memória vetorial . Portanto, não vemos essa linha na saída.



Mais uma vez sobre a expansão do vetor:



Foi mencionado que quando a memória do pool se esgota, o alocador a solicita usando o operador new().



Na verdade, isso não é inteiramente verdade - a memória é solicitada de memory_resource, retornada usando uma função livre

std::pmr::memory_resource* get_default_resource()

Por padrão, esta função retorna

std::pmr::new_delete_resource(), que por sua vez aloca memória usando um operador new(), mas pode ser substituída usando uma função.

std::pmr::memory_resource* set_default_resource(std::pmr::memory_resource* r)



Então, vamos ver um exemplo quando ela get_default_resourceretorna um valor por padrão.



Deve-se ter em mente que os métodos do_allocate()e do_deallocate()usam o argumento "alinhamento", por isso precisamos da versão C ++ 17 new()com suporte para alinhamento:



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;
}


Agora vamos voltar ao exemplo 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] << " ";


O programa tenta colocar 20 números em um vetor, mas dado que o vetor está crescendo, precisamos de mais espaço do que no buffer reservado com 32 entradas.



Portanto, em algum ponto, o alocador solicitará a memória get_default_resource, que por sua vez levará a uma chamada para o global new().



Resultado do programa:



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


A julgar pela saída para o console, o buffer alocado é suficiente para apenas 16 elementos, e quando inserimos o número 17, uma nova alocação de 128 bytes ocorre usando o operador new().



Na terceira linha, vemos um bloco de memória alocado usando um operador new().



O exemplo acima com anulação do operador new()provavelmente não é adequado para uma solução de produto.



Felizmente, ninguém nos incomoda em fazer nossa própria implementação da interface memory_resource.



Tudo o que precisamos é



  • herdar de std::pmr::memory_resource
  • Métodos de implementação:

    • do_allocate()
    • do_deallocate()
    • do_is_equal()
  • Passe nossa implementação para memory_resourcecontêineres.


Isso é tudo. No link abaixo você pode assistir ao registro do open house day, onde contamos em detalhes sobre o programa do curso, o processo de aprendizagem e respondemos a dúvidas de potenciais alunos:





Consulte Mais informação






All Articles