Безопасный пул памяти
Мое приложение в настоящее время имеет критическую производительность и запрашивает 3-5 миллионов объектов на фрейм. Первоначально, чтобы получить мяч, я new'd
все и получил приложение для работы и тестирования моих алгоритмов. Приложение многопоточное.
Как только я был доволен производительностью, я начал создавать диспетчер памяти для своих объектов. Очевидная причина - фрагментация памяти и потери. Приложение не могло продолжить работу более чем на несколько кадров перед сбоем из-за фрагментации памяти. Я проверил утечку памяти и знаю, что приложение не содержит утечек.
Итак, я начал создавать простой диспетчер памяти, используя TBB concurrent_queue
. В очереди хранится максимальный набор элементов, которые разрешено использовать приложению. Класс, требующий новых элементов, выставляет элементы из очереди. Метод try_pop
, согласно документации Intel, не блокируется. Это работало достаточно хорошо, поскольку потребление памяти идет (хотя фрагментация памяти еще не так много, как раньше). Проблема, с которой я столкнулся сейчас, заключается в том, что производительность приложения замедлилась примерно в 4 раза в соответствии с моим собственным простым профилировщиком (у меня нет доступа к коммерческим профайлерам или знаю о том, что будет работать в приложении реального времени... любая рекомендация будет оценена).
Мой вопрос: есть ли потокобезопасный пул памяти, который является масштабируемым. A must-have
Особенность пула - быстрая утилизация элементов и их доступность. Если их нет, можно ли использовать некоторые советы/трюки?
EDIT: Я думал, что я объясню проблему немного больше. Я мог бы легко инициализировать n количество массивов, где n - количество потоков и начать использовать объекты из массивов на поток. В некоторых случаях это будет отлично работать. В моем случае я повторно использую элементы (потенциально каждый кадр), и они могут быть переработаны в любой точке массива; то есть может быть от elementArray[0]
или elementArray[10]
или elementArray[1000]
части массива. Теперь у меня будет фрагментированный массив элементов, состоящий из элементов, которые готовы к использованию, и элементов, которые используются: (
Ответы
Ответ 1
Как сказано в комментариях, не получите потокобезопасный распределитель памяти, выделяйте память на поток.
Как вы подразумевали в своем обновлении, вам необходимо эффективно управлять свободными/эффективными. Это довольно простая проблема, учитывая постоянный тип и не concurrency.
Например (с верхней части головы, непроверенный):
template<typename T>
class ThreadStorage
{
std::vector<T> m_objs;
std::vector<size_t> m_avail;
public:
explicit ThreadStorage(size_t count) : m_objs(count, T()) {
m_avail.reserve(count);
for (size_t i = 0; i < count; ++i) m_avail.push_back(i);
}
T* alloc() {
T* retval = &m_objs[0] + m_avail.back();
m_avail.pop_back();
return retval;
}
void free(T* p) {
*p = T(); // Assuming this is enough destruction.
m_avail.push_back(p - &m_objs[0]);
}
};
Затем для каждого потока используйте экземпляр ThreadStorage и вызовите alloc() и free() по мере необходимости.
Вы можете добавить интеллектуальные указатели для управления вызовом free() для вас, и вы можете оптимизировать вызов конструктора/деструктора, если это дорого.
Вы также можете посмотреть boost:: pool.
Update:
Новое требование для отслеживания вещей, которые были использованы, чтобы их можно было обработать на втором проходе, кажется мне неясным. Я думаю, вы имеете в виду, что, когда первичная обработка завершена на объекте, вам не нужно ее выпускать, но сохраните ссылку на нее для обработки второй ступени. Некоторые объекты вы просто будете выпущены обратно в пул и не будут использоваться для обработки второго этапа.
Я предполагаю, что вы хотите сделать это в том же потоке.
В качестве первого прохода вы можете добавить такой метод в ThreadStorage и вызвать его, когда хотите выполнять обработку во всех неизданных экземплярах T. Нет необходимости в дополнительном ведении бухгалтерского учета.
void do_processing(boost::function<void (T* p)> const& f) {
std::sort(m_avail.begin(), m_avail.end());
size_t o = 0;
for (size_t i = 0; i != m_avail.size(); ++i) {
if (o < m_avail[i]) {
do {
f(&m_objs[o]);
} while (++o < m_avail[i]);
++o;
} else of (o == m_avail[i])
++o;
}
for (; o < m_objs.size(); ++o) f(&m_objs[o]);
}
Предполагает, что ни один другой поток не использует экземпляр ThreadStorage, что разумно, потому что он является поточно-локальным по дизайну. Опять же, с моей головы, непроверенный.
Ответ 2
Возможно, вы захотите посмотреть jemalloc.
Ответ 3
Google TCMalloc,
TCMalloc назначает каждый поток a поток-локальный кеш. Небольшие ассигнования выполняются из локально-потокового кэш. Объекты перемещаются из центра структуры данных в поток-локальный кеш при необходимости, и периодический мусор коллекции используются для переноса памяти назад из локального кэша потоков в центральные структуры данных.
Производительность:
TCMalloc быстрее, чем glibc 2.3 malloc... ptmalloc2 занимает приблизительно 300 наносекунд, чтобы выполнить пару malloc/free на 2.8 ГГц P4 (для небольших объектов). Реализация TCMalloc занимает примерно 50 наносекунд для одной и той же пары операций...