でかいデータをなにも考えずメモリ上に置いておくと、あっという間にメモリが埋まる。
不要なデータはこまめに消して、必要なときに必要なものだけメモリに置くようにすれば大抵なんとかなるのだけど、そうやって整理していくと、ある水準を超えたところで処理時間とかコードの可読性が問題になってしまう。
こういう問題の解決策として、データを生成したあとは圧縮してメモリ上に置いておき、使うときに解凍して呼び出すという方法が考えられる。もちろん普通にできることではない。ないのだが、pythonだとpickleを介することでできなくはない。当然オーバーヘッドは大きいけど。
こういうのは説明するよりソースを見せた方が早い。
import pickle import bz2 PROTOCOL = pickle.HIGHEST_PROTOCOL class ZpkObj: def __init__(self, obj): self.zpk_object = bz2.compress(pickle.dumps(obj, PROTOCOL), 9) def load(self): return pickle.loads(bz2.decompress(self.zpk_object))
こういうファイルを作ってパスの通ったとこに置いておく。pickle化・圧縮のプロトコルは気分で変えても良い。
使い方も特に難しくはない。
from zpkobj import ZpkObj #zpkobjというファイル名にした場合 ... obj = ZpkObj(obj) #圧縮するとき ... obj = obj.load() #解凍するとき
留意点としては、オリジナルのオブジェクトの参照がどこかに残っているとGCが動かないので期待したようなメモリ節約効果が得られないことが挙げられる。そういうときは手動で消す。
... zobj1 = ZpkObj(obj1) del obj1 #違う名前に代入するなら必須
せっかくなので、memory_profilerでベンチマークを取ってみる。
mtest.py(ベンチマーク用コード)
# coding: UTF-8 from zpkobj import ZpkObj @profile def main(): lst = list(range(10000000)) zlst = ZpkObj(lst) del lst lst = zlst.load() del zlst if __name__ == '__main__': main()
結果
$ python -m memory_profiler mtest.py Filename: mtest.py Line # Mem usage Increment Line Contents ================================================ 4 28.066 MiB 0.000 MiB @profile 5 def main(): 6 415.867 MiB 387.801 MiB lst = list(range(10000000)) 7 422.105 MiB 6.238 MiB zlst = ZpkObj(lst) 8 34.645 MiB -387.461 MiB del lst 9 428.691 MiB 394.047 MiB lst = zlst.load() 10 423.742 MiB -4.949 MiB del zlst
7~8行目を見ればわかるように、意図した通りオブジェクトのサイズを1/50以下に圧縮できている(圧縮の効きやすさはデータ依存なんで、すべてのオブジェクトのかさを1/50以下にできる訳ではないけど)。
ところで、6行目と10行目で若干メモリ消費量が増えてるのはなんで? memory_profilerの使用メモリが載っちゃってるか、それとも一千万個も生成した整数オブジェクトをぜんぶ回収しないうちに次の処理が進んでるんだろうか。そこに関しては詳しくないので謎。
2018/11/26 追記
この記事を書いたときからほぼ2年の歳月が過ぎ、古い記事を見直していた私はこう思った。
「joblibでできそう。わざわざ自分で書く意味なくね」
joblib.dump — joblib 0.13.0 documentation
近日中に検証して記事にする予定。