понедельник, июня 29, 2015

Фрагментация памяти в C++ приложениях

Поскольку памятью в С++ приложении программист управляет самостоятельно, то и за фрагментацией памяти приходится следить самим. Память фрагментируется, если приложение работает достаточно долго и при этом активно работает с памятью. Не такая редкая проблема. Я это видела в играх, игрок бегает, юнитов убивает, локации меняет, надо под это память выделять и потом освобождать. И в высоконагруженных системах, запросы приходят, надо под них память выделять, потом освобождать.

Чем это плохо? Во-первых, память может внезапно закончиться. Во-вторых может просесть производительность. Вот тут можно почитать историю как это выглядит на практике: Out of memory

Вот тут с картинками написано как оно происходит: Holier Than Thou
Но не написано - а что с этим делать-то? Итак, с чем я работала.

Region-based memory management. Выделяем память большим куском, например, под уровень в игре.  Популярный термин для такого куска - "арена". Потом уже внутри этого куска создаем нужные нам объекты. Когда все это стало не нужно - весь кусок памяти освобождаем. Заодно с утечками памяти проблем меньше.
Арену можно использовать вместе с STL, см. "STL custom allocators".


Boost.Pool. Там есть несколько разных интерфейсов, я использовала object_pool, который помогает эффективнее создавать и удалять большое количество сравнительно маленьких объектов.

Также я видела разное креативное использование placement new, как правило его можно заменить Boost.Pool'ом.

10 коммент.:

afiskon комментирует...

А как же слаб алокейторы?

Alena комментирует...

afiskon

А как же слаб алокейторы?

Это нечто Linux-специфичное, а я-то под Windows пишу. По фунциональности это примерно то же, что и object_pool.

Ni@m комментирует...

SLAB это не Linux-specific(вообще впервые появился в Solaris).
Насколько я понимаю Boost.Pool довольно нетривиально использовать в контексте singleton так как вручную приходится освобождать выделенную pool'ом память. И если я правильно понимаю то будет создано 2 независимых pool'а для int и bool, и что еще хуже, для int и struct {int}.
Я бы рекомендовал использовать jemalloc. Вроде есть сборки и под MS Windows.

moriarty комментирует...

slab / arena / pool дают отличный результат, если система изначально строилась с учетом кастомных аллокаторов. Если есть уже работающий код с, например, STL контейнерами (дающими иллюзию легкой кастомизации аллокаторов), то кроме jemalloc можно попробовать tcmalloc.
Для полноты картины надо упомянуть, что, кажется, с седьмой винды, (2013 сервер), в винде low fragmentation heap включен по дефолту, и он старается делать примерно то же, что и jemalloc / tcmalloc. Только имплементация у него закрытая, на статистику его не взглянешь, и я знаю как минимум один большой виндовый проект, на котором tcmalloc в тестах показал себя сильно лучше.

Кирилл комментирует...

В Windows есть Low-fragmentation Heap, которая весьма неплохо справляется с фрагментацией. Бороться с фрагментацией приходится только в весьма специфичных случаях. Хотя куча в таком режиме по умолчанию работает в Vista, а в более ранних версиях ее можно включить, но реально хорошо это работает начиная с Windows 7. Поэтому в большинстве случаев лучше не пытаться решать проблему фрагментации, пока не ясно, что она реально будет проявляться. Это просто потому, что может выйти хуже, чем если ничего не делать.

Win7 вообще довольно удачная вышла в плане разных плюшек. Тот же пул потоков в Windows 7 работает гораздо лучше, чем в Windows XP. Просто потому, что в WinXP он не умеет адекватно использовать более 2-х ядер процессора.

Kirill V. Lyadvinsky комментирует...

И еще хотел бы отметить насчет того, что не только С++ программист управляет памятью самостоятельно. При разработке серьезных приложений, в общем случае неплохо бы понимать что происходит за кулисами. Создавая программы на C# тоже можно легко попасть в неприятную ситуацию с памятью.

Анонимный комментирует...

tcmalloc неплохая вещь, некоторый эффект даёт.

Анонимный комментирует...

Есть такая проблема. Чем то подобным занимался года полтора назад.
На самом деле правильно ее понимать не как проблему фрагментации памяти,
а как проблему фрагментации адресного пространства процесса, когда операционка
тебе может еще выделить физические страницы а непрерывных номерков
в адресном пространстве уже не найти куда это отобразить чтоб выделить
непрерывный кусок необходимой длины.

В общем это больше проблема x32 легаси приложений, которые по каким то причинам пока
не получается перевести на x64.
как я понимаю для x64 приложений такой проблемы быть не должно.
Представте себе что у вас изза фрагментации на x32 адресном пространстве иногда заканчиваются
непрерывные номерки. А теперь представьте себе что вы работаете с x64 адресным пространством,
где адресного пространства в 4 миллиарда раз больше.

Если всетаки вынуждены работать с x32 и есть такая проблема, то есть рекомендации
"хранить" отельно объекты с разным временем жизни. То есть отделять долгоживущие от
короткоживущих в адресном пространстве. Для этого можно завести отдельную кучу например и хранить
там всю долгоживущую мелочь к примеру чтоб она не била пространство где живут к примеру
короткоживущие большие объекты. Использование разных куч как то гарантирует разделенность
в пространстве разных по природе объектов.

упомянутый выше LFH флаг помог только в плане увеличить производительность аллокаций на win 2003 server (x32)
когда на "свежей" куче загрузка отрабатывала скажем за 5 сек, а потом уже тупила 20-40-60 сек....

Kirill V. Lyadvinsky комментирует...

Поскольку процессоры Intel имеют 42-битную адресную линию, то теоретический размер виртуального адресного пространства "всего" 256 терабайт. При этом обычные выпуски Windows разрешают только 8 терабайт виртуального пространства на процесс. Серверные и Win 8.1 - 128 терабайт.

Учитывая, что обрабатываемые объемы данных растут, то фрагментировать доступные лимиты - дело времени.

Victor комментирует...

В свое время потратил много времени на исследование распределителей. В общем случае лучше всего с проблемой справляется jemalloc. Boost pool вам поможет только если вы используете один поток, если-же нет - то синхронизация "влоб" через критические секции или еще куда хуже мьютексы уничтожит производительность приложения. Boost Pool по-сути работает хорошо если его использовать как allocator для STL контейнера (или обойтись C++ 17 http://en.cppreference.com/w/cpp/experimental/polymorphic_allocator). Альтернативно можно "поиграться" с неблокирующими алгоритмами как я https://github.com/incoder1/small-object-allocator