server-completion の実装
SKK-ML で server-completion の話題が出ていました。L 辞書はソート済なので対数時間で処理できるはずです。むずむずしたので実験してみました。
補完動作の確認
まずは補完動作がきちんとできることを確認するために、小さなプログラムを書きます。main 関数からです。
int main() { std::vector<std::string> data; data.push_back("abc"); data.push_back("bcd"); data.push_back("bcdef"); data.push_back("bcdefg"); data.push_back("c"); complete<compare1>("bcd", data); }
テストデータを用意して、complete 関数を呼び出しています。complete 関数は以下の通りです。
template <typename CompareFunctor> void complete(const std::string& key, std::vector<std::string>& data) { typedef std::vector<std::string>::iterator vecstr_iterator; typedef std::pair<vecstr_iterator, vecstr_iterator> range; range result = std::equal_range(data.begin(), data.end(), key, CompareFunctor(key.size())); std::for_each(result.first, result.second, dump()); }
テンプレート引数は equal_range に渡す比較用ファンクタです。range は equal_range の戻り値を格納する型で、一致した範囲の先頭と末尾を示すイテレータのペアになります。最後に一致した内容をダンプしてます。ダンプするファンクタは単純そのもの。
struct dump { void operator()(const std::string& str) const { std::cerr << str << std::endl; } };
比較用ファンクタは、キーの長さで大小比較しているだけです。
class compare1 { int length_; public: compare1(int length) : length_(length) {} bool operator()(const std::string& lhs, const std::string& rhs) const { return lhs.substr(0, length_) < rhs.substr(0, length_); } };
では、実行してみます。
% ./a.out bcd bcdef bcdefg
うまくいっているようです。しかし、substr は string のインスタンスを毎回生成するので非効率です。調べてみると、compare というメソッドが用意されていたので、これを使うようにしてみます。
class compare2 { int length_; public: compare2(int length) : length_(length) {} bool operator()(const std::string& lhs, const std::string& rhs) const { return 0 < rhs.compare(0, length_, lhs, 0, length_); } };
complete 関数のテンプレート引数を compare2 にして確認したところ、結果は同じでした。これでひとまず OK ですね。
L 辞書で試してみる
では続けて、L 辞書でやってみるとどうなるのか確認してみます。main() を以下のように書き換えます。文字コードは当然 EUC です。
int main() { std::vector<std::string> data; std::fstream ifs("SKK-JISYO.L"); std::string word; // 送りなしまで読み飛ばす while(getline(ifs, word)) { if(word == ";; okuri-nasi entries.") break; } // 見出し語の vector を構築する while(ifs >> word) { data.push_back(word); std::getline(ifs, word); } // 補完 complete<compare2>("かたかな", data); }
実行してみます。
% ./a.out かたかな かたかなご かたかなひょうき
うまくいっているようです。
substr と compare の違いを計測する
せっかくなので、compare1 と compare2 でどれぐらい違いが出るのか計測してみました。まず計測用の perf 関数を用意します。
template <typename CompareFunctor> void perf(const std::string& key, std::vector<std::string>& data) { stopwatch t(typeid(CompareFunctor).name()); for(int i = 0; i < 100000; ++ i) { std::equal_range(data.begin(), data.end(), key, CompareFunctor(key.size())); } }
equal_range を 10 万回繰り返します。stopwatch は以下のような単純なクラスです。
class stopwatch { clock_t begin_; const char* msg_; public: stopwatch(const char* msg) : msg_(msg), begin_(clock()) {} ~stopwatch() { std::cerr << msg_ << ": " << (clock() - begin_) / (double)CLOCKS_PER_SEC << " seconds" << std::endl; } };
main を書き換えて、
int main() { std::vector<std::string> data; std::fstream ifs("SKK-JISYO.L"); std::string word; // 送りなしまで読み飛ばす while(getline(ifs, word)) { if(word == ";; okuri-nasi entries.") break; } // 見出し語の vector を構築する while(ifs >> word) { data.push_back(word); std::getline(ifs, word); } // 補完時間計測 perf<compare1>("かたかな", data); perf<compare2>("かたかな", data); }
実行してみます。
% g++ -O2 completion.cpp % ./a.out 8compare1: 2.65 seconds 8compare2: 0.31 seconds
その差は歴然と言いたいところですが、現実問題として 10 万回も検索することはないので、たとえ substr を使ったとしても体感速度的には全く問題ないと思います。線形検索ならもっと劇的な差が出たことでしょう。
いずれにしても、思った通りの結果が出ると気持ちがいいですね ;-) server-completion への対応は前向きに検討したいと思います。