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

в 7:46, , рубрики: c++, Блог компании Петер-Сервис, многопоточное программирование, Программирование

Как-то, анализируя дефект в разрабатываемом продукте, я наткнулся на архитектурную особенность менеджера памяти, который мы использовали. Дефект приводил к увеличению времени создания некоторых объектов. Особенность архитектуры заключалась в использовании паттерна Singleton при работе с менеджером памяти (далее X allocator). Схематично это выглядит так:

image
Рисунок 1 – Структурная схема работы X allocator

Из схемы видно, что доступ к глобальной куче защищен мьютексом. Такая архитектура, при интенсивном создании однотипных объектов из нескольких потоков, может привести к тому, что потоки будут вставать в очередь на этом мьютексе. А ведь одна из главных особенностей продукта – это возможность его масштабирования за счет увеличения количества потоков обработки (потоков выполняющих одинаковые действия). Поэтому такой подход потенциально может стать узким местом.

Осознав масштаб угрозы, я решил просветиться и полез в Интернет искать информацию про многопоточные менеджеры памяти (multi-threaded pooled allocators). Пока искал, наткнулся на несколько проектов:
• Hoard (http://www.hoard.org/), распространяемый под лицензией GPL v.2.0;
• TBB Memory Allocator от Intel (отбросил, т.к. библиотека платная);
• Thread-Caching Malloc из состава gperftools от Google (https://code.google.com/p/gperftools/).

Также в процессе поиска нашел очень интересную статью, в которой описываются различные подходы при работе с динамической памятью – www.realcoding.net/article/view/2747.

Имея на руках несколько решений, я решил поэкспериментировать: сравнить производительность различных менеджеров памяти и подсмотреть решение для оптимизации X allocator.

Когда я проанализировал реализацию Hoard и TCMalloc, у меня сложилось понимание, что реализованные в них менеджеры памяти используют кеширование по потокам (альтернатива такого подхода – использование lock-free списков). Кеширование позволяет при удалении объекта возвращать память не в глобальную кучу, а помещать в кэш этого потока. Так что при повторном создании такого же объекта потоку не придется лезть под мьютекс в общую кучу. Схематично этот подход можно изобразить следующим образом (рисунок 2):

image
Рисунок 2 – Структурная схема работы X allocator с использование кеширования потоков

Реализовав на коленке кеширование в X allocator (далее pooled X allocator), я приступил к сравнению производительности.

Исследование поведения менеджеров памяти проводилось при интенсивном потреблении CPU. Тест заключался в создании X объектов на N потоках, при этом проверялась гипотеза, что время выполнения теста обратно пропорционально количеству потоков. Пример для идеального мира: создание 1000 объектов на 1 потоке будет выполняться в 2 раза дольше, чем создание 1000 объектов на 2х потоках (по 500 объектов на поток).

Проводимые тесты можно разбить по следующим категориям:
1. Тип ОС:
a. Windows;
b. Linux.
2. Размер объектов:
a. 128b;
b. 1Kb;
c. 1Mb.
3. Менеджер памяти:
a. системный менеджер памяти;
b. TCMalloc (Google);
c. Hoard;
d. pooled X allocator (заменил X allocator singleton на массив X allocator’ов, при выделении и освобождении памяти номер X allocator’а высчитывается по tid потока).
e. X allocator;
4. Количество потоков: от 1 до 8;
5. Каждый эксперимент я проводил по 5 раз, после рассчитал среднее арифметическое значение.

Ниже приведу только один график (рисунок 3) и результаты эксперимента для него (таблица 1), так как для всех случаев результаты получились похожими. Результаты приведены для ОС Linux объекты размером 1 Kb.

image
Рисунок 3 – Результаты исследования производительности менеджеров памяти

На графике ось абсцисс – это количество потоков, ось ординат – это относительное время выполнения теста (за 100% берется время выполнения теста на одном потоке с использованием системного менеджера памяти).

Таблица 1 — Результаты исследования производительности менеджеров памяти
image

По полученным результатам можно сделать следующие выводы:
1. X allocator, который мы используем, показал хорошую производительность только на 1 потоке, при увеличении количества потоков производительность падает (из-за конкуренции на мьютексе);
2. остальные менеджеры памяти справились с задачей распараллеливания, действительно, при увеличении количества потоков задача стала выполняться быстрее;
3. TCMalloc показал лучшую производительность.

Конечно, я понимал, что эксперимент искусственный и в реальных условиях рост производительность не будет таким же значительным (некоторые тесты показали рост производительности в 80 раз). Когда я заменил в своем проекте X allocator на TCMalloc и провел эксперимент на реальных данных, увеличение производительности составило 10%, и это достаточно хороший прирост.
Производительность – это, конечно, замечательно, но X allocator предоставляет возможность анализировать потребляемую память и искать утечки. А как с этим обстоят дела у остальных менеджеров памяти? Это сравнение я постараюсь подробно раскрыть в следующей статье.

Автор: MI7

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js