Я сильно сперся на інтерновані рядки, як пропонує Базіле, де пошук рядків переводиться на 32-розрядний індекс для зберігання та порівняння. Це корисно в моєму випадку, оскільки я інколи маю сотні тисяч - мільйони компонентів із властивістю під назвою "x", наприклад, яка все-таки повинна бути зручною ім'ям рядка, оскільки до неї часто звертаються скриптери на ім'я.
Я використовую трие для пошуку (експериментував також, unordered_map
але мій налаштований трійник, підкріплений пулами пам’яті, принаймні почав працювати краще, а також було легше зробити безпеку для потоків, не замикаючись щоразу, коли структура була доступна), але це не так швидко для будівництва як створення std::string
. Сенс полягає в тому, щоб прискорити наступні операції, такі як перевірка рівності рядків, яка, в моєму випадку, просто зводиться до перевірки рівності двох цілих чисел і різко скоротити використання пам'яті.
Я думаю, одним із варіантів було б підтримання якогось реєстру вже виділених значень, але чи можливо зробити пошук реєстру швидше, ніж надмірні розподіли пам'яті?
Це буде важко зробити пошук через структуру даних набагато швидше, ніж єдину malloc
, наприклад, Якщо у вас є випадок, коли ви читаєте човновий набір рядків із зовнішнього входу, як-от файл, наприклад, тоді моя спокуса полягала б у використанні послідовного розподільника, якщо це можливо. Це пов'язано з недоліком того, що ви не можете звільнити пам'ять окремої струни. Усю пам'ять, об'єднану алокатором, потрібно вивільнити відразу або взагалі не звільнити. Але послідовний розподільник може бути корисним у тих випадках, коли вам потрібно просто виділити човновий набір крихітних шматочків пам'яті змінного розміру прямим послідовним способом, аби потім викинути все це пізніше. Я не знаю, чи це стосується у вашому випадку чи ні, але, коли це застосовується, це може бути простим способом виправити точку доступу, пов’язану з частими виділеннями підліткової пам'яті (що може мати більше спільного з помилками кешу та помилками сторінки, ніж базові алгоритм, який використовується, скажімо, malloc
).
Виділення фіксованого розміру простіше пришвидшити без послідовних обмежень розподільника, які не дозволяють вам звільнити конкретні шматки пам'яті, щоб згодом повторно їх використовувати. Але зробити розподіл змінного розміру швидше, ніж розподільник за замовчуванням, досить складно. В основному зробити який-небудь розподільник пам'яті швидше, ніж malloc
це, як правило, надзвичайно важко, якщо ви не застосовуєте обмежень, які звужують його застосовність. Одне рішення - використовувати алокатор фіксованого розміру для, скажімо, всіх рядків, розміром яких 8 байт або менше, якщо у вас є навантаження на човні, а довші рядки - рідкісний випадок (для якого ви можете просто використовувати розподільник за замовчуванням). Це означає, що 7 байтів витрачається на 1-байтні рядки, але це повинно усунути точки доступу, пов'язані з розподілом, якщо, скажімо, у 95% часу ваші рядки дуже короткі.
Ще одне рішення, яке мені щойно прийшло в голову, - це використовувати розкручені зв'язані списки, які можуть здатися божевільними, але мене почують.
Ідея тут полягає в тому, щоб зробити кожен розкручений вузол фіксованого розміру замість змінного розміру. Коли ви це зробите, ви можете використовувати надзвичайно швидкий розподільник шматок фіксованого розміру, який об'єднує пам'ять, виділяючи шматки фіксованого розміру для струн змінного розміру, пов'язаних між собою. Це не зменшить використання пам'яті, вона, як правило, додасть її через вартість посилань, але ви можете пограти з нерозкрученим розміром, щоб знайти баланс, відповідний вашим потребам. Це якась дурна ідея, але повинна усунути гарячі точки, пов'язані з пам'яттю, оскільки тепер ви можете ефективно об'єднати пам'ять, вже виділену в об'ємних суміжних блоках, і все ж мати переваги звільнення рядків окремо. Ось простий "фіксований розподільник", який я написав (ілюстративний, який я створив для когось іншого, позбавленого пуху, пов'язаного з виробництвом), яким ви можете вільно користуватися:
#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;
}