Статьи

Как Руби использует память

баран 2

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

Удержание объекта

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

RETAINED = [] 100_000.times do RETAINED << "a string" end 

Если мы запустим это и GC.stat(:total_freed_objects) с помощью GC.stat(:total_freed_objects) он вернет количество объектов, выпущенных Ruby. Запуск этого фрагмента до и после приводит к очень небольшим изменениям:

 # Ruby 2.2.2 GC.start before = GC.stat(:total_freed_objects) RETAINED = [] 100_000.times do RETAINED << "a string" end GC.start after = GC.stat(:total_freed_objects) puts "Objects Freed: #{after - before}" # => "Objects Freed: 6 

Мы создали 100 000 копий "a string" но, поскольку мы можем использовать эти значения в будущем, они не могут быть собраны мусором. Объекты нельзя собирать мусором, когда на них ссылается глобальный объект. Это касается констант, глобальных переменных, модулей и классов. Важно быть осторожным, ссылаясь на объекты из всего, что доступно глобально.

Если мы делаем то же самое без сохранения каких-либо объектов:

 100_000.times do foo = "a string" end 

Объекты освобождены, Objects Freed: 100005 : Objects Freed: 100005 . Вы также можете проверить, что объем памяти намного меньше, около 6 МБ по сравнению с 12 МБ при сохранении ссылки на объекты. Если хотите, измерьте это сами с самоцветом get_process_mem .

GC.stat(:total_allocated_objects) объекта может быть дополнительно проверено с использованием GC.stat(:total_allocated_objects) , где GC.stat(:total_allocated_objects) хранения равен total_allocated_objects - total_freed_objects .

Сохранение для скорости

Каждый в Ruby знаком с DRY или «Не повторяйся». Это верно как для распределения объектов, так и для кода. Иногда имеет смысл сохранить объекты для повторного использования, а не создавать их снова и снова. В Ruby эта функция встроена для строк. Если вы вызовете freeze для строки, интерпретатор узнает, что вы не планируете изменять эту строку, чтобы она могла остаться и использоваться повторно. Вот пример:

 RETAINED = [] 100_000.times do RETAINED << "a string".freeze end 

Запустив этот код, вы все равно получите Objects Freed: 6 , но использование памяти крайне мало. Проверьте это с помощью GC.stat(:total_allocated_objects) , только несколько объектов были выделены, поскольку "a string" сохраняется и используется повторно.

Вместо того, чтобы хранить 100 000 различных объектов, Ruby может хранить один строковый объект с 100 000 ссылок на этот объект. В дополнение к уменьшению памяти также уменьшается время выполнения, так как Ruby приходится тратить меньше времени на создание объектов и распределение памяти. Дважды проверьте это с помощью бенчмарк-ips , если хотите.

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

 RETAINED_REDIS_CONNECTION = Redis.new 

Поскольку константа имеет ссылку на соединение Redis, она никогда не будет собирать мусор. Интересно, что иногда, заботясь о сохраняемых объектах, мы можем фактически уменьшить использование памяти.

Короткоживущие объекты

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

 User.where(name: "schneems").first 

