Perl Hackers Hub

第55回 Perlコードの高速化―文字列処理の時間短縮とデータ構造の効率化(2)

この記事を読むのに必要な時間:およそ 6 分

前回の(1)こちらから。

文字列

まず紹介するのは文字列処理です。

文字列処理には,Perlの武器である正規表現が欠かせません。正規表現の記述ルールが手に馴染んでくると,文字列の照合や取り出し,書き換えにはまず正規表現を使うでしょう。しかし,正規表現で書いた処理には,組込み関数や演算子を使って書き換えられるものがいくつかあります。そして,多くの場合では関数や演算子を使った処理のほうが高速です。

本節では,正規表現を使った処理を関数や演算子で置き換える例を紹介します。

特定文字の削除にはy///を使う

不要な文字や,行末の改行コード\n)⁠タブ文字\tなどを空白文字に置き換える処理(削除)は,頻繁に行うものでしょう。特定文字を削除するときに最も使うのが置換演算子です。

たとえばメールアドレスは<neko@nyaan.jp>のように<>で囲まれた形式で現れることがあります。SMTPSimple Mail Transfer Protocolでは必要な<>ですが,メールアドレスだけが欲しいときは不要です。

次のコードは,これらを削除(空文字に置換)する方法を,正規表現の文字クラスを使う置換演算子s///と,同じく置換演算子y///とで比べた例です。

s-vs-y.pl

my $Email = '<neko@nyaan.jp>'; # < と > を削除したい
sub ss {
    my $v = $Email;
    $v =~ s/[<>]//g; # 正規表現の文字クラス
    return $v;
}
sub yy {
    my $v = $Email;
    $v =~ y/<>//d; # 置換対象を1文字単位で列挙
    return $v;
}
考察:y///s///に圧勝

表1に示すとおり,そしてperlperf - Perlの性能と最適化のテクニック - perldoc.jpでも言及されているとおり,y///置換演算子が圧倒的に速い結果となりました。保守性につながる読みやすさも損なわれていませんし,4.6倍も速いなら,y///を使った文字単位での置換が処理時間の短縮となります。

表1 特定文字の削除の比較

比較項目秒間実行回数速度比
s///895,5221.00
y///4,137,9314.62

先頭文字の確認にはindex()を使う

HTTPと同様にSMTPでもステータスコードの200番台は成功を,400番台と500番台はエラーを表します。接続先の応答が成功か失敗かは,まずステータスコードの先頭1文字を見てエラー判定を行い,そのあとでステータスコードの残り2桁やエラーメッセージの詳細を確認します。

次のコードは,SMTP応答コードの先頭1文字が45であることを,正規表現の文字クラスを使った方法と,index()関数を使った方法で比較したものです。

check-the-first-character.pl

my $Status = '550';
sub regexp { return 1 if $Status =~ /\A[45]/ }
sub index2 {
    # 4が文字列の先頭なら位置が0になる
    return 1 if index($Status, '4') == 0;

    # 5が文字列の先頭なら位置が0になる
    return 1 if index($Status, '5') == 0;
}
考察:index()が正規表現に辛勝

index()関数は2回も実行されていますが,表2に示すとおりindex2サブルーチンのほうがやや高速に動作します。逆に考えると,関数に比べて正規表現は実行時間が長くなります。1.1倍程度の差ですが,ループ内で何度も呼ばれるコードであれば,このわずかな差が大きな差となってくるでしょう。

表2 先頭1文字の確認の比較

比較項目秒間実行回数速度比
\A[45]4,411,7651.00
index( )5,000,0001.13

文字列の先頭/末尾の確認にはindex()とsubstr()を使う

メールアドレスをユーザー名やログインIDとして使っているWebサービスでは,入力された文字列がメールアドレスであることを確認するコードが動いていることでしょう。ここでは,ある文字列をメールアドレスと判定する簡易なコードを比較します。

次のコードは,ベンチマークを目的とした簡易なメールアドレス判定コードです。各サブルーチンは,<で始まり@を含み>で終わる文字列をメールアドレスとみなします。これを,正規表現と,2回のindex()関数,substr()関数の併用で比べます。

is-email-address-or-not.pl

my $Email = '<neko@nyaan.jp>';
sub regexp { return 1 if $Email =~ /\A<.*@.*>\z/ }
sub indexs {
    # index()とsubstr()
    if( index($Email, '<') == 0 && # 先頭文字が<
        index($Email, '@') > -1 && # 文字列に@を含む
        substr($Email, -1, 1) eq '>' ) { # 末尾文字が>
        return 1
    }
}
考察:index()とsubstr()組が正規表現に快勝

表3に示すとおり,1つの正規表現よりも,2回のindex()関数と1回のsubstr()関数のほうが1.8倍ほど高速でした。しかし,サブルーチンindexsのコードは少し長くて読みにくいように思えます。

