Безопасный пул памяти

Мое приложение в настоящее время имеет критическую производительность и запрашивает 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 наносекунд для одной и той же пары операций...