web-dev-qa-db-ja.com

このPerl正規表現で `\ s +`が `\ s \ s *`よりもずっと速いのはなぜですか?

\s*(または\s\s*)を\s+に置き換えると、この入力が高速化されるのはなぜですか?

use Benchmark qw(:all);
$x=(" " x 100000) . "_\n";
$count = 100;
timethese($count, {
    '/\s\s*\n/' => sub { $x =~ /\s\s*\n/ },
    '/\s+\n/' => sub { $x =~ /\s+\n/ },
});

オンラインバージョンへのリンク

コード内の正規表現s/\s*\n\s*/\n/gが遅いことに気付きました-所々にいくつかの非スペースを含む多くのスペースで構成される450KBの入力ファイルと、最後に最終改行があると、正規表現がハングして終了しませんでした。

直観的に正規表現をs/\s+\n/\n/g; s/\n\s+/\n/g;に置き換えましたが、すべてうまくいきました。

しかし、なぜそんなに速いのでしょうか? re Debug => "EXECUTE"を使用した後、\s+バージョンが1回の反復でのみ実行されるように何らかの形で最適化されていることに気付きました。 http://Pastebin.com/0Ug6xPiQ

Matching REx "\s*\n" against "       _%n"
Matching stclass ANYOF{i}[\x09\x0a\x0c\x0d ][{non-utf8-latin1-all}{unicode_all}] against "       _%n" (9 bytes)
   0 <> <       _%n>         |  1:STAR(3)
                                  SPACE can match 7 times out of 2147483647...
                                  failed...
   1 < > <      _%n>         |  1:STAR(3)
                                  SPACE can match 6 times out of 2147483647...
                                  failed...
   2 <  > <     _%n>         |  1:STAR(3)
                                  SPACE can match 5 times out of 2147483647...
                                  failed...
   3 <   > <    _%n>         |  1:STAR(3)
                                  SPACE can match 4 times out of 2147483647...
                                  failed...
   4 <    > <   _%n>         |  1:STAR(3)
                                  SPACE can match 3 times out of 2147483647...
                                  failed...
   5 <     > <  _%n>         |  1:STAR(3)
                                  SPACE can match 2 times out of 2147483647...
                                  failed...
   6 <      > < _%n>         |  1:STAR(3)
                                  SPACE can match 1 times out of 2147483647...
                                  failed...
   8 <       _> <%n>         |  1:STAR(3)
                                  SPACE can match 1 times out of 2147483647...
   8 <       _> <%n>         |  3:  EXACT <\n>(5)
   9 <       _%n> <>         |  5:  END(0)
Match successful!
Matching REx "\s+\n" against "       _%n"
Matching stclass SPACE against "       _" (8 bytes)
   0 <> <       _%n>         |  1:PLUS(3)
                                  SPACE can match 7 times out of 2147483647...
                                  failed...

改行が存在しない場合、Perl 5.10+はすぐに(実行せずに)正規表現に失敗します。改行の場所を使用して、検索の量を減らすのではないかと考えています。上記のすべての場合、関連するバックトラッキングを巧妙に削減するようです(通常、スペース文字列に対する/\s*\n/は指数関数的な時間がかかります)。 \s+バージョンが非常に高速である理由についての洞察を誰でも提供できますか?

また、\s*?は高速化を提供しません。

56
rjh

パターンの先頭に「プラス」ノード(例:\s+)があり、ノードが一致しない場合、正規表現エンジンは障害点まで前方にスキップして再試行します。一方、\s*を使用すると、エンジンは一度に1文字だけ前進します。

Yves Ortonはこの最適化をうまく説明しています here

開始クラスの最適化には、「有効な開始位置ごとに試行する」(doevery)と「フリップフロップモード」(!doevery)の2つのモードがあり、シーケンスの最初の有効な開始位置のみを試行します。

/(\ d +)X /と文字列 "123456Y"を考えます。 "123456"に一致した後Xに一致しない場合、 "23456"の後にも一致しません(邪悪なトリックがないと仮定すると、とにかく最適化が無効になります)、チェック/ fails /まで実際にスキップしてから実際の一致を探し始めることができます。これはフリップフロップモードです。

/\s+/はフリップフロップモードをトリガーします。 /\s*//\s\s*/、および/\s\s+/はありません。この最適化は、\s*のような「スター」ノードには適用できません。ゼロ文字と一致する可能性があるため、シーケンス内の1つのポイントでの障害は、同じシーケンスでの障害を示すものではありません。


