Отчасти повторяю предложение Килотана, но я бы порекомендовал решить это на уровне структуры данных, когда это возможно, а не на более низком уровне распределителя, если вы можете помочь.
Вот простой пример того, как вы можете избежать 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 вручную, чтобы избежать требования для назначения копирования, заставить его вызывать деструкторы при удалении элементов, предоставлять прямой итератор и т. Д. предпочел сохранить пример в стиле C, чтобы более четко проиллюстрировать концепцию, а также потому, что я очень ленив.
Тем не менее, эта структура имеет тенденцию ухудшаться в пространственной локализации после того, как вы удалите и вставите объекты в середину или из нее. В этот момент next
ссылки могут заставить вас идти вперед и назад по вектору, перезагружая данные, ранее удаленные из строки кэша, в рамках одного и того же последовательного обхода (это неизбежно при любой структуре данных или распределителе, которая позволяет удалять данные в постоянном времени без перетасовки элементов при возврате пробелы от середины с постоянной вставкой и без использования чего-либо вроде параллельного набора битов или removed
флага). Чтобы восстановить дружественность кешу, вы можете реализовать метод копирования ctor и 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 часто имеют начальное распределениеэто очень быстро, потому что используется последовательный распределитель, который не может освободить память из середины (почти как стек, вы не можете удалять вещи из середины). Затем он оплачивает дорогостоящие затраты, чтобы фактически разрешить удаление отдельных объектов в отдельном потоке путем копирования памяти и очистки ранее выделенной памяти в целом (например, уничтожение всего стека за один раз при копировании данных во что-то более похожее на связанную структуру), но поскольку это делается в отдельном потоке, это не обязательно останавливает потоки вашего приложения. Однако это несет в себе очень значительную скрытую стоимость дополнительного уровня косвенности и общей потери LOR после начального цикла GC. Это еще одна стратегия ускорения выделения ресурсов - сделать его дешевле в вызывающем потоке, а затем выполнить дорогостоящую работу в другом. Для этого вам нужно два уровня косвенности, чтобы ссылаться на ваши объекты, а не один, так как они будут в конечном итоге перетасовываться в памяти между временем, которое вы изначально выделяли, и после первого цикла.
Еще одна стратегия в том же духе, которую немного проще применить в C ++, - просто не пытайтесь освободить ваши объекты в основных потоках. Просто добавление и добавление и добавление в конец структуры данных, которая не позволяет удалять объекты из середины. Однако отметьте те вещи, которые нужно удалить. Затем отдельный поток мог бы позаботиться о дорогостоящей работе по созданию новой структуры данных без удаленных элементов, а затем атомарно заменить новую на старую, например, большая часть затрат как на выделение, так и на освобождение элементов может быть передана на отдельный поток, если вы можете сделать предположение, что запрос на удаление элемента не должен выполняться немедленно. Это не только делает освобождение более дешевым, насколько это касается ваших потоков, но и делает распределение более дешевым, поскольку вы можете использовать гораздо более простую и тупую структуру данных, которая никогда не должна обрабатывать случаи удаления из середины. Это как контейнер, который нуждается только вpush_back
функция для вставки, clear
функция для удаления всех элементов и swap
обмена содержимым с новым компактным контейнером, исключая удаленные элементы; это все, что касается мутаций.