Pergunta

Quais são algumas realmente boas razões para std::allocator vala em favor de uma solução personalizada? Tenha você se depara com quaisquer situações em que era absolutamente necessário para correção, desempenho, escalabilidade, etc? Quaisquer exemplos realmente inteligente?

allocators personalizado sempre foram uma característica da biblioteca padrão que eu não tive muita necessidade de. Eu só estava me perguntando se alguém aqui no SO poderia fornecer alguns exemplos convincentes para justificar a sua existência.

Foi útil?

Solução

Como eu mencionei aqui , eu vi STL costume da Intel TBB alocador melhorar significativamente o desempenho de um aplicativo multithreaded simplesmente mudando um único

std::vector<T>

para

std::vector<T,tbb::scalable_allocator<T> >

(esta é uma maneira rápida e conveniente de mudar o alocador de usar pilhas de rosca-privada bacana de TBB; ver página 7 neste documento )

Outras dicas

Uma área onde allocators personalizado pode ser útil é o desenvolvimento do jogo, especialmente nas consolas de jogos, como eles têm apenas uma pequena quantidade de memória e não swap. Nesses sistemas você quiser ter certeza de que você tem um controlo apertado sobre cada subsistema, de modo que um sistema não-crítica não pode roubar a memória de uma crítica. Outras coisas como alocadores de piscina pode ajudar a reduzir a fragmentação de memória. Você pode encontrar um artigo longo, detalhado sobre o tema em:

EASTL - Standard Template Library Electronic Arts

Eu estou trabalhando em um mmap-alocador que permite vetores para usar a memória de um arquivo de memória mapeada. O objetivo é ter vetores que o armazenamento de uso que estão diretamente na memória virtual mapeada por mmap. Nosso problema é melhorar a leitura dos realmente grandes arquivos (> 10 GB) na memória sem cópia sobrecarga, portanto, eu preciso deste alocador personalizado.

Até agora eu tenho o esqueleto de um alocador personalizado (Que deriva de std :: allocator), eu acho que é um bom ponto de partida apontam para escrever próprios allocators. Sinta-se livre para usar este pedaço de código de qualquer maneira que você quer:

#include <memory>
#include <stdio.h>

namespace mmap_allocator_namespace
{
        // See StackOverflow replies to this answer for important commentary about inheriting from std::allocator before replicating this code.
        template <typename T>
        class mmap_allocator: public std::allocator<T>
        {
public:
                typedef size_t size_type;
                typedef T* pointer;
                typedef const T* const_pointer;

                template<typename _Tp1>
                struct rebind
                {
                        typedef mmap_allocator<_Tp1> other;
                };

                pointer allocate(size_type n, const void *hint=0)
                {
                        fprintf(stderr, "Alloc %d bytes.\n", n*sizeof(T));
                        return std::allocator<T>::allocate(n, hint);
                }

                void deallocate(pointer p, size_type n)
                {
                        fprintf(stderr, "Dealloc %d bytes (%p).\n", n*sizeof(T), p);
                        return std::allocator<T>::deallocate(p, n);
                }

                mmap_allocator() throw(): std::allocator<T>() { fprintf(stderr, "Hello allocator!\n"); }
                mmap_allocator(const mmap_allocator &a) throw(): std::allocator<T>(a) { }
                template <class U>                    
                mmap_allocator(const mmap_allocator<U> &a) throw(): std::allocator<T>(a) { }
                ~mmap_allocator() throw() { }
        };
}

Para usar isso, declarar um recipiente STL da seguinte forma:

using namespace std;
using namespace mmap_allocator_namespace;

vector<int, mmap_allocator<int> > int_vec(1024, 0, mmap_allocator<int>());

Pode ser usado, por exemplo, para iniciar sessão sempre que a memória é alocada. O que é neccessary é o struct rebind, então o recipiente vector utiliza as superclasses alocar / desalocar métodos.

Update: O alocador de mapeamento de memória está agora disponível em https://github.com/johannesthoma/mmap_allocator e é LGPL. Sinta-se livre para usá-lo para seus projetos.

Eu estou trabalhando com um mecanismo de armazenamento do MySQL que usa C ++ para o seu código. Estamos usando um alocador personalizado para usar o sistema de memória MySQL em vez de competir com o MySQL para a memória. Ela nos permite ter certeza de que está usando a memória como o MySQL configurado usuário para uso, e não "extra".

Pode ser útil usar allocators personalizados para usar um pool de memória em vez do heap. Isso é um exemplo, entre muitos outros.

Na maioria dos casos, esta é certamente uma otimização prematura. Mas pode ser muito útil em determinados contextos (dispositivos embarcados, jogos, etc).

Eu não tenho escrito código C ++ com um alocador STL costume, mas posso imaginar um servidor web escrito em C ++, que usa um alocador personalizado para exclusão automática de dados temporários necessários para responder a uma solicitação HTTP. O alocador customizado pode libertar todos os dados temporários ao mesmo tempo uma vez que a resposta foi gerada.

