[Perl]UTF8-flagged strings affects regexps with the "i" modifier

HTML::FillInForm::Liteの使いどころという記事で,HTML::FillInForm::Liteが遅いということが取り上げられていた。
試しに記事内のベンチマークを行ったところ,確かに遅い。

# HTML::FillInForm 1.06
Benchmark: HTML::FillInForm vs. HTML::FillInForm::Lite
                  Rate fillinall_lite     fillinall fillinpart_lite   fillinpart
fillinall_lite   570/s             --          -52%            -76%         -78%
fillinall       1199/s           110%            --            -49%         -54%
fillinpart_lite 2349/s           312%           96%              --          -9%
fillinpart      2595/s           355%          116%             10%           --

ところで,このベンチマークの対象となっている文字列はutf8-flaggedだ。Perlではutf8フラグ付きの文字列に対するuc/lc/"i"正規表現修飾子は非常に遅いのだが,H::F::Liteでは/iを使っているので,試しに/iを使わないようにしてみると,速度が改善した*1。具体的にはm//iとしていたような正規表現をm/<[fF][oO][oO]>/と手動でignorecaseな正規表現に変換した*2

# HTML::FillInForm::Lite  1.07
fillinall       1185/s            --           -53%         -54%            -60%
fillinall_lite  2535/s          114%             --          -2%            -15%
fillinpart      2595/s          119%             2%           --            -13%
fillinpart_lite 2983/s          152%            18%          15%              --

従来の結果の通り,H::F::Liteのほうが常に高速になった。utf8 flags + /iが遅いだろうとは思っていたが,ここまで影響が大きいとは思わなかった。
そこで,純粋に/iの速度のみを比較するベンチマークをとってみた。
スクリプト

#!perl -w
use strict;
use Benchmark qw(:all);
# pronounced as 'zdrastvuiche'
my $s = qq{Здравствуйте\n};
my $w = $s;
utf8::decode($w);
cmpthese -1 => {
    'bytes without /i' => sub{
        $s =~ /$s/o for 1 .. 100;
     },
    'bytes with /i' => sub{
        $s =~ /$s/io for 1 .. 100;
     },
};
cmpthese -1 => {
     'utf8 without /i' => sub{
        $w =~ /$w/o for 1 .. 100;
     },
     'utf8 with /i' => sub{
        $w =~ /$w/io for 1 .. 100;
     },
};
__END__

結果:

                    Rate    bytes with /i bytes without /i
bytes with /i    21622/s               --             -54%
bytes without /i 46897/s             117%               --
                   Rate    utf8 with /i utf8 without /i
utf8 with /i      302/s              --            -99%
utf8 without /i 43530/s          14315%              --

この結果によれば,bytesでも/iによってパフォーマンスは50%程度落ちる。ところがutf8-flaggedの/iによるパフォーマンスの低下は尋常ではなく,/iによって1/140程度にまで性能が悪化する。2倍や10倍というレベルではない。H::F::Liteの性能が悪くなるわけだ。

もちろん,utf8-flaggedでなければ難しい処理もあるため,むやみにutf8-flaggedを避ける必要はない。たとえば今回使った/iのためのベンチマークのように,非ASCII文字でも簡単にuc/lc/iできるが,これを手動で行うのは難しい。

なお,uc/lc/iがこんなにも遅いのは,一文字毎にハッシュマップを引くからだ。また,これらの文字データは外部ファイルに保存されており,必要に応じてロードされる。
参考:

#!perl -w
use utf8;
# pronounced as 'zdrastvuiche'
my $s = qq{Здравствуйте\n};
binmode STDOUT, ':utf8';
print 'uc: ', uc $s;
print 'lc: ', lc $s;
print join(" ", sort keys %INC), "\n";
__END__

結果:

uc: ЗДРАВСТВУЙТЕ
lc: здравствуйте
strict.pm unicore/Canonical.pl unicore/Exact.pl
unicore/PVA.pl unicore/To/Lower.pl unicore/To/Upper.pl
utf8.pm utf8_heavy.pl warnings.pm

*1:この修正を加えたものを1.07としてリリースした。この記事で使用したベンチマークスクリプトも入れており,perl example/benchmark-with-tt.plで実行できる。

*2:この手法はHTML::Templateで使われている古典的な最適化である。