Pull to refresh

Эволюция сборщика мусора в Ruby. RGenGC

Reading time 5 min
Views 17K
Коити: Порог срабатывания сборщика мусора в Ruby — 8 МБ. Почему используется такое маленькое значение?
Matz: Потому что 20 лет назад я работал на машине с 10 МБ памяти.

Вопрос производительности всегда был одним из наиболее обсуждаемых и актуальных в Ruby-сообществе. Будь то высоконагруженный веб-сайт или простой скрипт по бекапу данных — скорость работы является их важнейшей характеристикой. При этом знание возможностей и ограничений языка разработки зачастую служит важным источником идей для оптимизации, позволяет «выжать» максимум из системы.

В статье речь пойдет об одной из наиболее сильно влияющих на производительность частей языка Ruby — сборщике мусора, алгоритмах его работы и улучшениях, внесенных в его работу в последних версиях языка. Речь пойдет о наиболее распространенной, «канонической» реализации Ruby — так называемой MRI или CRuby.

Основы


Сборка мусора (Garbage Collection; GC) в языках программирования — механизм, обеспечивающий автоматическое управление памятью без вмешательства программиста. GC не является специфической особенностью Ruby — подобный механизм используется в значительной части современных языков разработки: Java, Python, C# и других. В MRI используется классический Mark-and-Sweep алгоритм сборки мусора, разработанный в далеком 1960 году.

Ruby использует «кучу» для выделения памяти под новые объекты. Процесс создания объектов прерывается при наступлении одного из двух условий: память программы закончилась или достигнут определенный порог выделения памяти — так называемый malloc_limit. В этот момент MRI запускает сборку мусора, точнее ее первую, «mark» фазу.
image

Сборщик мусора проходит дерево объектов, находящихся в памяти программы, начиная с «корневых» объектов — это глобальные переменные или внутренние структуры языка. Пробегая по ссылкам объектов друг на друга, сборщик мусора помечает объекты как используемые. Все данные, не помеченные как используемые в mark-фазе — «мусор» и память, которую они занимают, может быть освобождена. Освобождение памяти от «мусора» происходит во второй, sweep-фазе GC.

Оптимизация сборщика мусора. Ruby 1.9.3 и 2.0


Классический Mark-and-Sweep алгоритм имеет ряд недостатков. Прежде всего это значительные паузы в работе программы, на время которых выполнение полезного пользовательского кода прекращается, а работает сборщик мусора. Чем длиннее такие «stop-the-world» паузы, тем медленнее работает программа с точки зрения конечного пользователя. Для уменьшения длительности пауз, затрачиваемых на сборку мусора, в Ruby 1.9.3 был реализован алгоритм Lazy Sweep. Начиная с этой версии MRI sweep-этап сборки мусора не заканчивается полным освобождением памяти от ненужных объектов, а освобождает только то количество памяти, которое необходимо для продолжения выполнения программы — для создания нового объекта. При создании следующего объекта опять производится некоторое освобождение памяти и так далее.

В Ruby 2.0 было интегрировано еще одно улучшение сборщика мусора — Bitmap Marking GC. Этот алгоритм направлен на Unix системы, в которых при создании дочерних процессов при помощи fork используется copy-on-write механизм. Порожденный процесс создается быстро, поскольку его память является лишь «отображением» памяти соответствующего родительского процесса. Реальное разделение памяти родительского и дочернего процесса происходит после записи каких-либо данных в общую область памяти. Многие популярные Ruby-библиотеки используют fork, например популярный сервер приложений Unicorn и библиотека для выполнения фоновых задач Resque. Однако классический сборщик мусора в MRI плохо сочетался с семантикой fork, поскольку во время mark-фазы устанавливал флаг «используемости» у каждого объекта, тем самым модифицируя значительную часть «кучи», фактически нивелируя тем самым преимущества copy-on-write механизма для Ruby-процессов. В Ruby 2.0 флаги, используются ли объекты или нет, были вынесены в отдельную структуру — битовую маску, хранящуюся независимо от самих объектов. Это позволило значительно увеличить объем памяти, разделяемый процессами при fork и лучше использовать преимущества copy-on-write семантики.

malloc_limit. Проблема 8 МБ.


