Як би повторюючи пропозицію Kylotan, але я б рекомендував вирішити це на рівні структури даних, коли це можливо, а не на нижньому рівні розподільника, якщо ви можете допомогти.
Ось простий приклад того, як можна уникнути виділення та звільнення Foos
багаторазово, використовуючи масив з отворами з елементами, пов'язаними між собою (вирішуючи це на рівні "контейнер" замість рівня "розподільник"):
struct FooNode
{
explicit FooNode(const Foo& ielement): element(ielement), next(-1) {}
// Stores a 'Foo'.
Foo element;
// Points to the next foo available; either the
// next used foo or the next deleted foo. Can
// use SoA and hoist this out if Foo doesn't
// have 32-bit alignment.
int next;
};
struct Foos
{
// Stores all the Foo nodes.
vector<FooNode> nodes;
// Points to the first used node.
int first_node;
// Points to the first free node.
int free_node;
Foos(): first_node(-1), free_node(-1)
{
}
const FooNode& operator[](int n) const
{
return data[n];
}
void insert(const Foo& element)
{
int index = free_node;
if (index != -1)
{
// If there's a free node available,
// pop it from the free list, overwrite it,
// and push it to the used list.
free_node = data[index].next;
data[index].next = first_node;
data[index].element = element;
first_node = index;
}
else
{
// If there's no free node available, add a
// new node and push it to the used list.
FooNode new_node(element);
new_node.next = first_node;
first_node = data.size() - 1;
data.push_back(new_node);
}
}
void erase(int n)
{
// If the node being removed is the first used
// node, pop it from the used list.
if (first_node == n)
first_node = data[n].next;
// Push the node to the free list.
data[n].next = free_node;
free_node = n;
}
};
Щось до цього ефекту: окремо пов'язаний список індексів із вільним списком. Посилання індексу дозволяють пропускати через видалені елементи, видаляти елементи в постійному часі, а також повертати / повторно використовувати / перезаписувати вільні елементи з вставкою постійного часу. Щоб повторити структуру, ви робите щось подібне:
for (int index = foos.first_node; index != -1; index = foos[index].next)
// do something with foos[index]
І ви можете узагальнити вищезазначений тип структури даних "пов'язаний масив дірок" за допомогою шаблонів, розміщення нового та виклику вручну dtor, щоб уникнути вимоги щодо призначення копії, змусити викликати деструктори, коли елементи видаляються, надавати ітератор вперед тощо. вирішив зберегти приклад дуже С-подібний, щоб більш наочно проілюструвати концепцію, а також тому, що я дуже ледачий
Зважаючи на це, ця структура має тенденцію до деградації в просторовій місцевості після того, як ви видаляєте та вставляєте речі багато в / із середини. У цей момент next
посилання можуть вести вас вперед і назад по вектору, перезавантажуючи дані, попередньо витягнуті з кеш-лінії в межах одного послідовного обходу (це неминуче для будь-якої структури даних або розподільника, що дозволяє видаляти постійний час без переміщення елементів під час відшкодування. пробіли від середини з постійною вставкою та без використання чогось на зразок паралельного біта або removed
прапора). Щоб відновити сприятливість кешу, ви можете реалізувати метод копіювання копіювання та swap таким чином:
Foos(const Foos& other)
{
for (int index = other.first_node; index != -1; index = other[index].next)
insert(foos[index].element);
}
void Foos::swap(Foos& other)
{
nodes.swap(other.nodes):
std::swap(first_node, other.first_node);
std::swap(free_node, other.free_node);
}
// ... then just copy and swap:
Foos(foos).swap(foos);
Тепер нова версія знову зручна в кеш-пам’яті. Інший метод - це зберігати в структурі окремий список індексів і періодично їх сортувати. Інша - використовувати біт, щоб вказати, які індекси використовуються. Це завжди дозволить вам переміщати біт в послідовному порядку (щоб це зробити ефективно, перевіряйте 64-бітні одночасно, наприклад, використовуючи FFS / FFZ). Біт-набір є найефективнішим і не нав'язливим, вимагає лише паралельного біта на один елемент, щоб вказати, які з них використовуються, а які видаляються, замість того, щоб вимагати 32-бітових next
індексів, але найбільш трудомісткий, щоб записати добре (він не буде будьте швидкі для обходу, якщо ви перевіряєте один біт за один раз - вам потрібно FFS / FFZ, щоб знайти набір або скинути біт негайно серед 32+ бітів одночасно, щоб швидко визначити діапазони зайнятих індексів).
Це пов'язане рішення, як правило, найпростіше в застосуванні та не нав'язливе (не вимагає змін Foo
для збереження якогось removed
прапора), що корисно, якщо ви хочете узагальнити цей контейнер для роботи з будь-яким типом даних, якщо ви не заперечуєте, що 32-бітний накладні витрати на елемент.
Чи варто створити будь-який пул пам’яті для динамічного розподілу, чи не потрібно це турбуватися? Що робити, якщо цільовою платформою є мобільні пристрої?
потреба - це сильне слово, і я упереджено працюю в дуже важливих для продуктивності сферах, таких як проміння, обробка зображень, моделювання частинок та обробка сітки, але виділяти та звільняти маленькі предмети, які використовуються для дуже легкої обробки, як кулі, відносно дуже дорого. і частинки окремо проти розподільника пам'яті загального призначення, змінного розміру. Зважаючи на те, що ви повинні мати можливість узагальнити вищевказану структуру даних за день-два, щоб зберігати все, що завгодно, я думаю, що варто було б обмінятися, щоб усунути такі витрати на розподіл / розсилку купівлі прямо від сплати за кожну підліткову річ. Крім зменшення витрат на розподіл / дислокацію, ви отримуєте кращу локальність довідкової інформації, яка проходить за результатами (менше пропусків кешу та помилок сторінки, тобто).
Що стосується того, що Джош згадував про GC, я не вивчав реалізацію GC C # настільки близько, як Java, але у GC-розподільників часто є початкове виділенняце дуже швидко, тому що використовується послідовний розподільник, який не може звільнити пам'ять із середини (майже як стек, ви не можете видалити речі з середини). Тоді вона окупає дорогі витрати, фактично дозволяючи видаляти окремі об’єкти в окремий потік, копіюючи пам'ять і очищаючи раніше виділену пам'ять у цілому (наприклад, знищуючи весь стек одразу, копіюючи дані на щось більше, як пов'язана структура), але оскільки це робиться в окремому потоці, воно не обов'язково так сильно затримує нитки програми. Однак це несе в собі дуже значну приховану вартість додаткового рівня непрямості та загальної втрати ЛОР після початкового циклу ГК. Це ще одна стратегія прискорити розподіл - здешевити її в потоці виклику, а потім виконати дорогу роботу в іншій. Для цього вам потрібно два рівні непрямості для посилання на ваші об’єкти замість одного, оскільки вони в кінцевому підсумку перетасуються в пам'яті між часом, який ви спочатку виділите, і після першого циклу.
Ще одна стратегія подібного типу, яку трохи простіше застосувати в C ++, - це просто не турбуватися звільняти об'єкти в основних темах. Просто зберігаючи додавання та додавання та додавання до кінця структури даних, яка не дозволяє видаляти речі з середини. Однак позначте ті речі, які потрібно зняти. Тоді окремий потік може піклуватися про дорогу роботу зі створення нової структури даних без вилучених елементів, а потім атомним чином поміняти нову на стару, наприклад, велика частина витрат на виділення та звільнення елементів може бути передана на окремий потік, якщо ви можете зробити припущення, що запит на видалення елемента не повинен бути задоволений негайно. Це не тільки робить звільнення дешевшим, що стосується ваших потоків, але робить розподіл дешевшим, оскільки ви можете використовувати набагато простішу і тупішу структуру даних, якій ніколи не доводиться обробляти випадки видалення з середини. Це як контейнер, який потребує лишеpush_back
функція для вставки, clear
функція для видалення всіх елементів та розміщення swap
вмісту новим, компактним контейнером, виключаючи вилучені елементи; це все, що стосується мутування.