На "schneems" взгляд это выглядит так, как будто для работы требуется несколько объектов (хеш, символ :name и строка "schneems" . Однако, когда вы вызываете его, создается гораздо больше промежуточных объектов, чтобы сгенерировать правильный оператор SQL используйте подготовленный оператор, если он доступен, и т. д. Многие из этих объектов действуют только до тех пор, пока выполняются методы, в которых они были созданы. Почему мы должны заботиться о создании объектов, если они не будут сохранены?

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

Рубиновая память идет вверх

Когда вы используете больше объектов, чем Ruby может уместить в память, он должен выделить дополнительную память. Запрос памяти из операционной системы — дорогостоящая операция, поэтому Ruby пытается сделать это нечасто. Вместо того, чтобы запрашивать еще несколько КБ за раз, он выделяет больший кусок, чем ему нужно. Вы можете установить эту сумму вручную, установив переменную среды RUBY_GC_HEAP_GROWTH_FACTOR .

Например, если Ruby потреблял 100 МБ и вы установили RUBY_GC_HEAP_GROWTH_FACTOR=1.1 тогда, когда Ruby снова выделит память, он получит 110 МБ. При загрузке приложения Ruby оно будет увеличиваться на тот же процент, пока не достигнет плато, на котором вся программа может выполняться в пределах выделенного объема памяти. Более низкое значение для этой переменной среды означает, что мы должны запускать GC и распределять память чаще, но мы подойдем к максимальному использованию памяти медленнее. Чем больше значение, тем меньше GC, однако мы можем выделить гораздо больше памяти, чем нам нужно.

Ради оптимизации сайта многие разработчики предпочитают думать, что «Ruby никогда не освобождает память». Это не совсем так, поскольку Ruby освобождает память. Мы поговорим об этом позже.

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

 def make_an_array array = [] 10_000_000.times do array << "a string" end return nil end 

Когда мы вызываем этот метод, создается 10 000 000 строк. Когда метод завершается, эти строки ни на что не ссылаются и будут собираться мусором. Однако во время работы программы Ruby должен выделить дополнительную память, чтобы освободить место для 10 000 000 строк. Это требует более 500 МБ памяти!

Не имеет значения, если остальная часть вашего приложения умещается в 10 МБ, процессу потребуется 500 МБ ОЗУ, выделенной для построения этого массива. Хотя это тривиальный пример, представьте, что процессу не хватило памяти в середине действительно большого запроса страницы Rails. Теперь GC должен запустить и выделить больше памяти, если он не может собрать достаточно слотов.

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

Модификация на месте для скорости

Один прием, который я использовал для ускорения программ и сокращения выделения объектов, заключается в изменении состояния вместо создания новых объектов. Например, вот некоторый код, взятый из гема mime-types :

 matchdata.captures.map { |e| e.downcase.gsub(%r{[Xx]-}o, '') end 

Этот код принимает объект matchdata, возвращенный методом match регулярному выражению. Затем он генерирует массив каждого элемента, захваченного регулярным выражением, и передает его в блок. Блок делает строку строчной и удаляет некоторые вещи. Это выглядит как вполне разумный код. Тем не менее, его называли тысячи раз, когда требовался самоцвет mime-types . Каждый вызов метода downcase и gsub создает новый строковый объект, который требует времени и памяти. Чтобы избежать этого, мы можем сделать модификацию на месте:

 matchdata.captures.map { |e| e.downcase! e.gsub!(%r{[Xx]-}o, ''.freeze) e } 

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

Примечание: вам не нужно использовать константу для хранения регулярного выражения, так как все литералы регулярных выражений «заморожены» интерпретатором Ruby.

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

Было бы ошибкой думать, что «объекты медленные». Правильное использование объектов может упростить понимание и оптимизацию программы. Даже самые быстрые инструменты и методы, при неэффективном использовании, приведут к замедлению.

Хороший способ отловить ненужные выделения — это гем derailed_benchmarks на уровне приложения. На более низком уровне используйте гем alloc_tracer или гем memory_profiler .

Примечание: я написал гем derailed_benchmarks. Посмотрите на rake perf:mem для статистики памяти.

Хорошо быть свободным

Как упоминалось ранее, Ruby освобождает память, хотя и медленно. После запуска метода make_an_array который заставляет нашу память make_an_array , вы можете наблюдать освобождение Ruby, выполнив:

 while true GC.start end 

Очень медленно память приложения будет уменьшаться. Ruby выпускает небольшое количество пустых страниц (набор слотов) в то время, когда выделяется слишком много памяти. Вызов операционной системы для malloc , который в настоящее время используется для выделения памяти, также может освободить освобожденную память обратно операционной системе в зависимости от конкретной реализации библиотеки malloc для ОС.

Для большинства приложений, таких как веб-приложения, действие, вызвавшее выделение памяти, может быть инициировано нажатием на конечную точку. Когда конечная точка подвергается частым ударам, мы не можем полагаться на способность Руби освобождать память, чтобы поддерживать небольшой размер нашего приложения. Кроме того, освобождение памяти требует времени. Лучше минимизировать создание объектов в горячих точках, когда мы можем.

Вы Вверх

Теперь, когда у вас есть хорошая основа для понимания того, как Ruby использует память, вы готовы приступить к измерениям. Выберите некоторые из инструментов, которые я упомянул:

Затем выполните тестирование кода. Если вы не можете найти что-либо для сравнения, попробуйте воспроизвести мои результаты здесь. После того, как вы разберетесь с этим, попробуйте покопаться в собственном коде, чтобы найти горячие точки создания объектов. Возможно, в конечном итоге это будет что-то, что вы написали, или, может быть, это будет сторонний камень. Как только вы нашли точку доступа, попробуйте оптимизировать ее. Продолжайте повторять этот шаблон: находите горячие точки, оптимизируйте их и тестируйте. Пришло время приручить свой Рубин.


Если вам нравятся твиты о статистике памяти Ruby, следите за @schneems .