Outro caso de uso possível para um alocador customizado (que eu usei) está escrevendo um teste de unidade para provar que o comportamento de uma função não depende de alguma parte de sua entrada. O alocador customizado pode encher a região de memória com qualquer padrão.

Ao trabalhar com GPUs ou outros co-processadores às vezes é benéfico para alocar estruturas de dados na memória principal em um maneira especial . Este maneira especial de alocação de memória pode implementado em um alocador de costume em uma forma conveniente.

A razão pela qual a alocação personalizado através do tempo de execução acelerador pode ser benéfico ao usar aceleradores é o seguinte:

  1. através da alocação costume o tempo de execução acelerador ou motorista é notificado do bloco de memória
  2. Além disso, o sistema operacional pode ter certeza de que o bloco de memória alocado é página bloqueada (alguns chamam este memória preso ), ou seja, o subsistema de memória virtual do sistema operacional não pode mover ou remover a página dentro ou a partir da memória
  3. se 1. e 2. espera e uma transferência de dados entre um bloco de memória página bloqueada e um acelerador é solicitado, o tempo de execução podem acessar diretamente os dados na memória principal, uma vez que sabe onde está e pode ter certeza que a operação sistema não se mexeu / removê-lo
  4. este salva uma cópia de memória que ocorreria com a memória que foi alocada de forma não-bloqueado-page: os dados a serem copiados na memória principal para a área de teste páginas bloqueado a partir com o acelerador pode inicializar a transferência de dados (através DMA)

Eu estou usando alocadores personalizados aqui; você pode até dizer que foi para o trabalho torno outro gerenciamento de memória dinâmica personalizado.

Fundo: temos sobrecargas para malloc, calloc, livre, e as várias variantes do operador de novo e apagar, eo vinculador felizmente faz uso STL estes para nós. Isso nos permite fazer coisas como pequena pooling automática objeto, detecção de fugas, preenchimento alloc, preenchimento livre, alocação de preenchimento com sentinelas, o alinhamento da linha de cache para certas alocações, e atrasou livre.

O problema é que estamos executando em um ambiente incorporado - não há memória suficiente em torno de realmente fazer a detecção de vazamento respondendo adequadamente durante um período prolongado. Pelo menos, não na RAM padrão - há uma outra pilha de RAM em outros lugares disponíveis, através de funções de alocação de costume

.

Solução: escrever um alocador personalizado que usa a pilha estendida, e usá-lo única nos internos da arquitetura de monitoramento de vazamento de memória ... Tudo o resto padrões para as sobrecargas novos / apagar normais que fazer rastreamento de vazamento. Isso evita o rastreador rastreamento em si (e fornece um pouco de funcionalidade embalagem extra também, nós sabemos o tamanho de nós Tracker).

Nós também usar isso para manter o custo função de perfis de dados, pelo mesmo motivo; escrever uma entrada para cada chamada de função e retorno, bem como interruptores de rosca, pode começar rápido caro. alocador de costume novamente nos dá alocações menores em uma área de memória de depuração maior.

Eu estou usando um alocador personalizado para contar o número de alocações / deallocations em uma parte do meu programa e medir quanto tempo leva. Há outras maneiras isso pode ser alcançado, mas este método é muito conveniente para mim. É especialmente útil que eu posso usar o alocador personalizado para apenas um subconjunto dos meus recipientes.

Uma situação essencial:. Ao escrever código que o trabalho deve através do módulo (EXE / DLL) limites, é essencial para manter suas alocações e exclusões acontecendo em apenas um módulo

Onde eu corri para este foi uma arquitetura de plug-in no Windows. É essencial que, por exemplo, se você passar um std :: string através do limite DLL, que qualquer realocações da corda ocorrer a partir do monte onde se originou a partir, não a pilha no DLL que pode ser diferente *.

* É mais complicado do que isso, na verdade, como se você está ligando de forma dinâmica para o CRT este trabalho poder de qualquer maneira. Mas se cada DLL tem um link estático para o CRT você estiver indo para um mundo de dor, onde erros de alocação fantasma ocorrem continuamente.

Um exemplo de I vez que eu usei estes estava trabalhando com sistemas embarcados com restrições muito recursos. Vamos dizer que você tem 2k de memória RAM livre e seu programa tem que usar um pouco dessa memória. Você precisa loja digamos 4-5 seqüências em algum lugar que não é na pilha e, adicionalmente, você precisa ter um acesso muito preciso sobre onde estas coisas ficam armazenados, esta é uma situação em que você pode querer escrever seu próprio alocador. As implementações padrão podem fragmentar a memória, isso pode ser inaceitável se você não tem memória suficiente e não pode reiniciar o programa.

