[Rails] ActiveRecordオブジェクトを生成せずに節約されたメモリを計測する
はじめに
ActiveRecordの機能が必要ではない時は可能な限りHashを利用してActiveRecordオブジェクトを生成しない方がパフォーマンスが良かったりメモリを食わないためよさそうだと思う場面が多いと思いますが、反面実際本当にそうなんだろうか?と思うこともあります
本記事では実際にメモリがどのくらい節約されるのか計測していきます
Rubyでメモリ使用量を計測する方法
Rubyではあるオブジェクトのメモリ使用量をObjectSpace.memsize_of
を使って計測できます
バージョン1.9から入っているようです
require 'objspace'
some_object = SomeObject.new
ObjectSpace.memsize_of(some_object)
#=> 168 (byte)
ObjectSpace.memsize_of
でActiveRecord
とHash
を計測する
この方法でActiveRecordオブジェクトとその値だけを取り出してHashにしたものを計測したところ以下のようになりました
user = User.first
ObjectSpace.memsize_of(user)
#=> 128 (bytes)
user_attributes = user.attributes # Hash化
ObjectSpace.memsize_of(user_attributes)
#=> 1760 (bytes)
Hashの方がメモリ使用量が少ないという想定とは真逆な結果となりました
Hashなどの組み込みクラスに対してユーザー定義クラスのインスタンス場合、計測対象が想定していたものと違うのではと思いユーザー定義クラスに対して以下の実験をしました
ObjectSpace.memsize_of
で想定通り計測されているか実験
インスタンスが# 1000字の文字列を計測
str = "a" * 1000
ObjectSpace.memsize_of(str)
#=> 1041 (bytes)
# インスタンス変数に1000字の文字列が保存されるクラスのインスタンスを計測
class Hoge1
def initialize
@str = "a" * 1000
end
end
hoge1 = Hoge1.new
ObjectSpace.memsize_of(hoge1)
#=> 40 (bytes)
インスタンス変数の分メモリ使用量が増えることを期待していましたが増えていませんでした
色々調べているとこんな記事を見つけました
Rubyプロセスの使用メモリ量の計測
ObjectSpace#memsize_ofメソッドを使うと対象のオブジェクトが消費しているメモリ量を取得できます。
ただし、対象のオブジェクトが他のオブジェクトを保有している場合は、ポインタのメモリ量しか計測しないため、「対象のオブジェクトに関連する全てのオブジェクトのメモリ量ではない」ということに注意する必要があります。
関連する全てのオブジェクトのメモリ量を取得するには、reachable_objects_fromメソッドを使って関連するオブジェクトをたどる必要があります。詳細は以下のリンクを参照してください。
要するに上記のhoge1
は「クラスのアドレス」と「インスタンス変数のアドレス」あたりを保持しているけどクラスの実装(メソッドなど)やインスタンス変数の値自体は直接保持していないようです
実際にインスタンス変数の数を増やしてみるとその数に応じて徐々にインスタンスのメモリ使用量が増えていました
class Hoge2
def initialize
@str_1 = "1" * 1000
...
@str_9 = "9" * 1000
end
end
hoge = Hoge2.new
ObjectSpace.memsize_of(hoge2)
#=> 128
ObjectSpace.reachable_objects_from
でhoge2
を見てみます
ObjectSpace.reachable_objects_from(hoge2)
#=> [
# #<Class:#<Hoge:0x0000ffff9ca0c188>>,
# "11...11",
# ...,
# "99...99",
# ]
reachable_objects_from
は想定通りの値を返してくれています
"reachable_objects"を含めて https://www.atdot.net/~ko1/diary/201212.html#d8 を参考に再度計算するメソッドを利用することにします
def memsize_of_all_reachable_objects_from(obj, exclude_class = Module)
# ...
# 実装は https://www.atdot.net/~ko1/diary/201212.html#d8 を参照してください
# ...
objs.inject(0){|r, (_, o)| r += ObjectSpace.memsize_of(o)}
end
ObjectSpace.reachable_objects_from
を利用して計測する
hoge1 = Hoge1.new
memsize_of_all_reachable_objects_from(hoge1)
#=> 1081 (bytes)
hoge2 = Hoge2.new
memsize_of_all_reachable_objects_from(hoge2)
#=> 9497 (bytes)
それぞれいい感じに期待付近の値が返ってきました、様様です
それではActiveRecord
とHash
も計測してみます
user = User.first
memsize_of_all_reachable_objects_from(user)
#=> 15249 (bytes)
user_attributes = user.attributes
memsize_of_all_reachable_objects_from(user_attributes)
#=> 5109 (bytes)
一応1/3ほどに節約できているようです!(思ったより少ない)
ps
コマンドを利用して計測する
再度こちら(Rubyプロセスの使用メモリ量の計測)を参考に計測
まずprint_memory_usage_before_and_after_diff
を用意します
def memory_usage
rss = `ps -o rss= -p #{Process.pid}`.to_i * 0.001
vsz = `ps -o vsz= -p #{Process.pid}`.to_i * 0.001
[rss, vsz]
end
def print_memory_usage_before_and_after_diff
before_rss, before_vsz = memory_usage
yield
after_rss, after_vsz = memory_usage
puts "rss: #{after_rss - before_rss}MB increased"
puts "vsz: #{after_vsz - before_vsz}MB increased"
end
RSSはResident Set Sizeの頭字語で、プロセスが確保している物理メモリサイズ
VSZはVirtual Memory Sizeの頭字語で、プロセスが確保している仮想メモリサイズ
らしいです
ではActiveRecord
とHash
を計測します
プロセス全体のメモリを計測するため他の要因が入ってきてしまうので、影響を小さくするためにオブジェクト数を各々1000にします
それぞれ実行するたびにプロセスを切って初期状態で何度か実行しました
active_record_objects = []
print_memory_usage_before_and_after_diff do
1000.times do
active_record_objects << User.first
end
end
#=> rss: 6.656000000000006MB increased
# vsz: 5.744000000000028MB increased
hash_objects = []
print_memory_usage_before_and_after_diff do
column_names = User.column_names
1000.times do
user_values = User.limit(1).pluck(*column_names)
hash_objects << column_names.zip(user_values).to_h
end
end
#=> rss: 6.783999999999992MB increased
# vsz: 6.432000000000016MB increased
結果Hashの方がメモリを若干食っているようです
何度か実行しましたが大体上記のような数値でした
Hashオブジェクトを生成するコードの書き方が悪いかもしてないですがその上で上回って欲しかった...
おわりに
結局どっちが総合的に良いのかですが、、
ここで言うまでもなくActiveRecordの機能を利用することがないならHashで十分だと思います
メモリ使用量に関しては2つの計測方法で片方はHashの勝ち、もう片方はほぼ同等だったのでHashの勝ち越しです
その上今回はメモリ使用量を計測しましたが、演算速度とか他にも指標はあるためです
ActiveRecordとHashでオブジェクト生成するのに必要な演算量、どちらが多いかはほぼほぼActiveRecordだと思います
本記事を書くにあたって色々調べて勉強になったし、さらに深入りする入り口をいくつか見つけたので下に貼っておきます
最近バイナリ解析に興味がわいているのでメモリ使用量40(bytes)とかの中身を覗いたりしてみたいなとか
もし気力まかせに行動に至った時は記事にしてみますのでまたいつか
Discussion