これは、各正規表現のデバッグ出力で確認できます。スキップされた文字を^で強調表示しました。これを比較します(一度に4文字スキップします):

$ Perl -Mre=Debug,MATCH -e'"123 456 789 x" =~ /\d+x/'
   ...
   0 <> <123 456 78>         |  1:PLUS(3)
                                  POSIXD[\d] can match 3 times out of 2147483647...
                                  failed...
   4 <123 > <456 789 x>      |  1:PLUS(3)
      ^^^^
                                  POSIXD[\d] can match 3 times out of 2147483647...
                                  failed...
   8 <23 456 > <789 x>       |  1:PLUS(3)
         ^^^^
                                  POSIXD[\d] can match 3 times out of 2147483647...
                                  failed...

これに(一度に1つまたは2つの文字をスキップ):

$ Perl -Mre=Debug,MATCH -e'"123 456 789 x" =~ /\d*x/'
   ...
   0 <> <123 456 78>         |  1:STAR(3)
                                  POSIXD[\d] can match 3 times out of 2147483647...
                                  failed...
   1 <1> <23 456 789>        |  1:STAR(3)
      ^
                                  POSIXD[\d] can match 2 times out of 2147483647...
                                  failed...
   2 <12> <3 456 789 >       |  1:STAR(3)
       ^
                                  POSIXD[\d] can match 1 times out of 2147483647...
                                  failed...
   4 <123 > <456 789 x>      |  1:STAR(3)
        ^^
                                  POSIXD[\d] can match 3 times out of 2147483647...
                                  failed...
   5 <123 4> <56 789 x>      |  1:STAR(3)
          ^
                                  POSIXD[\d] can match 2 times out of 2147483647...
                                  failed...
   6 <23 45> <6 789 x>       |  1:STAR(3)
          ^
                                  POSIXD[\d] can match 1 times out of 2147483647...
                                  failed...
   8 <23 456 > <789 x>       |  1:STAR(3)
           ^^
                                  POSIXD[\d] can match 3 times out of 2147483647...
                                  failed...
   9 <23 456 7> <89 x>       |  1:STAR(3)
             ^
                                  POSIXD[\d] can match 2 times out of 2147483647...
                                  failed...
  10 <23 456 78> <9 x>       |  1:STAR(3)
              ^
                                  POSIXD[\d] can match 1 times out of 2147483647...
                                  failed...
  12 <23 456 789 > <x>       |  1:STAR(3)
               ^^
                                  POSIXD[\d] can match 0 times out of 2147483647...
  12 <23 456 789 > <x>       |  3:  EXACT <x>(5)
  13 <23 456 789 x> <>       |  5:  END(0)

/\s\s+/はパターンの先頭にないため、最適化は\s+に適用されないことに注意してください。 /\s\s+/(論理的に/\s{2,}/と同等)と/\s\s*/(論理的に/\s+/と同等)はおそらくcouldただし、最適化されます。 Perl5-porters でどちらかが努力する価値があるかどうかを尋ねることは理にかなっているかもしれません。


興味がある場合は、コンパイル時に正規表現にPREGf_SKIPフラグを設定することにより、「フリップフロップモード」が有効になります。 5.24.0ソースの regcomp.c の7344行と7405行、および regexec.c の1585行目のコードを参照してください。

20

まず、結果の正規表現が同じ意味を保持しない場合でも、正規表現を\s*0\s+0に減らして、入力として(" " x 4) . "_0"を使用しましょう。懐疑論者については、 here ラグがまだ存在していることがわかります。

次のコードを考えてみましょう。

$x = (" " x 4) . "_ 0";
$x =~ /\s*0/; # The slow line 
$x =~ /\s+0/; # The fast line

use re debugcolor;を少し掘り下げると、次の出力が得られます。

