memoizeの甘い罠に悩む


 さて、memoizeである。メモ化とも言う。最初memorizeと間違えた。
 簡潔に言うと、重たい純粋関数はキャッシュしよう、である。
 平たく言い直すと、同じ引数を入れると必ず同じ物が返ってくるような関数なら、引数と戻り値で連想配列作ってキャッシュした方が速くなることもあるよね、である。まあ、やってる内容は昔ながらのソレである。

 Pythonの場合、モジュール変数は一度しか初期化されないので、実にキャッシュに向いている。しかも今触ってる鯖環境は、しばらくプロセスが生き続ける。もちろん変数も。実においしい。
 というわけで、重くて事前計算も不可能なものはmemoizeしたいのである。

 Pythonにはまた都合のいいことに、デコレータというものがあるのだが、memoizeをデコレータで書くならクロージャを使いたい気がするし、クロージャで縛った変数も鯖応答をまたいで保持されるのかいまいちよく分からんので、素直にモジュール変数を作って、重い関数内にキャッシュを直接実装した。

 テストランはしばらく調子が良かったのだが、どうも具合がおかしく、しばらく悩んだ末にようやく問題点を発見した。
 今回memoizeした関数はリストを返すのだが、Pythonのリストの代入は参照を代入する。戻り値を入れた変数をresultとすると、

cache[tuple(args)] = result

とか

cache[frozenset(kwargs.iteritems())] = result

とかでキャッシュに書き込んだ後、

return result

で終わる訳だが、それを受け取った呼び出し側がそのままリストを加工するとアウトな訳である。何てこったい。高級言語らしい罠だなあ。
 つーかどこもかしこもmemoizeデコレータってこんな感じの実装ばかり見るんスけど。ちくしょう油断した。
 というわけで、

cache[frozenset(kwargs.iteritems())] = list(result)

みたいな感じにコピーを書き込む仕様にした。直らなかった。
 まあ要するに、もう一ヶ所で全く同じ問題が起きていた。キャッシュヒットの時もコピーを返さないとね!
 つーか書き込みもlist()じゃなくtuple()にした方がいいな。それでもshallow copyだから、階層が深いならdeep copyが必要だなあ。

 まとめると、memoizeのキャッシュアクセスは全てimmutableかdeepcopyでなければならない、ってことだな。キーも値も。まあ、Pythonの連想配列のキーはimmutableが元々要求されるから、値だけ注意しろと。
 うーむ、単純ミスだ。参照代入な言語に触ってこなかったからか。むしろ値渡しのコピーコスト削減に必死だったしな…。まあ、参照だと認識してれば当然のバグな訳だが、抽象化の進んだコードにいまいち慣れてないな。C++とかはテンプレートをガシガシ書いてる時でもアセンブラマクロ目線だしな…。

 ところで、壮絶に久々に「りにとか」とかミスタイプして懐かしすぎた。何年ぶりかとか考えたくないものであるなあ。

(Visited 18 times, 1 visits today)

2 thoughts on “memoizeの甘い罠に悩む

  1. あ〜ごん says:

    クイリリラ
    全然話違うけどマビノギガムは買うんですか?

  2. みこしま says:

    うは何これ買うかも(笑)
    でも何かサービス初期の韓国イベントのすっげー古い服を使い回したようなデザインに見えてしょぼくて泣けるから別にいいかもしんないトントカイモ

コメントを残す

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください