さらなる高速化を目指して

  • 単語抽出部分を見直す

0.22.5+分かち書きパッチのプロファイルで、Classifier::MailParse::add_line で時間がかかっていることがわかったのでこの部分の最適化を考えてみた。まずは、前回にも書いた、単語抽出部分を見直してみる。具体的には、

 while ( $line =~ s/^$euc_jp*?(([A-Za-z]|$non_symbol_euc_jp)([A-Za-z\']|$non_symbol_euc_jp){1,44})([_\-,\.\"\'\)\?!:;\/& \t\n\r]{0,5}|$)//ox ) {
if ( ( $self->{in_headers__} == 0 ) && ( $self->{first20count__} < 20 ) ) {
$self->{first20count__} += 1;
$self->{first20__} .= " $1";
}

my $matched_word = $1;

# In Japanese, 2 characters words are common, so care about
# words between 2 and 45 characters

if (((length $matched_word >= 3) && ($matched_word =~ /[A-Za-z]/)) || ((length $matched_word >= 2) && ($matched_word =~ /$non_symbol_euc_jp/))) {
update_word($self, $matched_word, $encoded, '', '[_\-,\.\"\'\)\?!:;\/ &\t\n\r]'."|$symbol_euc_jp", $prefix);
}
}

という箇所である。1 行目の正規表現で、アルファベットも日本語(euc-jp)も一緒くたに扱うようにしているが、これを

 s/^$euc_jp*?([A-Za-z][A-Za-z\']{1,44}|$non_symbol_euc_jp{2,45})([_\-,\.\"\'\)\?!:;\/& \t\n\r]{0,5}|$)//ox

と順番を変更するだけで若干速度が向上する。また、ここで単語を抽出したあとに文字数を調べるような処理になっているが、これも抽出時にアルファベットなら 3 文字以上、日本語なら 2 文字以上という条件をつけることができる(日本語についてはすでに 2 文字以上になっている)。{1,44} を {2,44} に修正するだけだ。

 s/^$euc_jp*?([A-Za-z][A-Za-z\']{2,44}|$non_symbol_euc_jp{2,45})([_\-,\.\"\'\)\?!:;\/& \t\n\r]{0,5}|$)//ox

また、細かいところでは最後の「x」は不要なので削除し、さらに 2 つめの括弧は後方参照としては使わないので「?:」をつける(これらはほとんど変わらないだろうけれど)。
最終的には

 # In Japanese, 2 characters words are common, so care about
# words between 2 and 45 characters

while ( $line =~ s/^$euc_jp*?([A-Za-z][A-Za-z\']{2,44}|$non_symbol_euc_jp{2,45})(?:[_\-,\.\"\'\)\?!:;\/& \t\n\r]{0,5}|$)//o ) {
if ( ( $self->{in_headers__} == 0 ) && ( $self->{first20count__} < 20 ) ) {
$self->{first20count__} += 1;
$self->{first20__} .= " $1";
}

update_word($self, $1, $encoded, '', '[_\-,\.\"\'\)\?!:;\/ &\t\n\r]'."|$symbol_euc_jp", $prefix);
}

というようなコードになった。見た目にもかなりすっきりした。この処理の部分だけで、元のコードよりも 1.3〜1.7 倍くらい高速になった(幅があるのは日本語だけの場合や英語だけの場合で速度差が生じるため。英文の方がより有利だ)。

  • spacedout の処理を見直す

上記のコードをテストしている間に、add_line にはもっと大きなボトルネックが存在しているのを発見した。それは、例えば「V.I.A.G.R.A」のように文字と文字の間に空白やピリオドなどを挟み込むことによってフィルタを欺こうとしている箇所(POPFile ではこれを trick:spacedout という特別なキーワードとして認識する)を見つける処理を行っている部分だ。POPFile 0.22.5 では、次のようなコードだ。

 # Deal with runs of alternating spaces and letters

foreach my $space (' ', '\'', '*', '^', '`', ' ', '\38', '.' ){
while ( $line =~ s/( |^)(([A-Z]\Q$space\E){2,15}[A-Z])( |\Q$space\E|[!\?,])/ /i ) {
my $original = "$1$2$4";
my $word = $2;
print "$word ->" if $self->{debug__};
$word =~ s/[^A-Z]//gi;
print "$word\n" if $self->{debug__};
$self->update_word( $word, $encoded, ' ', ' ', $prefix);
$self->update_pseudoword( 'trick', 'spacedout', $encoded, $original );
}
}

間に挟まる文字の候補を変えながら何度も調べている。しかも、$space の内容が毎回変わるので o オプションも使えない。これは非常に重たい処理だ。ここを、ループや変数を使わずに書き換えることができればかなり高速化が期待できる。そこで、最初の正規表現を見直して

 s/( |^)([A-Za-z]([\'\*^`&\. ]|  )(?:[A-Za-z]\3){1,14}[A-Za-z])( |\3|[!\?,]|$)/ /

というように処理することにしてみた。すると、この部分だけの速度比較で 40 倍近く高速化された。これはかなり気持ちがよかった。ここは日本語処理とは無関係なところなので、英語環境で動かしている場合でもかなり効果があるはずだ。

  • Encode オブジェクトの再利用

Encode::Guess でオブジェクトが得られている場合には、それを使って例えば

 $string = Encode::encode($to, $enc->decode($string));

とする方が、

 Encode::from_to($string, $from, $to);

よりも速い(from_to では、再度 find_encoding で $from と $to に該当するエンコードを探すことになるからだろう)。また、一度作成したオブジェクトを保存しておいて再利用するようにすればより効率が良くなる。修正部分が多くなってしまうのでそこまではしなかったが、上記の修正だけでも若干の高速化は期待できる。


以上のような修正を、次のバージョンには含めたいと考えている。ざっとテストしてみたところでは、0.22.5+分かち書きパッチの状態よりも 1.4 倍くらい速くなっているようだ。このくらい変化があれば、また違いが体感できるだろうか。