統計や機械学習の処理を行うとプロセスや日を跨いで全く同じ計算をすることがしばしばあります。計算が重い場合、時間を随分無駄にするのでキャッシュ化するのが望ましいです。この記事ではPHPでキャッシュ化する方法を紹介します(そもそもPHPでそういった計算処理をする必要があるのか?という話がありますが計算の元になるモデルやらなにやらのPHPコードが既に完成されてると他言語で書き直すよりすぐにコードを開発できるのでPHPでやるパターンは十分あります)。この記事で例に出すキャッシュ化部のコードはLaravelですが、キーと値でキャッシュを保存、キーを元にキャッシュを読み込みする仕組みならば共通で使えます。計算処理とキャッシュの読み書きは次の様に書けます。
class CalcService { public function calc($x, $y){ $cacheKey = $x. $y // キャッシュキーを生成 if(\Cache::has($cacheKey)){ // 該当するキャッシュが存在するか問い合わせ return \Cache::get($cacheKey);// キャッシュがあれば、キャッシュの内容を返す } // $x, $y 以外の外部の変数を参照しない計算処理 \Cache::set($cacheKey, $result); // 結果をキャッシュ化 return $result; } }
こうすると二回目以降の計算が不要になり高速化されるのですが問題もあります。まず上述のキーは衝突の問題をはらんでおり、誤った処理が行われる確率が少なくありません。
キャッシュの仕組みは一意なキーとそのキーに対応する値の読み出しです。キーが一意にならない場合、誤った読み出しや誤った上書きが発生することがあります。先ほどのコード例では次の様な場合に問題が起きます。
$calclator = new CalcService(); $result1 = $calclator->calc('52', '13'); // キャッシュキー $x.$y === '52'.'13' === '5213' でキャッシュを生成 $result2 = $calclator->calc('5', '213'); // キャッシュキー $x.$y === '5'.'213' === '5213' でキャッシュを読み込み \Cache::set('5213', '五千二百十三');// 計算処理と関係ない場所で作られたキャッシュで上書き
これらの問題は一意なキーという前提を破壊しやすいキャッシュキー生成処理が原因となって発生します。これを防ぐためには例えば次の様にできます。
class CalcService { public function calc($x, $y){ // compactは['x' => $x, 'y' => $y]の様な変数名と同じ名前のキー、変数の値と同じ値を持つ連想配列を作る // @see https://www.php.net/manual/ja/function.compact.php $cacheKey = __METHOD__ . serialize(compact('x'. 'y')); // キャッシュキーを生成 if(\Cache::has($cacheKey)){ // 該当するキャッシュが存在するか問い合わせ return \Cache::get($cacheKey);// キャッシュがあれば、キャッシュの内容を返す } // $x, $y 以外の外部の変数を参照しない計算処理 \Cache::set($cacheKey, $result); // 結果をキャッシュ化 return $result; } } /************************************************/ $calclator = new CalcService(); // キャッシュキー'CalcService::calca:2:{s:1:"x";s:2:"52";s:1:"y";s:2:"13";}' でキャッシュを生成 $result1 = $calclator->calc('52', '13'); // キャッシュキー'CalcService::calca:2:{s:1:"x";s:2:"5";s:1:"y";s:2:"213";}' でキャッシュを生成 $result2 = $calclator->calc('5', '213'); \Cache::set('5213', '五千二百十三');// 計算処理と関係ない場所で作られたキャッシュで上書きされない
キャッシュ登録場所とキャッシュ内容の元になる値をキーにしました。今回は手っ取り早くシリアライズしましたが、キャッシュキーに入れられないくらい巨大なデータがキャッシュする値の元になるならば何かしらハッシュ値を計算するとよいでしょう。これで運用中に誤ったキャッシュの読み書きが発生して処理が壊れることを防げます。
運用中、と書いた通り実はこれでもまだ問題があります。というのも開発中にコードを書き換えるたびにキャッシュのクリアが必要になり開発が大変面倒になります。キャッシュ読み込みをコメントアウトで塞ぐなどの場当たり的な対応できますが、それをすると後に動作すべきキャッシュ動作が動作していないと発覚するなど危険なことが起きやすいです。これの対応は次のコードでできます。
class CalcService { public function calc($x, $y){ // sha1_fileはファイルの内容から、sha1 ハッシュ値を生成する // @see https://www.php.net/manual/ja/function.sha1-file.php // __FILE__はファイルのフルパスが定義されています // @see https://www.php.net/manual/ja/language.constants.predefined.php $cacheKey = sha1_file(__FILE__) . serialize(compact('x'. 'y')); // キャッシュキーを生成 if(\Cache::has($cacheKey)){ // 該当するキャッシュが存在するか問い合わせ return \Cache::get($cacheKey);// キャッシュがあれば、キャッシュの内容を返す } // $x, $y 以外の外部の変数を参照しない計算処理 \Cache::set($cacheKey, $result); // 結果をキャッシュ化 return $result; } } /************************************************/ $calclator = new CalcService(); // キャッシュキー $ファイル内容のハッシュ値 . 'a:2:{s:1:"x";s:2:"52";s:1:"y";s:2:"13";}' でキャッシュを生成 $result1 = $calclator->calc('52', '13'); // キャッシュキー $ファイル内容のハッシュ値 . 'a:2:{s:1:"x";s:2:"5";s:1:"y";s:2:"213";}' でキャッシュを生成 $result2 = $calclator->calc('5', '213'); \Cache::set('5213', '五千二百十三');// 計算処理と関係ない場所で作られたキャッシュで上書きされない
このようにするとcalc関数が書いてあるファイルが変更される度に同じ引数であってもキャッシュキーが異なるものになり、誤ったキャッシュ読み込みを防げます。これで快適に開発ができます。
ちなみに実はこれでもまだ不十分な部分があります。複数ファイル間にまたがっている部分を快適な開発のためにキャッシュ化するにはまたがっている複数ファイルそれぞれのハッシュ値を計算させる必要があります。↑のコードのままでは読み込んでいるファイルが変更されていた場合、その変更に気づけません。改善には適切なクラス設計をして読み込むべきファイルを限定する、glob関数なりなんなりで走査してハッシュ値を計算するのどちらかが良さそうです。