前回の
文字列
まず紹介するのは文字列処理です。
文字列処理には、
本節では、
特定文字の削除にはy///を使う
不要な文字や、\n)、\t)
たとえばメールアドレスは<neko@nyaan.のように<と>で囲まれた形式で現れることがあります。SMTP<と>ですが、
次のコードは、s///と、y///とで比べた例です。
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に示すとおり、y///置換演算子が圧倒的に速い結果となりました。保守性につながる読みやすさも損なわれていませんし、y///を使った文字単位での置換が処理時間の短縮となります。
| 比較項目 | 秒間実行回数 | 速度比 | 
|---|---|---|
| s/// | 895,522 | 1. | 
| y/// | 4,137,931 | 4. | 
先頭文字の確認にはindex()を使う
HTTPと同様にSMTPでもステータスコードの200番台は成功を、
次のコードは、4か5であることを、index()関数を使った方法で比較したものです。
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回も実行されていますが、index2サブルーチンのほうがやや高速に動作します。逆に考えると、
| 比較項目 | 秒間実行回数 | 速度比 | 
|---|---|---|
| \A[45] | 4,411,765 | 1. | 
| index( ) | 5,000,000 | 1. | 
文字列の先頭/末尾の確認にはindex()とsubstr()を使う 
メールアドレスをユーザー名やログインIDとして使っているWebサービスでは、
次のコードは、<で始まり@を含み>で終わる文字列をメールアドレスとみなします。これを、index()関数、substr()関数の併用で比べます。
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に示すとおり、index()関数と1回のsubstr()関数のほうが1.indexsのコードは少し長くて読みにくいように思えます。
「速いは正義」
| 比較項目 | 秒間実行回数 | 速度比 | 
|---|---|---|
| 正規表現 | 2,068,966 | 1. | 
| index( )、 | 3,658,537 | 1. | 
文字列の比較にはlc()を使う
大文字と小文字が入り混じった文字列を比較するとき、lc()関数で小文字に変換してから比較する方法と、i修飾子を用いるか、
実例で挙げるのはメールアドレスです。ほとんどのメール関連ソフトウェアの実装は、Mailer-Daemonやmailer-daemonのような表記をよく見かけるでしょう。
次のコードは、mailer-daemoni修飾子を指定した正規表現、lc()関数とで比べた例です。
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サブルーチンは、
| 比較項目 | 秒間実行回数 | 速度比 | 
|---|---|---|
| /正規表現/i | 2,752,294 | 1. | 
| 文字クラス | 2,857,143 | 1. | 
| lc( ) | 6,451,613 | 2. | 
そして、index()関数が正規表現よりも速かったのと同様、
メタ文字を含む正規表現は読みやすいものを使う
.や+などの正規表現で特別な意味を持つメタ文字を、
次のコードでは、\.とエスケープするか、[.]と表現するか、/\Q...\E/とメタ文字を無効にするエスケープシーケンスを使うか、
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,042,017 | 1. | 
| 文字クラス | 5,217,391 | 1. | 
| \Q\E | 5,263,158 | 1. | 
読みやすさには多少の個人差がありますが、\でメタ文字をエスケープする表記を好まない筆者は、\Qと\Eで挟むのが最も読みやすいと思います。
データ構造
本節では、
ある値が一覧に含まれるかはexists()を使う
ある文字列が一覧で定義した文字列と一致するかどうかを確認することは多いでしょう。たとえば、
次のコードでは、Reply-Toヘッダが一覧に含まれることの確認方法を、grep()関数と、exists()関数で比べたものです。
my $A = ['From', 'Reply-To', 'To'];
my $H = {'From' => 1, 'Reply-To' => 1, 'To' => 1};
sub usegrep {
    my $v = 'Reply-To';
    return 1 if grep { $v eq $_ } @$A;
}
sub hashkey {
    my $v = 'Reply-To';
    return 1 if exists $H->{ $v };
}考察:exists()がgrep()に快勝
表6に示すとおり、
| 比較項目 | 秒間実行回数 | 速度比 | 
|---|---|---|
| grep() | 2,166,065 | 1. | 
| exists() | 4,137,931 | 1. | 
では、grep-list-vs-exists-hash-100.に、exists()関数でハッシュのキーを調べる方法が20倍ほど高速でした。
このように大きな差が生まれる理由は、grep()関数exists()関数でハッシュのキーを確認する方法は計算量がO(1)です。つまり、
さて、Devel::Sizeモジュールを使うと、total_で計測したメモリサイズは、$Aが214バイト、$Hが380バイトでした。表7に示すとおり、
| 要素数 | 配列 | ハッシュ | サイズ比 | 
|---|---|---|---|
| 100 | 7. | 10. | 1. | 
| 10,000 | 781. | 1. | 1. | 
| 1,000,000 | 76. | 102. | 1. | 
要素数の少ないデータには配列を使う
前項のベンチマークでは配列よりもハッシュのキーが圧倒的に速かったため、
たとえばPerlのコアモジュールであるTime::Pieceは内部で日付データを配列として保持
次のコードは、Final-Recipient:の3要素を配列とハッシュで定義し、Final-Recipient:neko@nyaan.となるコードを、
my $A = ['final-recipient', 'rfc822', 'neko@nyaan.jp'];
my $H = {
    'field' => 'final-recipient',
    'type' => 'rfc822',
    'value' => 'neko@nyaan.jp'
};
sub ar { return $A->[0].': '.$A->[2] }
sub hs { return $H->{'field'}.': '.$H->{'value'} }考察:配列がハッシュに快勝
表8に示すとおり、
| 比較項目 | 秒間実行回数 | 速度比 | 
|---|---|---|
| ハッシュ | 4,761,905 | 1. | 
| 配列 | 6,185,567 | 1. | 
エラーメッセージの照合には配列を使う
バウンスメールに現れるエラーメッセージは多種多様です。メールサーバソフトウェアごと、
次のコードは、x修飾子grep()関数とindex()関数で、
my $Fa = 'Delivery failed, blacklisted by rbl';
my $Re = qr{(?>
    access[ ]denied[.][ ]ip[ ]name[ ]lookup[ ]failed
    |... # エラーメッセージパターンの正規表現がいくつか
    |blacklisted[ ]by
    )
}x;
my $Ar = [
    'access denied. ip name lookup failed',
    ..., # エラーメッセージ文字列がいくつか
    'blacklisted by',
];
sub regex { return 1 if $Fa =~ $Re } # 正規表現で照合
sub grep1 {
    # 配列に対するgrep()で照合
    return 1 if grep { index($Fa, $_) > -1 } @$Ar;
}考察:配列に対するgrep()が正規表現に圧勝
表9に示すとおり、grep()関数で回してindex()関数で照合するほうが、
| 比較項目 | 秒間実行回数 | 速度比 | 
|---|---|---|
| 正規表現 | 545,455 | 1. | 
| 配列に対するgrep( ) | 1,463,415 | 2. | 
正規表現と配列、Devel::Sizeモジュールのtotal_で計測したメモリサイズは、$Reは216バイト、$Arは457バイトでした。メモリ使用量は2倍以上の差ですが、
配列の末尾を参照するには負の添え字を使う
本項は、$#配列名よりも、
次のコードは、$#配列名を使った例と負の添え字を使った例で、1から100までの数字を入れた配列に対して、100)99)
my @A = (1..100);
sub ds { return ($A[$#A] + $A[$#A - 1]) }
sub ni { return ($A[-1] + $A[-2]) }考察:負の添え字が$#に楽勝
表10のように、$v[-1]と書きましょう。
| 比較項目 | 秒間実行回数 | 速度比 | 
|---|---|---|
| $v[$#] | 4,838,710 | 1. | 
| $v[-1] | 10,909,091 | 2. | 
ループ処理にはforeachを使う
本項も前項と同じく、forとforeachとwhileの3つの選択肢を比べましょう。
次のコードでは、1から256までの数字を要素に持つ配列から、forforeachとwhileを比較します。
sub loop1f {
    my $v = 0;
    my @p = (1..256);
    for( my $e = 0; $e < 256; $e++ ) {
        $v++ if $p[$e] % 2 == 0;
    }
    return $v;
}
sub loop2e {
    my $v = 0;
    my @p = (1..256);
    foreach my $e ( @p ) {
        $v++ if $e % 2 == 0;
    }
    return $v;
}
sub loop3w {
    my $v = 0;
    my @p = (1..256);
    while( my $e = shift @p ) {
        $v++ if $e % 2 == 0;
    }
    return $v;
}考察:foreachがwhileとC形式のforに快勝
表11に示すとおり、foreachの一強となりました。whileはイテレータが返す値を取り回すのにも使用しますが、forはめったに必要となりませんので、foreachの一択でしょう。
| 比較項目 | 秒間実行回数 | 速度比 | 
|---|---|---|
| while | 23,202 | 1. | 
| for | 29,326 | 1. | 
| foreach | 37,594 | 1. | 
しかし、whileをforeachに書き換えられるとは限りません。whileではshift()関数で代入時に偽となっていたコードが、foreachでは偽とならないケースがあります。詳しくは、fails-onforeach-loop.で確認してください。このような落とし穴もありますので、
<続きの
本誌最新号をチェック!
WEB+DB PRESS Vol.130
2022年8月24日発売
B5判/
定価1,628円
ISBN978-4-297-13000-8
- 特集1
 イミュータブルデータモデルで始める
 実践データモデリング
 業務の複雑さをシンプルに表現!
- 特集2
 いまはじめるFlutter
 iOS/Android両対応アプリを開発してみよう 
- 特集3
 作って学ぶWeb3
 ブロックチェーン、スマートコントラクト、 NFT 



