Rubyでは、作成したオブジェクトがなにかのタイミングでGCによってメモリから解放されています。
一般的なオブジェクトは、どこからかに参照されている間は必要、参照されていないなら不要とGCに判断され、メモリから解放されます。
例外的に 弱い参照 -Wikipedia として定義されたオブジェクトは、まだ参照されていてもなにかのタイミングでGCにぽいぽいされてしまいます。
一見、使いようが無いようですが、生成コストがかかる値をキャッシュしておき、メモリに余裕がない場合(一般にGCが動くタイミング)にキャッシュを破棄する、というようなことができます。
機能の実現に必要となることはありませんが、速度改善などに役立つテクニック的な。
なおタイトルはてきとーです。「○○編」と書いていますが、別の編があったりはしません。たぶん。
Ruby2.1で動作検証をしましたが、1.9以降ならたぶん大丈夫なんじゃないでしょうか。
メモリキャッシュを使わない実装
まず普通に実装してみましょう。
名前(name)と犬種名(breed)を与えと犬を作成するクラスを作ってみます。
このクラスはどうやら、登録された名前と犬種からその犬の性別と強さを計算するようです。
require 'digest/sha1'
class Dog
Info = Struct.new :name, :age, :sex, :strength
def initialize(name, breed="mix")
sex = calc_sex(name)
strength = calc_strength(breed) + calc_strength(name)
@info = Info.new name, 0, sex, strength
end
private
def basic_calc(str)
# sleepはなんらかの重い処理を表現
Digest::SHA1.hexdigest(str.to_s.downcase).split(/[^0-9]/).inject(0){|sum, num| sleep 0.1; sum + num.to_i}
end
# @return [Symbol] 犬の性別 (:male or :female)
def calc_sex(str)
sex = basic_calc(str) % 2
sex.zero? ? :male : :female
end
# @return [Integer] 犬の強さ (0-255)
def calc_strength(str)
basic_calc(str) % 256
end
end
Dog.new("ninja", "Chihuahua")
#=> #<Dog:0x007f9178 @info=#<struct Dog::Info name="ninja", age=0, sex=:male, strength=190>>
Dog.new("kotaro")
#=> #<Dog:0x007f9170 @info=#<struct Dog::Info name="kotaro", age=0, sex=:male, strength=152>>
Dog.new("chappy", "unkown")
#=> #<Dog:0x007f9172 @info=#<struct Dog::Info name="chappy", age=0, sex=:female, strength=306>>
Dog.new("ninja", "Chihuahua")
=> #<Dog:0x007f917c0 @info=#<struct Dog::Info name="ninja", age=0, sex=:male, strength=190>>
どうやらninjaはメスのようです。あと、チャッピー強い。
計算結果をキャッシュする実装
さて、上の実装だと、おなじ名前・犬種の組合せでは毎回同じ結果が帰ってきます。
しかし内部的には毎回 basic_calc
メソッドが呼ばれているためなんだか無駄な気がします。
おなじインプットに対しておなじ結果が帰ってくることがわかっているなら、二度目の計算では前の結果を使いたいと思うのが人の常。
クラス変数に結果を格納するようにして、おなじ文字列が与えられたならば、basic_calc
は計算をしないで結果を返すようにしましょう。
require 'digest/sha1'
class Dog
@@cache_basic_calc ={}
:
private
def basic_calc(str)
key = str.to_s.downcase
@@cache_basic_calc[key] ||=
Digest::SHA1.hexdigest(key).split(/[^0-9]/).inject(0){|sum, num| sleep 0.1; sum + num.to_i}
end
:
end
10匹の犬を登録する(一部、名前や犬種の重複あり)場合に、キャッシュなしとありの場合を測定してみると、明らかな差がでました。当然ですね。
調子に乗って犬5匹を1チームとして登録するクラスを作りましょう。
名前と犬種のセットが同じならば、Dogはおなじ構造体を返すのでキャッシュして問題なさそうですね。
class DogTeam
@@cache_dog = {}
def initialize(breed, dog1, dog2, dog3, dog4, dog5)
@team = [
dog(dog1, breed),
dog(dog2, breed),
dog(dog3, breed),
dog(dog4, breed),
dog(dog5, breed),
]
end
private
def dog(name, breed)
@@cache_dog[[name, breed]] ||= Dog.new(name, breed)
end
end
# 一億チームの集う犬の祭典である
100000000.times do |n|
puts DogTeam.new("team#{n}", "won", "one", "wonwon", "wanwano", "nyao")
end``
盛大な催しのようです。このスクリプトを実行してみましょう。嘘です。実行しないでください。
というのも、実際に実行したらパソコンが止まって再起動するはめになったからです。Qiitaで書いていなかったら、この記事も1から書き直すところでした。
上記はスクリプト実行からのメモリを監視した画像だが、モリモリとメモリ使用量が増加していく様子が分かります。このあと滅茶苦茶フリーズした。
どういうことでしょう。
犬を数えると機械は寝てしまうのでしょうか。それともRubyのバグ? いいえ、このスクリプトのバグです。
DogTeam#dog
のなかで、生成されたDog
オブジェクトが@@cache_dog
に保存されていきます。
@@cache_dog[["hoge", "huga"]]
がDog
オブジェクトを参照し続けるため、ループが続くと@@cache_dog
が肥大化しメモリを食いつぶすというわけです。
要約冒頭の話に戻ってきます。
このようなメモリキャッシュ機構を作る際、Hashがオブジェクトを「弱い参照」で参照することで、メモリが圧迫された際にGCがキャッシュを削除してくれるのを期待するわけです。
弱い参照、WeakRefクラス
上記の問題はWeakRefクラスとObjectSpaceクラスを利用することで解決できるかも知れません。
使い方
irb を起動して確認してみましょう。
WeakRefクラスによって作成したオブジェクトをHashから参照し、GCを動かしてみましょう。
強い参照の値は保持されますが、弱い参照はGCによって削除され、puts
で値を参照しようとするとWeakRef::RefError
になりました。
require "weakref"
hash = {}
hash[:a] = "hoge"*100 # 強い参照
hash[:b] = WeakRef.new("fuga"*100) # 弱い参照
hash #=> {:a => "hogehogeho...", :b => "fugafugafu..."}
GC.start
hash #=> {:a => "hogehogeho...", :b => #<WeakRef:0x3ff67d2b6908>}
puts hash[:a] #=> "hogehogeho..."
puts hash[:b] #=> WeakRef::RefError: Invalid Reference - probably recycled
上記のようにHashの参照先にWeakRefオブジェクトは残ったままなので、可能ならばGC時にHashからキーそのものを削除してあげたいところです。
ObjectSpace#define_finalizer
メソッドを利用することで、オブジェクがGCによってメモリから解放される際のイベントを登録できるので利用しましょう。
Dog, DogTeamへの応用
では具体的な実装。
まず以下のようなクラスを用意します。
require "weakref"
class WeekCache
def initialize
@caches = {}
end
def [](key)
@caches[key]
end
def []=(key, value)
ref = WeakRef.new(value)
ObjectSpace.define_finalizer(value, self.class.ensure_cleanup(self, key, ref)) #1 GC時のイベント定義
self[key] = ref
end
#2
def self.ensure_cleanup(caches, key, ref)
proc{
caches.delete(key) if caches[key] == ref
}
end
end
#1
でvalueがGCされるときに#2
を呼び、WeakRefオブジェクトをまるごとHashから消してあげることにしました。
これをしないと、
- WeakRefオブジェクト自身は強い参照によってメモリ上に残り続けるので、Hashが肥大化すればメモリリークが発生する可能性が残る
- 参照先のオブジェクトがGCされたWeakRefオブジェクトを参照しようとするとエラーになる
上記クラスを使うように、DogTeamを書き換えましょう。
なおDogでは、キャッシュが参照しているのがFixnumのためWeakRefで参照することは出来ません(というかする必要がない)。
class DogTeam
@@cache_dog = WeakCache.new
:
def dog(name, breed)
@@cache_dog[[name, breed]] ||= Dog.new(name, breed)
end
end
実際10億回くらいのループでDogTeam
インスタンスを作成しても、メモリを食いつぶすことはなくなりました。
ベンチマーク
最初の実装・WeakRefをつかったキャッシュ・使わないキャッシュでベンチマークもとってみた所、以下のような関係になりました。
## 10インスタンス程度作る場合 ##
user system total real
no_cache 0.000000 0.000000 0.000000 ( 0.000611)
weakref_cache 0.000000 0.000000 0.000000 ( 0.000349)
cache 0.000000 0.000000 0.000000 ( 0.000208) #よさげだがメモリリークの危険あり
## 1000インスタンス程度作る場合 ##
user system total real
no_cache 0.050000 0.000000 0.050000 ( 0.053537)
weakref_cache 0.050000 0.000000 0.050000 ( 0.045995)
cache 0.030000 0.000000 0.030000 ( 0.030020)
GCが頻繁に発生するような場合(作成インスタンス数が多い場合)は、仕方ないですが効果が薄くなりますね。
がんばって実装した割にはあまり大した効果がない……?
ベンチマークテストを含めたコードはGist登録しておきました。
まとめ
- Hashをつかったメモリキャッシュは工夫しないとメモリリーク起こすよ
- WeakRefを使うときは、GC後にWeakRefも消す仕組みをつかおうよ
参考
- Rubyリファレンス
- Java: java.util.logging のバグに見る WeakReference 使用時の注意点 - sardineの日記 ... Mapに弱参照オブジェクトが残ってしまいメモリリークとなるバグについて、Javaでの事例説明
- therubyracerのpull request ... 凄く参考になる…というか最終的にここの丸パクリになった