Screaming Loud

日々是精進

rubyのyieldでのGCを調べてみた

rubyのyieldでGCってどうなるのかちょっと気になったので調べてみました。

コード

gc.rb

def opener1
  Array.new(3).each do |_e|
    a = Array.new(1_000_000) do
      'hello'
    end
    yield a
  end
end

def opener2
  Array.new(3).map do |_e|
    Array.new(1_000_000) do
      'hello'
    end
  end
end

def check_gc(p)
  p.length
  GC.start
  puts "did gc: #{GC.stat.slice(:minor_gc_count, :major_gc_count, :heap_free_slots, :heap_live_slots, :old_objects)}"
end

GC::Profiler.enable
puts "start: #{GC.stat.slice(:minor_gc_count, :major_gc_count, :heap_free_slots, :heap_live_slots, :old_objects)}"

# opener1 do |p|
#   check_gc(p)
# end

opener2.each do |p|
  check_gc(p)
end

puts "total_time: #{GC::Profiler.total_time.round(3)}ms"

結果

yieldで返すopener1の場合、loopごとにheap free slotが増えold objectが消えてるので使い終わったらGCされていそうです。

$ ruby gc.rb 
start: {:minor_gc_count=>9, :major_gc_count=>2, :heap_free_slots=>70, :heap_live_slots=>23163, :old_objects=>14939}
did gc: {:minor_gc_count=>17, :major_gc_count=>6, :heap_free_slots=>229, :heap_live_slots=>1015920, :old_objects=>1015702}
did gc: {:minor_gc_count=>18, :major_gc_count=>7, :heap_free_slots=>1000068, :heap_live_slots=>1015922, :old_objects=>15702}
did gc: {:minor_gc_count=>18, :major_gc_count=>8, :heap_free_slots=>1000067, :heap_live_slots=>1015923, :old_objects=>15702}
total_time: 0.149ms

通常のArrayを返すopener2では、配列を生成してそれを読み込んでいるので、GCされずにold_objectとして保持されています。

$ ruby gc.rb
start: {:minor_gc_count=>9, :major_gc_count=>2, :heap_free_slots=>77, :heap_live_slots=>23155, :old_objects=>14940}
did gc: {:minor_gc_count=>19, :major_gc_count=>6, :heap_free_slots=>725, :heap_live_slots=>3015923, :old_objects=>3015705}
did gc: {:minor_gc_count=>19, :major_gc_count=>7, :heap_free_slots=>724, :heap_live_slots=>3015924, :old_objects=>3015705}
did gc: {:minor_gc_count=>19, :major_gc_count=>8, :heap_free_slots=>723, :heap_live_slots=>3015925, :old_objects=>3015705}
total_time: 0.343ms