「速いは正義」と先述しましたが,読みにくくなる場合は一考の余地があります。一度しか実行されないコードであれば読みやすさを優先,何千回も実行されるコードであれば速度を優先するといった,背景に合わせた判断が必要でしょう。

表3 文字列の先頭/末尾の確認の比較

比較項目秒間実行回数速度比
正規表現2,068,9661.00
index( ),substr( )3,658,5371.77

文字列の比較にはlc()を使う

大文字と小文字が入り混じった文字列を比較するとき,みなさんはどのようなコードを書いているでしょうか。筆者は,lc()関数で小文字に変換してから比較する方法と,正規表現を使う方法の2通りが真っ先に思い浮かびます。正規表現を使う場合も,大文字と小文字を区別しないi修飾子を用いるか,大文字か小文字かを断定できない箇所だけを文字クラスで記述するかの2通りがあります。

実例で挙げるのはメールアドレスです。ほとんどのメール関連ソフトウェアの実装は,メールアドレスの大文字と小文字を区別しません。バウンスメールでは,ローカルパート注1Mailer-Daemonmailer-daemonのような表記をよく見かけるでしょう。

次のコードは,ローカルパートがmailer-daemon(大文字が混在する表記も含む)であるかどうかの確認を行う方法を,i修飾子を指定した正規表現,文字クラスを使った正規表現,小文字に変換するlc()関数とで比べた例です。

lc-vs-i-modifier-vs-char-class.pl

my $Text = 'Mailer-Daemon';
sub regex1 {
    # /正規表現/i(大文字/小文字の区別なし)
    return 1 if $Text =~ /\Amailer-daemon\z/i;
}
sub regex2 {
    # 文字クラスを使う正規表現(MとDだけ大文字を考慮)
    return 1 if $Text =~ /\A[Mm]ailer-[Dd]aemon\z/;
}
sub lcfunc {
    # lc()関数 (小文字にしてから比較)
    return 1 if lc $Text eq 'mailer-daemon';
}
考察:lc()が正規表現に圧勝

表4に示すとおり,i修飾子を使う正規表現と,文字クラスを使う正規表現は僅差でしたが,いったんlc()関数で小文字に変換してから比較するlcfuncサブルーチンは,正規表現双方の2.3倍も速く動作しました。読みやすさも向上しており,速度と保守性が両立する例です。

表4 文字列の比較の比較

比較項目秒間実行回数速度比
/正規表現/i2,752,2941.00
文字クラス2,857,1431.04
lc( )6,451,6132.34

そして,先頭文字の確認でindex()関数が正規表現よりも速かったのと同様,関数は正規表現よりも速く動作します。逆に,いま関数を使っているコードを正規表現に書き換えると,遅くなる可能性が高いでしょう。

注1)
メールアドレスの@の左側です。

メタ文字を含む正規表現は読みやすいものを使う

.+などの正規表現で特別な意味を持つメタ文字を,文字そのものとして一致させたい場合に考えられる書き方の比較です。たとえばURLやメールアドレスに現れるホスト名やドメイン名の照合で.を扱うときにどう書くか,です。

次のコードでは,メールアドレスのドメイン部分に含まれるメタ文字を\.とエスケープするか,文字クラスを使って[.]と表現するか,あるいは/\Q...\E/とメタ文字を無効にするエスケープシーケンスを使うか,を比べます。

escape-vs-char-class-vs-qe.pl

my $Email = 'kijitora@neko.nyaan.jp';
sub bs { return 1 if $Email =~ /neko\.nyaan\.jp/ }
sub cc { return 1 if $Email =~ /neko[.]nyaan[.]jp/ }
sub qe { return 1 if $Email =~ /\Qneko.nyaan.jp\E/ }
考察:実力伯仲

表5に示すとおり,正規表現の書き方としては,それぞれのコードに大差はありませんでした。ただし,大差がなかったからベンチマークが無駄であったとは言えません。差がないことも結果の一つで,どれを選択してもよいから読みやすいコードを採用する,と判断ができます。

表5 メタ文字のエスケープの比較

比較項目秒間実行回数速度比
\エスケープ5,042,0171.00
文字クラス5,217,3911.03
\Q\E5,263,1581.04

読みやすさには多少の個人差がありますが,\でメタ文字をエスケープする表記を好まない筆者は,文字列全体を\Q\Eで挟むのが最も読みやすいと思います。

著者プロフィール

東邦之(あずまくにゆき a.k.a. azumakuniyuki)

データセンターには行かないインフラエンジニアとプログラマでPerlとRubyを書く。特にSMTPと縁がある。

2008年から株式会社Cubicrootに所属,2010年にbounceHammerを開発,2012年は大阪でのPerl入学式お手伝い,2014年にPerl版Sisimaiを,2016年にはRuby版Sisimaiを開発してOSSとして公開,現在も開発中。2018年は迷い猫を保護してお世話をした。

一年のうち360日は京都にいる。趣味は外猫の観察と酒場と銭湯。