Mi appoggio pesantemente alle stringhe internate come suggerisce Basile, dove una ricerca di stringhe si traduce in un indice a 32 bit da memorizzare e confrontare. Questo è utile nel mio caso poiché a volte ho centinaia di migliaia o milioni di componenti con una proprietà denominata "x", ad esempio, che deve ancora essere un nome stringa user-friendly poiché spesso è accessibile dagli script per nome.
Uso un trie per la ricerca (sperimentato anche con unordered_map
ma il mio trie sintonizzato supportato da pool di memoria almeno ha iniziato a funzionare meglio ed è stato anche più facile rendere sicuro il thread senza bloccarlo ogni volta che si accedeva alla struttura) ma non è come veloce per la costruzione come la creazione std::string
. Il punto è più quello di accelerare le operazioni successive come il controllo dell'uguaglianza delle stringhe che, nel mio caso, si riduce al controllo dell'uguaglianza tra due numeri interi e alla riduzione drastica dell'utilizzo della memoria.
Immagino che un'opzione sarebbe quella di mantenere una sorta di registro di valori già allocati, ma è anche possibile rendere le ricerche del registro più veloci delle allocazioni di memoria ridondanti?
Sarà difficile effettuare una ricerca attraverso una struttura di dati molto più velocemente di una singola malloc
, ad esempio Se hai un caso in cui stai leggendo un carico di stringhe da un input esterno come un file, ad esempio, la mia tentazione sarebbe di usare un allocatore sequenziale, se possibile. Ciò ha il rovescio della medaglia che non è possibile liberare memoria di una singola stringa. Tutta la memoria messa in comune dall'allocatore deve essere liberata in una volta o per niente. Ma un allocatore sequenziale può essere utile nei casi in cui hai solo bisogno di allocare un carico di piccoli pezzi di memoria di dimensioni variabili in modo sequenziale, solo per poi buttare via tutto in seguito. Non so se ciò si applichi nel tuo caso o no, ma quando applicabile, può essere un modo semplice per correggere un hotspot correlato a frequenti allocazioni di memoria per adolescenti (che potrebbe avere più a che fare con mancate cache e errori di pagina rispetto al sottostante algoritmo utilizzato da, diciamo, malloc
).
Le allocazioni di dimensioni fisse sono più facili da accelerare senza i vincoli sequenziali dell'allocatore che impediscono di liberare blocchi di memoria specifici da riutilizzare in seguito. Ma rendere l'allocazione di dimensioni variabili più veloce dell'allocatore predefinito è piuttosto difficile. Fondamentalmente rendere qualsiasi tipo di allocatore di memoria più veloce di quanto malloc
sia generalmente estremamente difficile se non si applicano vincoli che ne restringono l'applicabilità. Una soluzione consiste nell'utilizzare un allocatore di dimensioni fisse per, per esempio, tutte le stringhe che sono 8 byte o meno se ne hai una barca e stringhe più lunghe sono un caso raro (per il quale puoi semplicemente utilizzare l'allocatore predefinito). Ciò significa che vengono sprecati 7 byte per le stringhe da 1 byte, ma dovrebbe eliminare gli hotspot relativi all'allocazione, se, diciamo, il 95% delle volte, le stringhe sono molto brevi.
Un'altra soluzione che mi è appena venuta in mente è quella di utilizzare elenchi collegati non srotolati che potrebbero sembrare pazzi ma ascoltarmi.
L'idea qui è di rendere ogni nodo srotolato una dimensione fissa anziché una dimensione variabile. Quando lo fai, puoi utilizzare un allocatore di blocchi di dimensioni fisse estremamente veloce che raggruppa la memoria, allocando blocchi di dimensioni fisse per stringhe di dimensioni variabili collegate tra loro. Ciò non ridurrà l'uso della memoria, tenderà ad aggiungerlo a causa del costo dei collegamenti, ma puoi giocare con le dimensioni srotolate per trovare un equilibrio adatto alle tue esigenze. È una specie di idea stravagante, ma dovrebbe eliminare gli hotspot relativi alla memoria poiché ora è possibile raggruppare efficacemente la memoria già allocata in blocchi contigui e avere comunque i vantaggi di liberare le stringhe singolarmente. Ecco un semplice vecchio allocatore fisso che ho scritto (uno illustrativo che ho realizzato per qualcun altro, privo di lanugine legata alla produzione) che puoi usare liberamente:
#ifndef FIXED_ALLOCATOR_HPP
#define FIXED_ALLOCATOR_HPP
class FixedAllocator
{
public:
/// Creates a fixed allocator with the specified type and block size.
explicit FixedAllocator(int type_size, int block_size = 2048);
/// Destroys the allocator.
~FixedAllocator();
/// @return A pointer to a newly allocated chunk.
void* allocate();
/// Frees the specified chunk.
void deallocate(void* mem);
private:
struct Block;
struct FreeElement;
FreeElement* free_element;
Block* head;
int type_size;
int num_block_elements;
};
#endif
#include "FixedAllocator.hpp"
#include <cstdlib>
struct FixedAllocator::FreeElement
{
FreeElement* next_element;
};
struct FixedAllocator::Block
{
Block* next;
char* mem;
};
FixedAllocator::FixedAllocator(int type_size, int block_size): free_element(0), head(0)
{
type_size = type_size > sizeof(FreeElement) ? type_size: sizeof(FreeElement);
num_block_elements = block_size / type_size;
if (num_block_elements == 0)
num_block_elements = 1;
}
FixedAllocator::~FixedAllocator()
{
// Free each block in the list, popping a block until the stack is empty.
while (head)
{
Block* block = head;
head = head->next;
free(block->mem);
free(block);
}
free_element = 0;
}
void* FixedAllocator::allocate()
{
// Common case: just pop free element and return.
if (free_element)
{
void* mem = free_element;
free_element = free_element->next_element;
return mem;
}
// Rare case when we're out of free elements.
// Create new block.
Block* new_block = static_cast<Block*>(malloc(sizeof(Block)));
new_block->mem = malloc(type_size * num_block_elements);
new_block->next = head;
head = new_block;
// Push all but one of the new block's elements to the free stack.
char* mem = new_block->mem;
for (int j=1; j < num_block_elements; ++j)
{
void* ptr = mem + j*type_size;
FreeElement* element = static_cast<FreeElement*>(ptr);
element->next_element = free_element;
free_element = element;
}
return mem;
}
void FixedAllocator::deallocate(void* mem)
{
// Just push a free element to the stack.
FreeElement* element = static_cast<FreeElement*>(mem);
element->next_element = free_element;
free_element = element;
}