Guessing start of match in sv for REx "\s*0" against "    _0"
Found floating substr "0" at offset 5...
start_shift: 0 check_at: 5 s: 0 endpos: 6 checked_upto: 0
Does not contradict STCLASS...
Guessed: match at offset 0
Matching REx "\s*0" against "    _0"
Matching stclass ANYOF_SYNTHETIC[\x09-\x0d 0\x85\xa0][{unicode_all}] against "    _0" (6 bytes)
   0 <    _0>|  1:STAR(3)
                                  POSIXD[\s] can match 4 times out of 2147483647...
                                  failed...
   1 <    _0>|  1:STAR(3)
                                  POSIXD[\s] can match 3 times out of 2147483647...
                                  failed...
   2 <    _0>|  1:STAR(3)
                                  POSIXD[\s] can match 2 times out of 2147483647...
                                  failed...
   3 <    _0>|  1:STAR(3)
                                  POSIXD[\s] can match 1 times out of 2147483647...
                                  failed...
   5 <    _0>|  1:STAR(3)
                                  POSIXD[\s] can match 0 times out of 2147483647...
   5 <    _0>|  3:  EXACT <0>(5)
   6 <    _0>|  5:  END(0)
Match successful!

-----------------------

Guessing start of match in sv for REx "\s+0" against "    _0"
Found floating substr "0" at offset 5...
start_shift: 1 check_at: 5 s: 0 endpos: 5 checked_upto: 0
Does not contradict STCLASS...
Guessed: match at offset 0
Matching REx "\s+0" against "    _0"
Matching stclass POSIXD[\s] against "    _" (5 bytes)
   0 <    _0>|  1:PLUS(3)
                                  POSIXD[\s] can match 4 times out of 2147483647...
                                  failed...
Contradicts stclass... [regexec_flags]
Match failed

Perlは 失敗に対して最適化される のようです。最初に定数文字列(O(N)のみを消費します)を探します。ここでは、0を探します:Found floating substr "0" at offset 5...

次に、チェックする最小文字列全体に対して、正規表現のvariable部分で、それぞれ\s*および\s+で始まります。

Matching REx "\s*0" against "    _0"
Matching stclass ANYOF_SYNTHETIC[\x09-\x0d 0\x85\xa0][{unicode_all}] against "    _0" (6 bytes)
Matching REx "\s+0" against "    _0"
Matching stclass POSIXD[\s] against "    _" (5 bytes) # Only 5 bytes because there should be at least 1 "\s" char

その後、stclass要件を満たす最初の位置、ここでは位置0を探します。

  • \s*0
    • 0から始まり、4つのスペースを見つけて失敗します。
    • 1から始まり、3つのスペースを見つけて失敗します。
    • 2から始まり、2つのスペースを見つけて失敗します。
    • 3から始まり、1つのスペースを見つけて失敗します。
    • 4から始まり、0個のスペースを見つけて、失敗しません;
    • 正確な0を見つける
  • \s+0
    • 0から始まり、4つのスペースを見つけて失敗します。スペースの最小数が一致しないため、正規表現はすぐに失敗します。

Perl正規表現の最適化を楽しみたい場合は、次の正規表現/ *\nおよび/ * \nを検討できます。一見、同じように見え、同じ意味を持っています...しかし、(" " x 40000) . "_\n"に対して実行すると、最初の可能性がすべての可能性をチェックし、2番目の可能性が" \n"を探してすぐに失敗します。

バニラ、最適化されていない正規表現エンジンでは、両方の正規表現が突発的なバックトラッキングを引き起こす可能性があります。ただし、上記の例では、find floating substr "0%n"に最適化されているため、2番目はPerlで失敗しません。


Jeff Atwoodのブログ で別の例を見ることができます。

また、問題は\sの考慮事項ではなく、xx*の代わりにx+が使用されるパターンであることに注意してください。 sの例 および regex爆発的数量詞

このような短い例では、動作は「検索可能」ですが、複雑なパターンでプレイし始めると、簡単に見つけることはできません。たとえば、次のようになります。 正規表現がプログラムをハングさせる(CPU使用率100%)

28
Thomas Ayoub

_\s+\n_では、_\n_の前の文字がSPACEである必要があります。

use re qw(debug)によれば、コンパイルでは、入力で最初にチェックされる部分文字列_\n_までの既知のスペース数の直線文字列が必要であることを確立します。次に、固定長のスペースのみの部分文字列を入力の残りの部分と照合し、___になると失敗します。入力に含まれるスペースの数に関係なく、チェックするのは1つの可能性です。 (__\n_がさらにある場合、デバッグ出力ごとにそれぞれが同様に直接失敗することがわかります。)

このように見れば、それはあなたがほとんど期待する最適化であり、かなり具体的な検索パターンを利用し、この入力でうまくやっています。明らかにこの種の分析を行わない他のエンジンと比較した場合を除きます。

