Eu tenho um componente C ++ bastante complexo, cujo desempenho se tornou um problema. A criação de perfil mostra que a maior parte do tempo de execução é simplesmente alocada memória para std::string
s.
Eu sei que há muita redundância entre essas strings. Um punhado de valores se repete com muita frequência, mas também existem muitos valores exclusivos. As strings são geralmente bastante curtas.
Agora estou pensando se faria sentido reutilizar essas alocações frequentes. Em vez de 1000 ponteiros para 1000 valores distintos "foobar", eu poderia ter 1000 ponteiros para um valor "foobar". O fato de que isso seria mais eficiente em termos de memória é um bom bônus, mas estou mais preocupado com a latência aqui.
Eu acho que uma opção seria manter algum tipo de registro de valores já alocados, mas é possível fazer pesquisas de registro mais rapidamente do que alocações de memória redundantes? Essa é uma abordagem viável?
fonte
+
operador ou com fluxos de strings? De onde vêm as cordas? Literais no seu código ou entrada externa?Respostas:
Eu me apóio muito em seqüências de caracteres internas, como sugere Basile, onde uma pesquisa de sequência se traduz em um índice de 32 bits para armazenar e comparar. Isso é útil no meu caso, já que às vezes tenho centenas de milhares a milhões de componentes com uma propriedade chamada "x", por exemplo, que ainda precisa ser um nome de seqüência de caracteres fácil de usar, pois geralmente é acessado por scripts por nome.
Eu uso um trie para a pesquisa (experimentei também com,
unordered_map
mas meu trie sintonizado, apoiado por pools de memória, pelo menos começou com um desempenho melhor e também foi mais fácil tornar o thread-safe sem bloquear apenas toda vez que a estrutura foi acessada), mas não é tão rápido para construção como criaçãostd::string
. O ponto é mais para acelerar as operações subseqüentes, como verificar a igualdade de strings, que, no meu caso, se resume apenas a verificar a igualdade de dois números inteiros e reduzir drasticamente o uso de memória.Vai ser difícil fazer uma pesquisa através de uma estrutura de dados muito mais rápido do que um único
malloc
, por exemplo, se você tem um caso em que está lendo um monte de strings de uma entrada externa como um arquivo, por exemplo, minha tentação seria usar um alocador seqüencial, se possível. Isso vem com a desvantagem de que você não pode liberar memória de uma sequência individual. Toda a memória reunida pelo alocador deve ser liberada de uma só vez ou não. Mas um alocador seqüencial pode ser útil nos casos em que você apenas precisa alocar uma carga de pequenos pedaços de memória de tamanho variável de maneira sequencial direta, apenas para depois jogar tudo fora. Não sei se isso se aplica ao seu caso ou não, mas quando aplicável, pode ser uma maneira fácil de corrigir um ponto de acesso relacionado a alocações freqüentes de memória pequenina (que podem ter mais a ver com falhas de cache e falhas de página do que as subjacentes algoritmo usado por, digamosmalloc
)).As alocações de tamanho fixo são mais fáceis de acelerar, sem as restrições sequenciais do alocador que impedem que você libere pedaços específicos de memória para reutilização posterior. Mas tornar a alocação de tamanho variável mais rápida que o alocador padrão é bastante difícil. Basicamente, criar qualquer tipo de alocador de memória mais rápido do que
malloc
geralmente é extremamente difícil, se você não aplicar restrições que restrinjam sua aplicabilidade. Uma solução é usar um alocador de tamanho fixo para, digamos, todas as cadeias de caracteres com 8 bytes ou menos, se você tiver uma carga de barco delas e cadeias mais longas forem um caso raro (para o qual você pode apenas usar o alocador padrão). Isso significa que 7 bytes são desperdiçados para cadeias de caracteres de 1 byte, mas devem eliminar pontos de acesso relacionados à alocação, se, digamos, 95% do tempo, suas cadeias de caracteres são muito curtas.Outra solução que me ocorreu é usar listas vinculadas não desenroladas que podem parecer loucuras, mas que me escutam.
A idéia aqui é tornar cada nó desenrolado um tamanho fixo em vez de tamanho variável. Ao fazer isso, você pode usar um alocador de bloco de tamanho fixo extremamente rápido que agrupa memória, alocando blocos de tamanho fixo para cadeias de tamanho variável vinculadas. Isso não reduzirá o uso de memória, tenderá a aumentar por causa do custo dos links, mas você pode jogar com o tamanho não enrolado para encontrar um equilíbrio adequado às suas necessidades. É uma idéia meio maluca, mas deve eliminar os pontos ativos relacionados à memória, pois agora você pode agrupar efetivamente a memória já alocada em blocos contíguos e volumosos e ainda ter os benefícios de liberar seqüências individualmente. Aqui está um simples e antigo alocador fixo que escrevi (um ilustrativo que criei para outra pessoa, desprovido de problemas relacionados à produção) que você pode usar livremente:
fonte
Você pode querer ter algumas máquinas de cordas internamente (mas as cordas devem ser imutáveis, use
const std::string
-s). Você pode querer alguns símbolos . Você pode procurar por ponteiros inteligentes (por exemplo, std :: shared_ptr ). Ou mesmo std :: string_view no C ++ 17.fonte
Uma vez na construção do compilador, usamos algo chamado data-chair (em vez de data-bank, uma tradução coloquial em alemão para DB). Isso simplesmente criou um hash para uma string e o usou para alocação. Portanto, qualquer string não era um pedaço de memória no heap / stack, mas um código de hash para essa cadeira de dados. Você poderia substituir
String
por essa classe. Precisa de algum retrabalho de código, no entanto. E é claro que isso só é utilizável para seqüências de E / S.fonte
Observe como a alocação de memória e a memória real usada estão relacionadas ao desempenho ruim:
O custo de realmente alocar a memória é, obviamente, muito alto. Portanto, std :: string já pode usar alocação in-loco para pequenas seqüências de caracteres, e a quantidade de alocações reais pode ser menor do que você imagina primeiro. Caso o tamanho desse buffer não seja grande o suficiente, você pode se inspirar na classe de string do Facebook ( https://github.com/facebook/folly/blob/master/folly/FBString.h ), que usa 23 caracteres internamente antes da alocação.
Também vale a pena notar o custo de usar muita memória. Este talvez seja o maior ofensor: você pode ter bastante RAM em sua máquina; no entanto, os tamanhos de cache ainda são pequenos o suficiente para prejudicar o desempenho ao acessar a memória que ainda não está armazenada em cache. Você pode ler sobre isso aqui: https://en.wikipedia.org/wiki/Locality_of_reference
fonte
Em vez de tornar as operações de cadeia mais rápidas, outra abordagem é reduzir o número de operações de cadeia. Seria possível substituir seqüências de caracteres por uma enumeração, por exemplo?
Outra abordagem que pode ser útil é usada no cacau: há casos em que você tem centenas ou milhares de dicionários, todos com a mesma chave. Então, eles permitem que você crie um objeto que é um conjunto de chaves de dicionário e existe um construtor de dicionário que aceita esse objeto como argumento. O dicionário se comporta da mesma forma que qualquer outro dicionário, mas quando você adiciona um par de chave / valor com uma chave nesse conjunto de chaves, a chave não é duplicada, mas apenas um ponteiro para a chave no conjunto de chaves é armazenado. Portanto, esses milhares de dicionários precisam de apenas uma cópia de cada sequência de teclas nesse conjunto.
fonte