Важнейшей характеристикой сборщика мусора является параметр malloc_limit — порог выделения памяти, после которого запускается GC. Однако значение этой характеристики по-умолчанию было крайне небольшим — 8 МБ. В современных системах обработка запроса на веб-сайте может приводить к выборке десятков мегабайт данных из базы или чтению больших файлов. При этом сборка мусора производится слишком часто, снижая скорость работы программы.

Отчасти для решения этой проблемы разработчиками серверов приложений предпринимались попытки сделать поведение сборщика мусора более предсказуемым и свести к минимуму влияние GC на скорость обработки HTTP-запросов. Это привело к появлению таких технологий, как Unicorn OobGC и Passenger Out-of-Band Work. Оба решения отключают сборку мусора на время обработки запроса и запускают его принудительно после того, как запрос обработан сервером. Такой механизм также не лишен недостатков: GC может запускаться чаще, чем нужно, если запросы «легковесные», либо, наоборот — процесс может «съесть» слишком много памяти.

Функционал malloc_limit был пересмотрен в Ruby 2.1 — порог срабатывания GC стал адаптироваться к поведению приложения, а значение по-умолчанию было увеличено до 16 МБ. Теперь malloc_limit автоматически изменяется при массивном выделении памяти, в результате чего сборщик мусора запускается значительно реже.
image

Ruby 2.1. Поколения объектов


Релиз Ruby 2.1 был ознаменован значительными изменениями в алгоритме работы GC. В язык был интегрирован сборщик мусора на основе поколений. Его работа основана на гипотезе о том, что большинство объектов, создаваемых в программе — «умирают молодыми». Таким образом, основная деятельность сборщика мусора связана с объектами, имеющими короткий жизненный цикл.

В Ruby память разбита на 2 поколения объектов — поколение молодых объектов и старшее поколение, к которому относятся объекты, пережившие хотя бы одну сборку мусора. В большинстве случаев сборка мусора осуществляется только внутри молодого поколения. Лишь в момент, когда память заканчивается — выполняется полная сборка мусора с участием обоих поколений.

Важной проблемой при таком подходе становятся обратные ссылки с объектов старшего поколения на объекты молодого поколения.
image

Если не учитывать возможность возникновения таких ссылок, minor GC — сборка мусора внутри младшего поколения может пометить как мусор реально нужные, использующиеся объекты. Для решения этой проблемы используется специальная структура — Remember Set, в которой запоминаются проблемные ссылки. Для определения факта появления таких ссылок используется барьер на чтение в Ruby-интерпретаторе. «Взрослые» объекты, с которых имеются ссылки на объекты младшего поколения используются как корневые при minor-сборке мусора.

Значительным ограничением сборки мусора в MRI является необходимость сохранения обратной совместимости со всеми многочисленными C-расширениями, разработанными для языка. Именно поэтому алгоритм GC в Ruby носит название RGenGC — Restricted Generational GC. «Ограниченность» сборки мусора заключается в том, что все объекты делятся на 2 типа: shady-объекты — это объекты, которые используются или потенциально могут быть использованы в C-расширениях. Соответственно сборщик мусора не может безопасно перемещать их между поколениями. Если такой объект попадет в старшее поколение, он не будет защищен барьером на чтение, поскольку прежняя семантика языка не требовала от C-расширений использования таких барьеров. В результате могут возникнуть «проблемные» ссылки, описанные выше.

Shady-объекты не участвуют в поколениях сборки мусора, между поколениями мигрируют только обычные объекты. Такое решение позволило сохранить совместимость с существующими C-библиотеками и упростило разработку сборщика мусора. Однако, в свою очередь, оно исключает из работы нового сборщика мусора значительное число объектов. Тем не менее, по измерениям разработчиков RGenGC, ускорение работы приложений при использовании нового сборщика мусора составляет порядка 10%.

Планы на будущее


Сборщик мусора — одна из наиболее динамично развивающихся частей языка. В ближайших планах — доведение количества поколений языка до трех. Из более глобальных вещей планируется введение параллелизма в mark и sweep фазы сборки мусора, что позволит лучше использовать возможности современных многоядерных процессоров.

Дополнительные источники информации

Презентация Коити Сасада на RubyConf 2013
Книга Ruby Under a Microscope (2013)
Tags:
Hubs:
+50
Comments 20
Comments Comments 20

Articles