_\s*\n_では、これは当てはまりません。 _\n_が見つかり、前の文字がスペースでない場合、_\s*_は何も許可しない(ゼロ文字)ため、検索は失敗しません。固定長の部分文字列も存在せず、バックトラックゲームに含まれています。

14
zdim

正規表現エンジンの内部はわかりませんが、_\s+_が何らかの形でsame as _\s\s*_。2番目のスペースではスペースに一致し、増え続けるスペースのマッチングを試みますが、最初のスペースでは一致しないとすぐに結論付けます。

use re qw( Debug );を使用した出力は、はるかに短い文字列を使用して、これを明確に示しています。

test_re.pl

_#!/usr/bin/env Perl
use re qw(debug);

$x=(" " x 10) . "_\n";
print '-'x50 . "\n";
$x =~ /\s+\n/;
print '-'x50 . "\n";
$x =~ /\s\s*\n/;
print '-'x50 . "\n";
_

出力

_Compiling REx "\s+\n"
Final program:
    1: PLUS (3)
    2:   SPACE (0)
    3: EXACT <\n> (5)
    5: END (0)
floating "%n" at 1..2147483647 (checking floating) stclass SPACE plus minlen 2
Compiling REx "\s\s*\n"
Final program:
    1: SPACE (2)
    2: STAR (4)
    3:   SPACE (0)
    4: EXACT <\n> (6)
    6: END (0)
floating "%n" at 1..2147483647 (checking floating) stclass SPACE minlen 2
--------------------------------------------------
Guessing start of match in sv for REx "\s+\n" against "          _%n"
Found floating substr "%n" at offset 11...
    start_shift: 1 check_at: 11 s: 0 endpos: 11
Does not contradict STCLASS...
Guessed: match at offset 0
Matching REx "\s+\n" against "          _%n"
Matching stclass SPACE against "          _" (11 bytes)
   0 <> <          >         |  1:PLUS(3)
                                  SPACE can match 10 times out of 2147483647...
                                  failed...
Contradicts stclass... [regexec_flags]
Match failed
--------------------------------------------------
Guessing start of match in sv for REx "\s\s*\n" against "          _%n"
Found floating substr "%n" at offset 11...
    start_shift: 1 check_at: 11 s: 0 endpos: 11
Does not contradict STCLASS...
Guessed: match at offset 0
Matching REx "\s\s*\n" against "          _%n"
Matching stclass SPACE against "          _" (11 bytes)
   0 <> <          >         |  1:SPACE(2)
   1 < > <         _>        |  2:STAR(4)
                                  SPACE can match 9 times out of 2147483647...
                                  failed...
   1 < > <         _>        |  1:SPACE(2)
   2 <  > <        _>        |  2:STAR(4)
                                  SPACE can match 8 times out of 2147483647...
                                  failed...
   2 <  > <        _>        |  1:SPACE(2)
   3 <   > <       _%n>      |  2:STAR(4)
                                  SPACE can match 7 times out of 2147483647...
                                  failed...
   3 <   > <       _%n>      |  1:SPACE(2)
   4 <    > <      _%n>      |  2:STAR(4)
                                  SPACE can match 6 times out of 2147483647...
                                  failed...
   4 <    > <      _%n>      |  1:SPACE(2)
   5 <     > <     _%n>      |  2:STAR(4)
                                  SPACE can match 5 times out of 2147483647...
                                  failed...
   5 <     > <     _%n>      |  1:SPACE(2)
   6 <      > <    _%n>      |  2:STAR(4)
                                  SPACE can match 4 times out of 2147483647...
                                  failed...
   6 <      > <    _%n>      |  1:SPACE(2)
   7 <       > <   _%n>      |  2:STAR(4)
                                  SPACE can match 3 times out of 2147483647...
                                  failed...
   7 <       > <   _%n>      |  1:SPACE(2)
   8 <        > <  _%n>      |  2:STAR(4)
                                  SPACE can match 2 times out of 2147483647...
                                  failed...
   8 <        > <  _%n>      |  1:SPACE(2)
   9 <         > < _%n>      |  2:STAR(4)
                                  SPACE can match 1 times out of 2147483647...
                                  failed...
   9 <         > < _%n>      |  1:SPACE(2)
  10 <          > <_%n>      |  2:STAR(4)
                                  SPACE can match 0 times out of 2147483647...
                                  failed...
Contradicts stclass... [regexec_flags]
Match failed
--------------------------------------------------
Freeing REx: "\s+\n"
Freeing REx: "\s\s*\n"
_
7
xxfelixxx