ちょっと自分の中の構想で、設定ファイルはSymfonyみたいにyamlで書きたいなーって思うところがあり、 yamlパーサを調べてた所、ライブラリタイプとpecl拡張がありました。
peclはちょっとサーバ要件を上げてしまうので、今回はライブラリタイプの「Spyc」(スパイクと読むと教えてもらいました)を調べて見ることに。
早速ダウンロードしてテストコードを走らせます。環境は、
Mac OS X 10.7.3 Intel Core 2 Duo 2.4GHz 8GB Memory apache + PHP5.3.10
です。テストコードは、Spycに同梱されているspyc.yamlをパースするものです:
xhprof_enable(XHPROF_FLAGS_MEMORY);
require_once('./spyc/spyc.php');
$yaml = Spyc::YAMLLoad('./spyc/spyc.yaml');
echo '```
<a href="spyc.yaml">spyc.yaml</a> loaded into PHP:<br>';
print_r($yaml);
echo '```
';
$xhprof_data = xhprof_disable();
$XHPROF_ROOT = '/xhprofをインストールしたディレクトリ';
$XHPROF_SOURCE_NAME = 'Spyc';
include_once $XHPROF_ROOT . "/xhprof_lib/utils/xhprof_lib.php";
include_once $XHPROF_ROOT . "/xhprof_lib/utils/xhprof_runs.php";
$xhprof_runs = new XHProfRuns_Default();
$run_id = $xhprof_runs->save_run($xhprof_dataundefined $XHPROF_SOURCE_NAME);
echo "<a href="\" http: localhost xhprof_html index.php?run="$run_id&source=$XHPROF_SOURCE_NAME\" "" target="\" _blank\"">result</a>";
早速xhprofで結果を見ると・・・(xhprofがやや不安定で、大体平均値のレコードです)
お、重い…
処理速度もさることながら、メモリ使用量がちょっと多くてこれだと毎回パースするのには使いづらいなーという印象で※1、もう少し軽くできるんじゃないかなと頑張ってみた記録です。JavaScriptの高速化などはuupaa.jsとか見て研究してたんですが、PHPの高速化は未体験。勉強も兼ねてやってみました。
※1 実際の用途では、一度パースしたものはシリアライズキャシュするなどすることを推奨しているみたいです。
注意事項
以下の記事は、明らかなバッドノウハウばかりです。また、実際にPHPのソースを読んで効率を研究しているものでもありません><速く動かすためとはいえ、実際のアプリケーションロジックで書くと偉い人に怒られちゃいますので、ふーんという感じで軽く見て頂けると…。
目標
チューニングの目標はこんな感じです:
- Spycと同じ結果が返却されること(当たり前ですね)
- 処理速度平均を向上すること
- メモリ使用量を極力減らすこと
では、実際にやったメモなどを書きます。
その1: staticメソッド/プロパティに変更する
メソッドやプロパティをstaticに変更して、パース時にクラスインスタンスなどを作らずにそのまま処理できるようにしてみました。これはあんまり効果なしでした。
その2:比較演算子に型変換を行わせない
結構迷信かもしれませんが、型変換をなるべく行わせないように比較させてみました。 具体的には、「==」を「===」にするなどですね。
その3:function callを減らす
Spycでは7,570callが発生しています(これは実際のyamlファイルの大きさに比例するでしょうが)。これを何とか減らそうとしてみます。
メソッドコールをインラインで処理してみる
Spyc::stripIndent()など、細々とした文字列処理のメソッドが度々呼ばれるので、これをメインループの中に埋め込んでインラインで処理させます。さらにメソッド自体をコメントアウトしてクラスのコンパイル率を上げてメモリを節約。
JavaScriptでは関数コールの際のCallerオブジェクトやコンテキストの生成でオーバーヘッドが出ることがままありますが、PHPではなさそうですね。
これは結構効果がありました。メモリ量がだいぶ減りました。
その4:速そうな関数で書き換えてみる
Yamlドキュメントでは、「a: bc」のようなコロン区切りをパースする事が多いようで、Spycでは、
$exp = explode(': 'undefined $line);
$key = trim($exp[0]);
array_shift($exp);
$value = trim(implode(': 'undefined $exp));
って感じでパースしてました。「a: bc」を「a」と「bc」に分割すればいいので、違う方法を考えてみます。
list($keyundefined $value) = array_map('trim'undefined explode(': 'undefined $lineundefined 2));
explodeの第三引数でlimitを指定するワンライナーで書いてみたりしましたが、結局 array_mapのコストが高くてダメ。なんだかんだで文字列処理が一番安定して高速でした。
$point = strpos($lineundefined ': ');
$key = trim(substr($lineundefined 0undefined $point);
$value = trim(substr($lineundefined ++$point);
これもちょっとだけ効果有りでした。
定形文字列をクラス定数にして参照してみる
コード中に、パース用のプレースホルダ文字列やシングル/ダブルクオート文字が頻繁に出てきます。これをクラス定数にしてメソッド内で使えば、新しく文字列生成のコストが省けるんじゃないかなーって思いました。
// Placefolders constant
const PLACEHOLDER = '__SPICYYAML__';
const ZEROKEY = '__SPICYZERO__';
const SEQUENCE_PLACEHOLDER = '__SPICYSEQUENCE__';
const MAP_PLACEHOLDER = '__SPICYMAP__';
const STRING_PLACEHOLDER = '__SPICYSTRING__';
// Double/Single quotes
const DOUBLE_QUOTE = '"';
const SINGLE_QUOTE = '\'';
その他、in_arrayで検索する文字列リストも先にクラスプロパティとして作っておきます。
private static $trulyChars = array('true'undefined 'on'undefined '+'undefined 'yes'undefined 'y');
private static $falsyChars = array('false'undefined 'off'undefined '-'undefined 'no'undefined 'n');
private static $escapedQuotes = array ('\\"' => '"'undefined '\'\'' => '\''undefined '\\\'' => '\'');
これは結構効果がありました。文字列を生成するコストが減らせたのが良かったのかもですね。
2012/05/24追記 上記は調査不足でした><メモリ効率は「クラス定数宣言 > 逐次生成」のようで、逐次生成の方がメソッド内で GCが働いてメモリ効率が良いみたいですごめんなさい。
その3(改):もっとfunction callを減らす
この辺りで割と限界は見えてましたが、あと少しでも…という感じでやりました。is_string()やis_int()の関数コールを押さえるため、キャスト比較に変更。
if ( is_string($value) ) {
}
↓
if ( (string)$value === $value ) {
}
これでもfunction callは80くらい減らせました。メモリは800byteくらい節約。
結果
チューニング後のクラスのxhprofの結果は以下のとおりです。
メモリ使用量はだいたい40%ぐらい効率化、速度もそれなりに上がった感じですね。(ただ、思ってたよりも速くならなかったのはちょっと悔しいですが><)
それでも、多分まだ使えないかも
400KBを下回ることができたら使えるかなーって思ってましたが、これだとまだちょっと使うのに躊躇するかもですね。JSのようには上手く行きませんでした(´・ω・`)これ以上を求めるなら、自分で一から書くしかないかも…
まだまだ修行が足りませんでした…もっと頑張らなきゃ。
続き書きました→Spycをもっと頑張ってチューニングしてみた