Um projeto que eu estava trabalhando foi usando AVR-GCC em alguns chips de baixa potência. Tivemos de armazenar 8 sequências de comprimento variável mas com um máximo conhecido. A implementação da biblioteca padrão do gerenciamento de memória é um wrapper fino ao redor malloc / livre que mantém o controle de onde colocar itens com precedendo cada bloco de memória alocado com um ponteiro para apenas após o final desse pedaço de memória alocado. Ao alocar uma nova peça de memória o alocador padrão tem de andar sobre cada um dos pedaços de memória para encontrar o próximo bloco que está disponível onde o tamanho de memória solicitada vai caber. Em uma plataforma de desktop isso seria muito rápido para este alguns itens, mas você tem que ter em mente que alguns destes microcontroladores são muito lento e primitivo em comparação. Além disso, o problema de fragmentação de memória era um problema enorme que significava que realmente não tinha escolha a não ser tomar uma abordagem diferente.

Então, o que fizemos foi implementar nosso próprio . Cada bloco de memória era grande o suficiente para caber a maior seqüência precisaríamos nele. Este alocados blocos de tamanho fixo de frente memória do tempo e marcado que blocos de memória foram atualmente em uso. Nós fizemos isso, mantendo um número inteiro de 8 bits onde cada bit representado, se foi usado um determinado bloco. Nós negociados fora uso de memória aqui para tentar fazer todo o processo mais rápido, que no nosso caso foi justificado como nós estávamos empurrando este chip microcontrolador perto a sua capacidade máxima de processamento.

Há uma série de outras vezes que eu posso ver escrevendo seu próprio alocador de costume no contexto de sistemas embarcados, por exemplo, se a memória para a seqüência não está na RAM principal como poderia ser frequentemente o caso em estas plataformas .

Para a memória compartilhada é vital que não só a cabeça do recipiente, mas também os dados que ele contém são armazenados na memória compartilhada.

O alocador de impulso :: Interprocess é um bom exemplo. No entanto, como você pode ler aqui este allone não é suficiente, para fazer todos os recipientes STL memória compartilhada compatível (Devido a diferentes deslocamentos de mapeamento em diferentes processos, os ponteiros pode "break").

link obrigatório para CppCon 2.015 palestra de Andrei Alexandrescu em allocators:

https://www.youtube.com/watch?v=LIb3L4vKZ7U

O bom é que só inventando-los faz você pensar em idéias de como você usá-los: -)

Algum tempo atrás eu encontrei esta solução muito útil para mim: rápido C ++ 11 alocador para contêineres STL . É ligeiramente acelera recipientes STL sobre VS2017 (~ 5x), bem como no GCC (~ 7x). É um alocador de propósito específico com base no pool de memória. Ele pode ser usado com recipientes STL apenas graças ao mecanismo que você está pedindo.

Eu pessoalmente uso Loki :: Allocator / SmallObject para uso de memória optimize para pequenos objetos - que mostram boa eficiência e desempenho satisfatório se você tem que trabalhar com quantidades moderadas de realmente pequenos objetos (de 1 a 256 bytes). Pode ser até ~ 30 vezes mais eficientes do que o padrão C ++ alocação novo / delete se falamos de alocação de quantidades moderadas de pequenos objetos de diversos tamanhos. Além disso, há uma solução específica de VC chamado "QuickHeap", que traz melhor desempenho possível (alocar e operações desalocar apenas ler e escrever o endereço do bloco que está sendo alocado / retornou à pilha, respectivamente em até 99. (9)% dos casos - depende das configurações e inicialização), mas a um custo de uma sobrecarga notável - ele precisa de dois ponteiros por medida e um extra para cada novo bloco de memória. É uma mais rápida possível solução para trabalhar com enorme (10 000 ++) quantidades de objetos que estão sendo criados e excluídos se você não precisa de uma grande variedade de tamanhos de objeto (ele cria uma piscina individual para cada tamanho do objeto, de 1 a 1023 bytes na implementação atual, de modo de inicialização custos podem diminuir o impulso desempenho global, mas pode-se ir em frente e alocar / desalocar alguns objetos fictícios antes da aplicação entra a sua fase de desempenho crítico (s)).

O problema com o padrão C ++ implementação novo / delete é que normalmente é apenas um wrapper para C malloc / atribuição gratuita, e funciona bem para grandes blocos de memória, como 1024 + bytes. Tem uma sobrecarga notável em termos de desempenho e, às vezes, memória extra utilizado para mapeamento também. Assim, na maioria dos casos allocators personalizados são implementados de forma a maximizar o desempenho e / ou minimizar a quantidade de memória extra necessária para alocar pequenas (=1024 bytes) objetos.

allocators personalizados Em uma simulação gráfica, eu vi utilizadas para

  1. Restrições de alinhamento que std::allocator não suportam diretamente.
  2. Minimização fragmentação usando piscinas separadas para alocações de vida curta (apenas este quadro) e de longa duração.
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top