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

Среди популярных сегодня языков программирования выделение и освобождение памяти в C и C ++ выполняется вручную программистами. В других языках, таких как Java, Python и Ruby, память освобождается (когда объекты выходят за пределы области видимости) сборщиком мусора.

Знакомство со сборкой мусора Ruby - Ruby GC:

Что такое сборщик мусора? GC - это модуль Ruby, который предоставляет интерфейс к механизму сборки мусора Ruby (в частности: механизм маркировки и очистки начиная с Ruby 2.2). Два наиболее часто используемых и важных метода - это GC.stat и GC.start.

GC.start по умолчанию инициирует сборку мусора, т. Е. Освобождает память.

GC.stat показывает статистику сборщика мусора - если мы запустим GC.stat в irb, пример результата будет:

{ 
 :count=>9, 
 :heap_allocated_pages=>132, 
 :heap_sorted_length=>133, 
 :heap_allocatable_pages=>0, 
 :heap_available_slots=>53798, 
 :heap_live_slots=>53585, 
 :heap_free_slots=>213, 
 :heap_final_slots=>0, 
 :heap_marked_slots=>23105, 
 :heap_swept_slots=>13998, 
 :heap_eden_pages=>132, 
 :heap_tomb_pages=>0, 
 :total_allocated_pages=>132, 
 :total_freed_pages=>0, 
 :total_allocated_objects=>164009, 
 :total_freed_objects=>110424, 
 :malloc_increase_bytes=>389720, 
 :malloc_increase_bytes_limit=>16777216, 
 :minor_gc_count=>6, 
 :major_gc_count=>3, 
 :remembered_wb_unprotected_objects=>210, 
 :remembered_wb_unprotected_objects_limit=>394, 
 :old_objects=>20937, 
 :old_objects_limit=>37436, 
 :oldmalloc_increase_bytes=>390184, 
 :oldmalloc_increase_bytes_limit=>16777216
}

Ух! Информации много! По соображениям простоты, сейчас нам нужно обратить внимание только на два ключа: :total_allocated_objects и :total_freed_objects. Как следует из названий, первый показывает общее количество выделенных объектов, а второй - общее количество освобожденных объектов.

Кроме того, чтобы отслеживать использование памяти Ruby, нам нужен гем с именем get_process_mem, чтобы получить информацию об использовании памяти процессом. Добавляем gem ‘get_process_mem’ в Gemfile и запускаем bundle instal. Затем мы инициализируем новый объект GetProcessMem и используем .mb, чтобы вернуть использование памяти в МБ. Наша основная настройка среды тестирования:

require ‘get_process_mem’
GC.start
allocated_before = GC.stat(:total_allocated_objects)
freed_before = GC.stat(:total_freed_objects)
mem = GetProcessMem.new
puts “Memory usage before: #{mem.mb} MB.”
#
# some code to test
#
mem = GetProcessMem.new
puts “Memory usage after: #{mem.mb} MB.”
GC.start
allocated_after = GC.stat(:total_allocated_objects)
freed_after = GC.stat(:total_freed_objects)
puts “Total objects allocated: #{allocated_after — allocated_before}”
puts “Total objects freed: #{freed_after — freed_before}”

А теперь приступим к тестам!

Сначала давайте проверим массив, поскольку объекты сохраняются в массиве, давайте посмотрим, что произойдет, если мы перелопатим огромное количество объектов в массив:

array = []
1_000_000.times do
 array << “a sample string”
end

Вот результат:

Memory usage before: 11.70703125 MB.
Memory usage after: 60.765625 MB.
Total objects allocated: 1000119
Total objects freed: 140

Как мы видим, использование памяти значительно увеличивается, и освобождается лишь несколько объектов. Мы создали 1 000 000 копий "a sample string", но они не собираются сборщиком мусора, поскольку Ruby предполагает, что мы можем использовать их в будущем, потому что мы сохраняем объекты.

Но что произойдет, если мы все же сделаем 1 000 000 раз, но не сохраним их?

1_000_000.times do
 string = “a sample string”
end

И вот результат:

Memory usage before: 11.5859375 MB.
Memory usage after: 11.5859375 MB.
Total objects allocated: 1000118
Total objects freed: 1000140

Теперь все объекты освобождены, а использование памяти до и после практически не меняется.

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

array = []
1_000_000.times do
 array << “a sample string”.freeze
end

Результат:

Memory usage before: 11.63671875 MB.
Memory usage after: 19.26953125 MB.
Total objects allocated: 119
Total objects freed: 140

Как мы видим, хотя общее количество освобожденных объектов не сильно меняется, общее количество выделенных объектов и использование памяти намного меньше в нашем первом тесте. Итак, что происходит с Ruby, так это то, что вместо хранения 1 000 000 различных объектов Ruby сохраняет только один строковый объект с 1 000 000 ссылок на этот объект.

Кроме того, тот же результат может быть достигнут более распространенной практикой - просто присвоить строку переменной:

array = []
string = “a sample string”
1_000_000.times do
 array << string
end

И результат:

Memory usage before: 11.57421875 MB.
Memory usage after: 19.32421875 MB.
Total objects allocated: 120
Total objects freed: 140

Работает как часы!

Замечательно. Теперь мы хотим изменить некоторые перечислимые объекты, как показано ниже:

array = [“cat”, “mice”, “dog”, “giraffe”,”elephant”]
array *= 100_000 #simply copy the array 100000 times
array.map do |element|
 element.gsub(/[aeiou]/, ‘*’).upcase
end

Давайте еще раз проверим состояние памяти:

Memory usage before: 11.640625 MB.
Memory usage after: 92.2578125 MB.
Total objects allocated: 3500129
Total objects freed: 3500142

Итак, теперь есть способ уменьшить использование памяти и ускорить наш код? да. Начиная с Ruby 2.0, рубисты могут создавать счетчики _17 _ (помните, все программисты ленивы?). Эта замечательная функция может значительно улучшить использование памяти при связывании методов в перечислителе. Чтобы использовать эту функцию, мы просто добавляем lazy в счетчик:

array = [“cat”, “mice”, “dog”, “giraffe”,”elephant”]
array *= 100_000 #simply copy the array 100000 times
array.lazy.map do |element|
 element.gsub(/[aeiou]/, ‘*’).upcase
end

Давай посмотрим что происходит:

Memory usage before: 11.62109375 MB.
Memory usage after: 15.28515625 MB.
Total objects allocated: 136
Total objects freed: 149

Это так удобно и аккуратно! Добавляя такой небольшой фрагмент кода, мы можем сэкономить большое количество памяти!

Дополнительные чтения:

В Ruby все является объектом. Все объекты Ruby создаются в памяти Heap. Ruby также использует ссылки для указания на объекты.

Как языки программирования используют память компьютера? Стек против кучи

Передача по значению или передача по ссылке

Ссылки: