Hisakeyのブログ

エンジニアが色々呟くブログです。

Ruby/Railsのメモ化について、詳しく調べてみた。

はじめに

この記事では、Ruby/Railsのメモ化 をハンズオンで確認します。

「メモ化をすると内部で何が起きるのか?」という疑問に対して、低レイヤ(参照・割り当て・GC)の視点で手を動かして確かめます。

背景・動機

Rails で I/O(DB/HTTP)を伴う処理にメモ化が効くのは直感的に分かります。同じ結果を再利用できれば、DB への複数回アクセスを避けられるからです。

一方で、I/O がない純計算でも「毎回新しいオブジェクトを確保→破棄(GC)」を繰り返すと、割り当て回数や GC 負荷が増えて遅くなります。

内部で何が起きているのかを具体的に理解するため、今回の記事を書きました。

それでは、手を動かして確認していきます。

実例・やってみたこと

サンプルコードを書いて、3つの観点から確認します。

  • 同じ参照か?: Object_id
  • 割り当て回数:GC
  • 速度:Benchmark

サンプルコードは以下になります。

require 'benchmark'

def build_obj
  # I/Oの代わりに「確保がちょっと重い」処理を模擬
  Array.new(10_000) { rand }.sum
  "x" * 10_000
end

def no_memo
  build_obj
end

def memorized
  @memo ||= build_obj
end

puts "=== object_id ==="
3.times { p [:no_memo, no_memo.object_id] }
3.times { p [:memo,    memorized.object_id] }

puts "=== allocations (GC.stat) ==="
GC.start
before = GC.stat(:total_allocated_objects)
1000.times { no_memo }
after = GC.stat(:total_allocated_objects)
puts "no_memo allocations: #{after - before}"

GC.start
before = GC.stat[:total_allocated_objects]
1000.times { memorized }
after = GC.stat[:total_allocated_objects]
puts "memorized allocations: #{after - before}"

puts "=== speed (Benchmark.bm) ==="
Benchmark.bm do |x|
  x.report("no_memo x10000") { 10_000.times { no_memo } }
  x.report("memorized x10000"){ 10_000.times { memoized } }
end

結果は以下のようになりました。

=== object_id ===
[:no_memo, 60]
[:no_memo, 80]
[:no_memo, 100]
[:memo, 120]
[:memo, 120]
[:memo, 120]
=== allocations (GC.stat) ===
no_memo allocations: 2002
memorized allocations: 2
=== speed (Benchmark.bm) ===
                      user     system      total        real
no_memo x10000    4.498254   0.085579   4.583833 (  4.584453)
memorized x10000  0.000541   0.000005   0.000546 (  0.000545)

結果からわかること

  • object_id:
    • no_memoは毎回異なる。(毎回新規に割り当てがされている)
    • memorizedは毎回同じ。(最初に割り当てられた参照を繰り返している)
  • GC:
    • memorizedのallocations(割り当て回数)が圧倒的に少ない。
  • Benchmark:
    • memoirzed の方が速い(割り当てとGCが減るため)

学び・気付き

  • メモ化の本質は、「同じ参照を返す」ことによって、割り当て削減やGC負荷を減らすこと。
    • メモリ消費がゼロにはならない (1つは必ず必要)
  • I/Oがなくても、効果がある。重い計算(複雑ロジック)や大きなオブジェクトがあるのなら、メモ化で改善できる。

まとめ

  • メモ化あり → 最初に確保→参照を保持→以降は同じ参照を返す
  • メモ化なし → 毎回確保→使い終わりはGC
  • 効果:割り当て回数とGC時間の削減 → スループット向上・CPU節約

とりあえずメモ化すれば良いと思っていましたが、内部がどのように動作しているかを調べることで、なぜメモ化が必要なのかを理解した上で、処理を書くことができるようになったかなと思います。

用語メモ

  • I/O:外部とのやり取り(DB・ファイル・ネットワークなど)
  • GC(Garbage Collection):不要になったオブジェクトを自動回収する仕組み
  • allocations(割り当て回数):新しいオブジェクトを作った回数(区間差分で見る)
  • benchmark:性能計測。Benchmark.bm ã‚„ benchmark-ips を使うと 時間/スループットを測れる