Потокобезопасный пул памяти - PullRequest
12 голосов
/ 21 апреля 2011

Мое приложение в настоящее время крайне критично к производительности и запрашивает 3-5 миллионов объектов на кадр.Изначально, чтобы все заработало, я new'd все и получил приложение для работы и тестирования моих алгоритмов.Приложение многопоточное.

Когда я был доволен производительностью, я начал создавать диспетчер памяти для своих объектов.Очевидная причина - фрагментация и потеря памяти.Приложение не может продолжаться более нескольких кадров до сбоя из-за фрагментации памяти.Я проверил на утечки памяти и знаю, что приложение без утечек.

Итак, я начал создавать простой диспетчер памяти, используя TBB concurrent_queue.В очереди хранится максимальный набор элементов, которые разрешено использовать приложению.Класс, требующий новых элементов, выталкивает элементы из очереди.Метод try_pop, согласно документации Intel, не блокируется.Это работало довольно хорошо, поскольку потребление памяти идет (хотя есть все еще фрагментация памяти, но не так много, как прежде).Проблема, с которой я сейчас сталкиваюсь, заключается в том, что производительность приложения снизилась примерно в 4 раза по сравнению с моим собственным простым профилировщиком (у меня нет доступа к коммерческим профилировщикам или я не знаю ни одного, который будет работать с приложением реального времени ... любая рекомендациябудет оценено).

Мой вопрос: существует ли потокобезопасный пул памяти, который можно масштабировать?Особенностью must-have пула является быстрая переработка элементов и обеспечение их доступности.Если их нет, какие-нибудь советы / хитрости по производительности?

РЕДАКТИРОВАТЬ: Я думал, что объясню проблему немного больше.Я мог бы легко инициализировать n число массивов, где n - количество потоков, и начать использовать объекты из массивов на поток.Это будет отлично работать в некоторых случаях.В моем случае я также перерабатываю элементы (возможно, каждый кадр), и они могут быть переработаны в любой точке массива;то есть это может быть elementArray[0] или elementArray[10] или elementArray[1000] часть массива.Теперь у меня будет фрагментированный массив элементов, состоящий из готовых к использованию элементов и используемых элементов: (

Ответы [ 3 ]

5 голосов
/ 21 апреля 2011

Как сказано в комментариях, не получайте поточно-ориентированный распределитель памяти, выделяйте память для каждого потока.

Как вы подразумевали в своем обновлении, вам необходимо эффективно управлять свободным / используемым. Это довольно простая проблема, учитывая постоянный тип и отсутствие параллелизма.

Например (с головы не проверено):

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.

Обновление:

Новое требование для отслеживания вещей, которые были использованы, чтобы их можно было обработать за второй проход, мне кажется немного неясным. Я думаю, что вы имеете в виду, что, когда первичная обработка завершена на объекте, вам нужно не освобождать его, а сохранять ссылку на него для обработки второго этапа. Некоторые объекты вы просто будете возвращены в пул и не будете использоваться для обработки второго этапа.

Полагаю, вы хотите сделать это в той же теме.

В качестве первого прохода вы можете добавить метод, подобный этому, в 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 голосов
/ 21 апреля 2011

Google's TCMalloc ,

TCMalloc назначает каждому потоку локальный кэш-поток.Небольшие выделения выполняются из локального кеша потока.При необходимости объекты перемещаются из центральных структур данных в локальный кэш-поток, а периодические сборки мусора используются для переноса памяти из локального кэш-потока в центральные структуры данных.

Производительность:

TCMalloc быстрее, чем glibc 2.3 malloc ... ptmalloc2 требует приблизительно 300 наносекунд для выполнения пары malloc / free на P4 2,8 ГГц (для небольших объектов).Реализация TCMalloc занимает примерно 50 наносекунд для одной и той же пары операций ...

2 голосов
/ 21 апреля 2011

Возможно, вы захотите взглянуть на jemalloc .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...