| Translations Blog |

Transparent PNG

Почему вы должны быть в восторге от сборки мусора в Ruby 2.0

Перевод статьи - Why You Should Be Excited About Garbage Collection in Ruby 2.0

Автор(ы) - Pat Shaughnessy

Источник оригинальной статьи:

http://patshaughnessy.net/2012/3/23/why-you-should-be-excited-about-garbage-collection-in-ruby-2-0

Хотя это и не очень гламурно, Сбор мусора с растровой маркировкой
- это драматическое, творческое новшество!

Возможно, вы слышали на прошлой неделе, как великая функция Enumerable::Lazy Иннокентия Михайлова была принята в кодовую базу Ruby 2.0. Но вы, возможно, не слышали о еще более существенном изменении, которое было объединено в Ruby 2.0 в январе: новый алгоритм сбора мусора под названием “Растровая маркировка.” Разработчик этого сложного и инновационного изменения, Нарихиро Накамура, работает над этим по крайней мере с 2008 года, а также реализовал алгоритм сборки мусора “Ленивой развертки”, уже включенный в Ruby 1.9.3. Новый алгоритм Bitmap Marking GC обещает значительно снизить общее потребление памяти всеми процессами Ruby, работающими на веб-сервере!

Но что же на самом деле означает “растровая маркировка”? И почему именно это уменьшит потребление памяти? Если вы знаете японский вы можете прочитать подробную академическую статью опубликованную в 2008 году Нарихиро Накамура вместе с Юкихиро ("Мац”) Мацумото. Мне было так интересно, что я провел некоторое время на этой неделе, изучая код сборки мусора в MRI Ruby, и в этой статье будет обобщено то, что я узнал. Сегодня вы не получите никаких советов по программированию на Ruby, но, надеюсь, вы лучше поймете, как на самом деле работает внутренняя сборка мусора, почему Ruby 2.0-это то, чего стоит ожидать, и насколько инновационными и творческими являются разработчики ядра Ruby.

Метка и развертка

Как я объяснил в своей статье от января, никогда не создавайте строки Ruby длиной более 23 символов, каждое строковое значение Ruby сохраняется внутренне с помощью MRI в структуре C под названием RString, сокращенно “Ruby String".” Каждая структура RString разделена на две половины, как это:

The RString structure

В нижней части у нас есть фактические строковые данные, в то время как в верхней части я показал слово “флаги”, чтобы представить различные значения внутренних метаданных о строке, которую отслеживает Ruby. Оказывается , что все значения , используемые вашей программой Ruby , сохраняются в аналогичных структурах, называемых RArray, RHash, RFile, т. Д. Все они имеют один и тот же базовый макет: некоторые данные и один и тот же набор флагов. Общее имя для этого типа структуры, которое является общим для всех внутренних типов объектов, - RValue - означает “Ruby Value".”

Ruby выделяет и организует эти структуры RValue в массивах, называемых “кучами".” Вот концептуальная схема массива Ruby heap, содержащая три строковых значения вместе со многими другими значениями RValue:

A Ruby heap

По мере выполнения программы Ruby всякий раз, когда вы создаете новую переменную или значение какого-либо типа, интерпретатор Ruby находит доступную структуру RValue в куче и использует ее для сохранения нового значения. Конечно, вам не нужно беспокоиться об этом вообще; все это обрабатывается автоматически и плавно для вас.

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

Когда больше нет доступных структур RValue и вашей программе нужно сохранить новое значение, Ruby запускает свой код “сборки мусора” (GC). Задача сборщика мусора состоит в том, чтобы найти, на какое из этих значений RValueбольше не ссылается ваша программа и может быть переработано и повторно использовано для какого-то другого значения. Вот как это работает, на высоком уровне....

Во-первых, код GC “помечает” все активные структуры RValue, то есть перебирает все переменные и другие активные ссылки, которые ваша программа имеет на структуры RValue, и помечает каждую из них с помощью одного из этих внутренних флагов, называемых FL_MARK.

The FL_MARK flag

Это первая половина алгоритма “Mark and Sweep” GC от Ruby. Отмеченные структуры активно используются вашей программой Ruby и не могут быть освобождены или повторно использованы.

Как только все структуры в системе помечены, остальные структуры “сметаются” в один связанный список с помощью указателя “next” в каждой структуре RValue: На этой схеме я показал флаги FL_MARK в массиве кучи с буквой “M”, а ниже вы можете увидеть список немаркированных RValue, называемый “свободным списком:”

The free list

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

Через некоторое время может оказаться, что в куче вообще не осталось немаркированных структур, что используются все доступные RValue, и в этом случае Ruby выделит целую новую кучу с большим количеством структур RValue. (На самом деле он выделяет новые кучи по 10 штук за раз.) Типичная программа Ruby может в конечном итоге иметь множество различных массивов кучи.

Copy-On-Write: как Unix разделяет память между различными дочерними процессами

Прежде чем мы перейдем к “Растровой маркировке” и объясним, почему это важно, нам сначала нужно узнать о функции Linux и других Unix и Unix-подобных операционных систем, которая связана с управлением памятью и распределением памяти: оптимизация копирования при записи. В этих ОС, когда процесс вызывает fork для создания дочернего процесса, который является копией самого себя, новый дочерний процесс будет совместно использовать всю память - все данные, переменные и т. д. Это делает вилку вызов намного быстрее, избегая ненужного копирования памяти, а также сокращая общий объем требуемой памяти.

Это называется “Copy-On-Write”, потому что отдельные копии сегмента общей памяти создаются, когда и если один из дочерних процессов пытается изменить общую память. Это похоже на трюк, который сам интерпретатор Ruby использует для управления значениями RString; подробнее об этом читайте в посте, который я написал в январе: Двойное видение: как Ruby делится строковыми значениями.

Чтобы лучше понять это, взгляните на эту концептуальную диаграмму процесса Ruby:

A Ruby process

Здесь я показал программу Ruby, которая имеет две кучи в качестве примера. Теперь предположим, что эта программа Ruby работает на веб - сервере - возможно, это веб-приложение Rails-и теперь второй HTTP-запрос поступает от другого пользователя:

Sharing memory

Теперь у нас работают два рубиновых процесса. Возможно, на этом сервере работает Apache с чем-то вроде Passenger, который развивает отдельный процесс Ruby для обработки каждого HTTP-запроса.

Хорошая вещь в оптимизации копирования при записи в Linux заключается в том, что многие структуры RValue в массивах кучи могут быть разделены между этими двумя программами Ruby, поскольку они часто содержат одни и те же значения. На первый взгляд может показаться, что это не так; почему многие - или любая - из переменных в двух программах Ruby должны быть одинаковыми? Но помните, что на веб-сервере вы фактически запускаете две или более копий одного и того же кода, создавая одни и те же переменные снова и снова. Кроме того, многие из RValue структуры в куче фактически соответствуют разбираемой версии самой программы Ruby - узлам в “Абстрактном синтаксическом дереве” (AST). Поскольку каждый процесс выполняет один и тот же код, все эти узлы будут иметь одинаковые значения и никогда не изменятся. Конечно, некоторые значения данных будут отличаться и будут сохраняться отдельно внутри каждого процесса - пользовательские данные, набранные в веб-формы и отправленные, результаты SQL-запросов по разным записям и т. д.

Но, как бы здорово это ни звучало, на самом деле это не работает для Руби!

Почему нет? Ну, потому что как только Ruby должен запустить алгоритм сборки мусора Mark & Sweep, который я объяснил выше, все эти узлы AST и многие другие структуры RValue в куче будут помечены, поскольку они все еще используются программой Ruby. Это означает, что они модифицируются для установки флага FL_MARK, и код копирования при записи в операционной системе должен начать создавать новые копии памяти. Так что на самом деле на типичном веб сервере Ruby происходит именно это:

Ruby doesn't share memory

Этот маленький кусочек FL_MARK сеет хаос! Это предотвращает то, что обычно было бы огромным сокращением использования памяти сервера на самом деле.

Одно важное замечание здесь: Хонгли Лай из Phusion, создатели популярного компонента промежуточного программного обеспечения Passenger, который соединяет Apache с Rack-приложениями Ruby, исправил Ruby 1.8 и создал новую версию Ruby, известную как Ruby Enterprise Edition, которая решает эту проблему и содержит ряд других улучшений производительности. Таким образом, на самом деле многие приложения Ruby 1.8, использующие REE, уже много лет могут использовать преимущества Unix Copy-On-Write. Но Copy-On-Write по-прежнему не работает со стандартным MRI Ruby 1.8 или 1.9.

Сборка мусора в Ruby 2.0: Растровая маркировка

Вот тут-то и вступают в игру изменения Нарихиро Накамуры для Ruby 2.0! Вместо того, чтобы использовать бит FL_MARK в каждой из структур RValue, чтобы указать, что Ruby все еще использует значение и что оно не может быть освобождено, Ruby 2.0 сохраняет эту информацию в так называемом “растровом изображении”. Нет... здесь “bitmap” не относится к файлу изображения; “bitmap” в этом контексте относится к литеральной коллекции битов, сопоставленных структурам RValue:

Bitmap marking

Для каждой кучи в Ruby 2.0 теперь существует соответствующая структура памяти, содержащая ряд 1 или 0 битовых значений. Как вы можете догадаться, значения 1 эквивалентны флагу FL_MARK, установленному в процессе Ruby 1.8 или Ruby 1.9, в то время как 0 эквивалентно флагу FL_MARK, который не установлен. Другими словами, биты FL_MARK были перемещены из RString и других структур значений объектов в эту отдельную область памяти, называемую bitmap.

Нарихиро реализовал это, добавив структуру заголовка в начало каждой кучи, которая содержит указатель на растровое изображение, соответствующее RValue этой кучи структуры, наряду с некоторыми другими ценностями. Это означает, что Ruby 2.0 теперь может пометить все используемые структуры во время “пометочной” части обработки GC, фактически не изменяя сами структуры, что позволяет Unix продолжать совместно использовать память между различными процессами Ruby! Сами растровые изображения, конечно, часто модифицируются Ruby 2.0, но поскольку они используют непрерывный поток битов, они на самом деле довольно малы и могут быть сохранены отдельно в каждом процессе, не используя слишком много памяти.

Одна интересная и важная деталь здесь заключается в том, что память, выделенная для кучи, теперь должна быть “выровнена".” Это означает, что при выделении памяти для кучи, вместо вызова malloc, как обычно, код Ruby C вызывает posix_memalign, который в операционной системе Linux или Unix возвращает новую память, выровненную по степени границы двух адресов.

Что, черт возьми, это значит? Ну а если вы знакомы с программированием на языке Си или побитовой арифметикой, то это позволяет коду Ruby C быстро вычислить местоположение структуры “заголовка”, содержащей указатель на растровое изображение, по адресу памяти данного объекта RValue. Давайте еще раз взглянем на кучу Ruby 2.0:

Memory alignment

Предположим, что код сборщика мусора Ruby 2.0 должен пометить пятый объект RValue в этой куче, на который ссылается значение ptr. Трюк выравнивания памяти позволяет Ruby 2.0 взять значение ptr и быстро вычислить адрес его структуры заголовка кучи. Все, что нужно сделать Ruby 2.0, это замаскировать последние несколько битов адреса RValue, шестнадцатеричное смещение “68” в этом примере, чтобы получить адрес структуры заголовка в “membase” или 0x80FFC000 в этом 32-битном примере.

Вывод

На первый взгляд сборка мусора-не самая гламурная или интересная часть языка Ruby, но, как мы уже видели, если вы внимательно посмотрите на то, как он работает, там происходит много интересных инноваций. Практически говоря, изменение растровой маркировки поможет MRI Ruby 2.0 лучше работать в производственных средах веб-серверов, значительно сократив потребление памяти. Но я рассматриваю растровую маркировку не столько как практическое улучшение, которое поможет моим приложениям Rails работать лучше, а скорее как захватывающее, творческое решение сложной проблемы. Было очень весело узнать, как GC работает в Ruby 2.0, и я надеюсь, что теперь вы лучше оцените всю тяжелую работу талантливой команды Ruby core!