Ruby
RubyDay 8

Ruby でラインメモリプロファイラ

More than 1 year has passed since last update.

プロファイラ好きなモニタの前の皆さんこんにちは。@sonots です。この記事では、Ruby コードのどの行がどのぐらいメモリを消費しているか調べる方法を紹介します。


オブジェクトの数を数える

Ruby には ObjectSpace というオブジェクトの情報を集めたり操作したりする module があります。

このモジュールの each_object メソッドを使用すると、RubyVM 上の全てのオブジェクトを取り出すことができます。

このメソッドを使って、以下のようなコードを書くと、実行した地点で、RubyVM 中にどのクラスのオブジェクトが何個存在しているのかカウントできたりするわけです。興味深いですね!

ObjectSpace.each_object.inject(Hash.new 0) {|h,o| 

h[o.class]+=1; h
}
#=> {Class=>241, String=>9522, Module=>22, Encoding=>100, Object=>2, Bignum=>2, File=>19, Hash=>64, Regexp=>68, RubyVM::InstructionSequence=>659, Array=>779, Mutex=>2, Complex=>1, ThreadGroup=>1, IOError=>1, Binding=>9, RubyVM::Env=>13, Thread=>1, .... }

Cレベルのオブジェクトはこれではカウント出来てなかったりするので、より正確には count_objects も併用します。

ObjectSpace.count_objects

#=> {:TOTAL=>26498, :FREE=>605, :T_OBJECT=>44, :T_CLASS=>511, :T_MODULE=>22, :T_FLOAT=>4, :T_STRING=>9542, ...}

メモ:ヒープを全部辿って VALUE の TYPE 毎の内訳を出力しているようです。:TOTAL が全体で、:FREE が ruby 内で確保されているけれど、使われてないもの。

ちなみに、sigdump という gem を require しておくと、kill -CONT {pid} のように CONT シグナルを送るとこれらの情報をファイルに吐き出してくれるので便利です。


オブジェクトのメモリサイズを知る

オブジェクトのではなく、オブジェクトのメモリ使用量を知りたい場合は memsize_of を利用できます。

こちらは Ruby 1.9.2 から利用可能で、require 'objspace' して使用します。

require 'objspace'

ObjectSpace.memsize_of({a: 4})
#=> 192

(短い) String や (小さい) Integer には 0 が返ります。

ObjectSpace.memsize_of(123)

#=> 0
ObjectSpace.memsize_of("123")
#=> 0

これは、Cレベルで最適化の為に、小さい Integer、短い String は VALUE に直接保存されるようになっていて、オブジェクトが作られていないためのようです。この辺りについては Rubyのしくみ - Ruby Under a Microscope に書いてあるはずなので、そちらを読むと良いと思います。

GC::INTERNAL_CONSTANTS[:RVALUE_SIZE] を足すと正しい値になります。

rvalue_size = GC::INTERNAL_CONSTANTS[:RVALUE_SIZE]

#=> 40 (x86_64)
ObjectSpace.memsize_of(123) + rvalue_size
#=> 40
ObjectSpace.memsize_of("123") + rvalue_size
#=> 40
ObjectSpace.memsize_of({a: 4}) + rvalue_size
#=> 232

なお、RVALUE_SIZE も最初から足してくれたらいいのに、という件については issue になっているようです。2.2.0 には入るのかな? => Bug #8984

後記: Ruby 2.2.0 では加算されるようになりました


オブジェクトが定義された箇所を知る

Ruby 2.1 以降の ObjectSpace にはさらに機能が追加されていて、オブジェクトが定義された箇所を知ることができるようになっています。

この機能を使うには trace_object_allocations でブロックを囲むか、trace_object_allocations_start および trace_object_allocations_stop で処理を囲む必要があります。

# example.rb

require 'objspace'

ObjectSpace.trace_object_allocations_start
o = Object.new # 調べたいやつ
ObjectSpace.trace_object_allocations_stop

puts ObjectSpace.allocation_sourcefile(o) #=> "example.rb"
puts ObjectSpace.allocation_sourceline(o) #=> 5
ObjectSpace.trace_object_allocations_clear

ファイル名と行数がわかりましたね!


Ruby コードのどの行がどのぐらいメモリを消費しているのか調べる

では、実際にこれらを組み合わせて Ruby コードのどの行がどのぐらいメモリを消費しているのか調べてみましょう。今回は example.rb 中のオブジェクトだけ追って見ることにします。

# example.rb

require 'objspace'
ObjectSpace.trace_object_allocations_start
foo = ["foo"] * 255 # 調べたいやつ
ObjectSpace.trace_object_allocations_stop

trace_file = File.basename(__FILE__) # example.rb
rvalue_size = GC::INTERNAL_CONSTANTS[:RVALUE_SIZE]
ObjectSpace.each_object do |o|
file = ObjectSpace.allocation_sourcefile(o)
next unless file == trace_file # 今回は example.rb だけ追う
line = ObjectSpace.allocation_sourceline(o)
memsize = ObjectSpace.memsize_of(o) + rvalue_size
klass = o.class
puts "#{memsize} #{file}:#{line}:#{klass}"
end
ObjectSpace.trace_object_allocations_clear

$ ruby example.rb | sort -nr

2080 example.rb:3:Array
40 example.rb:3:Array
40 example.rb:3:String

おー、example.rb の 3行目でなにか大きい Array があることがわかりました!!


memprof2 gem :dancers:

毎回上のコードを書くのが面倒?よろしい、ならば gem だ!ということで拵えたものがこちらになります。=> memprof2

名前は Ruby 1.8 時代に暗躍したらしい memprof をリスペクトして memprof2 にしてみました。使い方は README に書いていますが、こんなかんじです。

# example.rb

require 'memprof2'
Memprof2.start
12.times{ "abc" }
Memprof2.report
Memprof2.stop

$ ruby example.rb

480 example.rb:3:String

シンプルですね!


まとめ

Ruby 2.1 から入った ObjectSpace に入った機能を駆使することで、Ruby コードのどの行でどのぐらいのメモリが使われているのかを調べることができるようになりました。

簡単に利用できるように gem にしたものが memprof2 にありますので、ご利用いただけます。

是非ご活用ください :sushi:

注意: かなりリソースを喰うのでproductionではご利用いただけません