do_search()の効率化†
- ページ: BugTrack2
- 投稿者: Cue
- 優先順位: 低
- 状態: 完了
- カテゴリー: その他
- 投稿日: 2006-04-14 (金) 13:58:15
- バージョン:
メッセージ†
"PukiWiki/1.4/ちょっと便利に/do_search()の改造実験" (削除済み)から移動。
do_search()の改造実験 by Cue†
検索文字列の含まれるページをリストアップする関数ですが、割と重い処理のようなので軽くできないか考えてみます。
期待する効果†
1.4.6の配布ファイルで呼び出している以下の2箇所の処理が軽減されます。
- searchプラグインの単語検索
- Autolink対象となるページが新設された場合に、そのページに対してAutolinkする可能性のあるページをリストアップする時
改造点†
- pregの呼出し回数が多いのでパターンをstudyさせてみる。(効果大*1)
- キーワードを含むか否かの判定にpreg_grep()を使っているが、キーワードを含む全ての行を抽出する必要は無いのでpreg_match()に置き換える。(効果中)
- ↑の結果、ページ内容を配列で取得しなくても良いのでfread()を使って一括読み込みする*2。(効果大)
- ファイル内容を読んだ場合にはfstat()で取得した時刻情報を流用する。(効果微妙*3)
- links_update()から呼ばれる場合*4、キーワードの完全一致で良いのでget_search_words()の呼出しを抑制する。(効果大*5)
パッチ†
1.4.6(func.php,v 1.46)からの差分
--- lib/func.php Mon Jul 04 00:09:28 2005
+++ lib/func.new.php Thu Mar 30 21:42:50 2006
@@ -190,32 +190,37 @@
$retval = array();
$b_type = ($type == 'AND'); // AND:TRUE OR:FALSE
- $keys = get_search_words(preg_split('/\s+/', $word, -1, PREG_SPLIT_NO_EMPTY));
+ $keys = $non_format ? array(preg_quote($word, '/')) :
+ get_search_words(preg_split('/\s+/', $word, -1, PREG_SPLIT_NO_EMPTY));
+ $keys = preg_replace('/^.*$/', '/$0/S', $keys);
$_pages = get_existpages();
$pages = array();
$non_list_pattern = '/' . $non_list . '/';
- foreach ($_pages as $page) {
+ foreach ($_pages as $file=>$page) {
if ($page == $whatsnew || (! $search_non_list && preg_match($non_list_pattern, $page)))
continue;
+ $source = $non_format ? '' : $page . "\n";
// 検索対象ページの制限をかけるかどうか (ページ名は制限外)
if ($search_auth && ! check_readable($page, false, false)) {
- $source = get_source(); // Empty
+ $stat = NULL;
} else {
- $source = get_source($page);
+ if (($fp = fopen(DATA_DIR . $file, 'rb')) === FALSE)
+ continue; // ページが消えている?
+ flock($fp, LOCK_SH);
+ $stat = fstat($fp);
+ $source .= fread($fp, $stat['size']);
+ fclose($fp);
}
- if (! $non_format)
- array_unshift($source, $page); // ページ名も検索対象に
- $b_match = FALSE;
foreach ($keys as $key) {
- $tmp = preg_grep('/' . $key . '/', $source);
- $b_match = ! empty($tmp);
+ $b_match = preg_match($key, $source);
if ($b_match xor $b_type) break;
}
- if ($b_match) $pages[$page] = get_filetime($page);
+ if ($b_match) $pages[$page] = isset($stat) ?
+ $stat['mtime'] - LOCALZONE : get_filetime($page);
}
if ($non_format) return array_keys($pages);
コメント†
- 違うところからアプローチしていたのですが、改造点2について、同じ結論にたどり着きましたので後ほどコミットしようとおもいます。ちなみに、コミットする際にBugTrackの方がよさそうなので、本件、BugTrackに移行します。 -- teanan
- 移行してコミットしました。-- teanan
- cvs:lib/func.php (1.55)
- cvs:lib/html.php (1.55)
- 'preg_grep' => 'preg_match'へ修正(改善点2)
- get_search_words()でアルファベットの大文字/小文字の考慮を外しました。
その代わり、preg_matchにおいて、大文字/小文字を区別しないようにしています。
- 改善点3(freadで一括読み出し)の件、実装しました。get_source() を拡張して配列ではない通常の文字列として返せるようにしましたので、他の同様な部分についても対応できます。 -- teanan
- 私は約2000ページのテスト環境ですが、ここまでの対応で大きな効果が出ています。 -- teanan
- 本来あるべき実装かどうかを確認し追究する、という良いサイクルが回っている気がします :) さてマルチバイト文字列に対する正規表現周りじゃないかと思うのですが、一つ例外を見つけました。(対象は数日前のdevサイトのバックアップデータ)。観察中 -- henoheno
Search words | func.php | Hits/2020 | Time |
"BugTrack" | r1.54 | 1428pages | 20.4sec |
r1.57 | 6.2sec |
"開発日記" | r1.54 | 618pages | 3.9sec |
r1.57 | 2.0sec |
"Web委員" | r1.54 | 52pages | 4.1sec |
r1.57 | 51pages | 10.2sec |
"b委" | r1.54 | 52pages | 5.8sec |
r1.57 | 51pages | 11.7sec |
"b" | r1.54 | 1845pages | 18.3sec |
r1.57 | 3.3sec |
"委" | r1.54 | 58pages | 3.6sec |
r1.57 | 1.8sec |
- おはようございます。検証ありがとうございます。get_search_wordsの修正がマズかったのかもしれませんね・・・ (^^; -- teanan
get_search_words() に対する修正だけを含んでいる func.php を r1.54 に戻すだけで直る様ですので、そうかもしれません (単純に r1.54にして戻るのは)get_search_words() だけじゃなかった・・・-- henoheno
- 他に気になっている部分・・・(観察中) -- henoheno
- get_source(): それぞれの挙動のポイントをコメントにしたいところ (追加し
ておきます ました)
- get_source(): $join 時に str_replace("\r", '') していないのは恐らくコールする側の想定外。これをキャンセルさせたいなら別途引数追加を要するやも(順番は多分$joinの手前)。 -> 追加しておきました。特に処理速度に影響がある様にも見えません
- do_search(): ページ名だけで検索条件が成立するならば、ページの中を読む必要はない(このシチュエーションはOR条件の時や、検索単語が一つだけの時や、ありふれた極端に短い文字や、例えばここだと"BugTrack"の様に、ページ名として良く使われている検索キーワードで顕著に発生する)。また、現在は巨大な文字列の連結をしている。ただしこれらは、preg_match() を多く呼ぶ事につながるので実験を要する。
- cvs:func.php (r1.55) のコメントログの指摘「'get_search_words' doesn't consider upper and lower cases of the alphabet.」は正しくありません。従来から foreach (array(strtoupper($char), strtolower($char)) の部分で大文字小文字を「確実に両方扱って」います。 そしてr.155 の修正は大文字小文字を文字単位では見なくなっていますから、「全角の」アルファベットを確実にケアしない状態になっている様です(例えば小文字のアルファベットが入力されたとき、全角小文字のための正規表現は作るが全角大文字のための正規表現は作らない) -- henoheno
- mb_convert_kana('A') の部分ですね -- henoheno
- あ、foreachがなくなっているので、entity reference のための正規表現(アスキーコード) についても同様で。 -- henoheno
- get_search_words() についてはbackoutするのがいいかもしれません -- henoheno
- 言われてみれば顔文字に見えますね (^^; これで「Web委員」で検索した時にヒットするページ数が従来通りになった様です。ただし12sec(少なくとも従来より改善はしていない)。何が良く無いのだろう? (予想: 正規表現末尾のオプション) -- henoheno
- back out した時は、コミットログにどのリビジョンに対するback out なのかとか、なぜback outしたのか触れていただけると助かります(cvsリポジトリ単独で追跡できる様に) -- henoheno
- 了解です。コミットログを修正しました。正規表現のオプションで"S"をつけると早くなりますね*7。もう少し調査中。 -- teanan
- back out する修正は直前のものとは限りませんから、back out したリビジョンを具体的にログに書くのがお勧めです(コミットログを少々修正しました)。to r1.54 と書くと、そこから先の全ての修正を却下したかのようにも読めてしまいます。コミットログを簡潔に書ける様にするためには、もちろん前提として「個々の修正をなるべく論理的に分けてコミットしている」習慣付けが必要です。今回、問題の修正のリビジョン番号が、複数のファイルでたまたま同じなんですね。珍しいです -- henoheno
- すみません、なかなか難しいです (^^; お手数をおかけしました。 -- teanan
- どの件もそうですが、日常生活に支障が出ない程度に、休み休み進めましょう。一つ一つがきちんとして行くのであれば、何日かかってもいいです (区切りがついたタイミングごとにリリースするだけです)。*8 -- henoheno
- お気遣いありがとうございます。やれる範囲でやらせて頂いておりますのでご安心を :) *9 -- teanan
- 現在の状況は以下の通り。do_search() の中にある get_search_words() が返す正規表現はどちらでも同じであるため、これ("b委"周辺)は do_search() 内部の処理の差ではないかと思われます -- henoheno
Search words | func.php | Hits/2020 | Time |
"BugTrack" | r1.54 | 1428pages | 20.4sec |
r1.58 | 7.5sec |
"開発日記" | r1.54 | 618pages | 3.9sec |
r1.58 | 2.1sec |
"Web委員" | r1.54 | 52pages | 4.1sec |
r1.58 | 13.0sec |
"b委" | r1.54 | 52pages | 5.8sec |
r1.58 | 14.8sec |
"b" | r1.54 | 1845pages | 18.3sec |
r1.58 | 3.7sec |
"委" | r1.54 | 58pages | 3.6sec |
r1.58 | 1.9sec |
- PCRE装飾子 S または i を付けるかどうかによって速度が変わります。PCREの内部的にどのようなことをやっているのか興味があるところですが、とりあえず、手元の環境で一番効果がありそうな "S" をつけたバージョンをコミットしました。 -- teanan
- 次に、一致したページ名を記録する部分で、$non_formatがtrueの場合にfiletimeを呼ばないように修正しました(こちらは検索の速度には関係しません)。 -- teanan
- 今回の修正の中で、ページ名とページ本文を収めた変数が直接連結される形になっていますが、その(文字連結等のコスト以外の)副作用として、例えば「456」というページの本文が「789」だった時、「456789」という単語検索にひっかかる様になっています。この意図しない状況を回避するのであれば、少なくとも
半角スペース 改行コード1つなどを間に追加しておくと良いと思われます。 -- henoheno
- ご指摘ありがとうございます。確かにそのとおりです (^^; -- teanan
- r1.61で、今までのコンセプトに沿った本来の実装になったわけですが、余計なファイル処理をキャンセルする修正の都合、ページ名を別途先にチェックする形に改めさせていただきました。 -- henoheno
- 現状は以下の通り。 "S" オプションは他の要所でも使えそうですね。 -- henoheno
Search words | func.php | Hits/2020 | Time |
"BugTrack" | r1.54 | 1428pages | 20.3sec |
r1.61 | 2.6sec |
r1.64 | 1.9sec |
r1.67 | 1.9sec |
"開発日記" | r1.54 | 618pages | 3.9sec |
r1.61 | 2.1sec |
r1.64 | 1.8sec |
r1.67 | 1.8sec |
"Web委員" | r1.54 | 52pages | 4.1sec |
r1.61 | 2.2sec |
r1.64 | 2.1sec |
r1.67 | 2.2sec |
"b委" | r1.54 | 52pages | 5.8sec |
r1.61 | 2.1sec |
r1.64 | 2.1sec |
r1.67 | 2.2sec |
"b" | r1.54 | 1845pages | 18.1sec |
r1.61 | 2.7sec |
r1.64 | 1.9sec |
r1.67 | 1.9sec |
"委" | r1.54 | 58pages | 3.6sec |
r1.61 | 1.9sec |
r1.64 | 1.8sec |
r1.67 | 1.9sec |
- $search_non_list, $search_auth の除外処理を事前に行っておいたり、get_existpages() が返す値を軽くするか別の手段で最小限のデータを得るようにするともう少し軽くなるでしょう。 -- henoheno
- tracker_listとかにSオプションつけると効果がありそうですね :) -- teanan
- というわけで、他のこまごました修正と、ピーク時に大変になるだろう要求メモリの無駄を省いて cvs:lib/func.php (r1.67) まで持って来ました。これ以上やると 上で上げている別関数の拡張や見直しの話になりそう(=深みにはまりそう)ですから、上で盛り上がった話はひとまずここまででしょうか。他に採用できる部分があればコメントお願いします -- henoheno
- おっと、検索結果が $show_passage の設定を無視している事に気付いたので、対応させました。get_filetime() を一切発行しなくなるので、これは結構(0.1~0.2sec)効果が大きいです(ここの検索結果だと5秒短縮される・・・)。 cvs:lib/func.php (r1.68) 今やらないけど、検索wordの処理も削れる気がする・・・(新たに設定を追加して、検索/表示に関する一切のwordの処理を無効化する路線の話) -- henoheno
- 確認しました、お疲れ様です。私もこの辺で一段落付いたとおもいますので、一度「完了」にしておきます。他に効果がありそうなものが確認できたら、また蒸し返しましょう :) -- teanan
- ざっと見ただけですが、蒸し返し用に改造点と現況との比較の確認をさせて下さい。1~3については取込済。4については未取込だが、別方面の($show_passageを見る)対応済。5については未取込。って事で良いでしょうか。 -- にぶんのに
- はい、私も同じ認識です。 -- teanan
- にぶんのにさん、チェックありがとうございます。Cueさんの名前が今回多分cvsリポジトリに無い件、結果的に採用されたものが単独でコミットできているのであれば、できれば状況に応じて Pointed out by とか Patched by といった形で貢献を付記しておいて下さい。cvs:lib/file.php (1.61) 等。貢献者の*10ためでもありますし、我々が後で修正点をまとめたり、Special Thanksの候補をピックアップする際の手助けにもなります。 -- henoheno
- searchプラグインで検索対象のページを指定した場合、正しく検索できなくなっていましたので修正しました。cvs:lib/func.php (1.73) -- teanan
- BugTrack2/330 --
取り込まれたコードを見て†
取り込み作業お疲れ様でした。遅まきながら感想など。
fstat()とstat()の速度差に関する補足説明†
4番目の改造点を効果微妙と評しましたが、これは多数のページにヒットするような検索はあまりないだろうという前提に立っています。
私Cueが書き込んだパッチの当該部分を見てもらうと$b_match=trueの場合だけページの更新時刻を調べる構造になっていますので、この前提に立てばそもそもget_filetime()の呼び出しはそれほど発生していない事になります。逆に言うと多量のページがヒットする検索ではfstat()とstat()の速度差は無視できないものとなります。
どの程度無視できないのか?†
速度の計測は裏で何が動いているか把握できる隔絶された環境で厳密に比較すべきですが追試可能であることも重要でしょう。おあつらえ向き(?)に1.4.7のコードでは検索ヒットしたページのpassage取得のみ別ループでの処理*11に変えられているので、この点を利用してstat()系の処理の重さを見積もることができたりします*12。
下の結果は`と*を使ってPlus!の公式サイトで検索したときのものです(勝手に実験に使って済みません>plusの方々*13)。
検索文字 | convert time | hitしたページ数 |
` | 0.21sec程度 | 7 |
* | 0.30sec程度 | 689 |
多数のページにヒットする検索があまりないなら構う事もないのですが、実は同様の全ページに渡るstat()取得がget_source()呼び出しに隠されています(is_page()(の中のfile_exists())とfilesize()*14)。軽量化ネタなわけですし、このあたりもう少し配慮できたんじゃないの?と感じてしまいます。
links_update()からの呼び出し†
link.phpには踏み込みたくなかったので5番目の改造点で留めておいたのですが、もしteananさんがlink.php側からココに到達したとなると話は違ってきます(どこからとは書いてないですけど)。link.phpの各処理で最も重い部分はInlineConverterの呼び出しと考える方が自然でしょう。もしこの処理を軽くしようとするならばむしろpreg_grep()を使ってInlineConverterに渡す行数を絞るコードを別に用意する方が正しいと思います。*15