2016-07-18 9 views
55

なぜ\s*(またはさらに\s\s*)を\s+と置き換えると、この入力が高速化されるのでしょうか?このPerl正規表現の ` 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/ }, 
}); 

Link to online version

私は私のコードでは遅い正規表現s/\s*\n\s*/\n/gに気づいた - 最後にここにあるいくつかの非スペースでスペースがたくさんからなる450キロバイトの入力ファイル、および最後の改行を与えられたとき、正規表現は掛かっており、決して終わらない。

私は直感的に正規表現をs/\s+\n/\n/g; s/\n\s+/\n/g;に置き換えました。すべて正常でした。

なぜそれがずっと速いのですか?

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... 

http://pastebin.com/0Ug6xPiQ

は、私はPerlの5.10+はすぐに改行が存在しない場合(これを実行せずに)正規表現を失敗します知っている: re Debug => "EXECUTE"を使用した後、私は \s+バージョンは何とか一つだけの繰り返しで実行するように最適化されて気づきました。私はそれがしている検索の量を減らすために改行の場所を使用していると思われる。上記のすべての場合、それは巧妙に関連するバックトラッキングを減らすようです(通常、 /\s*\n/はスペース文字列に対して指数関数的な時間を要します)。誰でも \s+のバージョンがそれほど高速である理由を知ることができますか?

また、\s*?はスピードアップを提供していません。

+8

'\ s'も' \ n'にマッチするのに役立ちません。改行ではない空白文字は '[^ \ S \ n]'です。あるいは、 "水平空白" '\ h'を使うこともできます。 – Borodin

+0

'/ \ s * \ n /'と '/ \ s + \ n /' [ライブ参照](http://rextester.com/DSXF83795)との比較を絞り込むことができます。また、文字列が一致しない場合は高速になることに注意してください。試合の場合、同じ時間がかかるようです –

+0

@ThomasAyoub私はそれが比較を狭めているとは思わない。 '\ s \ s *'は '\ s +'と同じでなければなりませんが、投稿された2つの正規表現は異なる正規表現です。しかし、あなたが投稿した2人の間でさえパフォーマンスの違いが驚くべきことであることに私は同意します! – rjh

答えて

19

パターンの先頭に「プラス」ノード(例:\s+)があり、ノードが一致しない場合、正規表現エンジンは失敗したポイントまでスキップして再試行します。一方、\s*では、エンジンは一度に1文字だけ進められます。

イヴ・オートンがうまくhereこの最適化について説明します。

開始クラスの最適化は、それが唯一のtrys場所(doevery)と「フリップフロップモード」(doevery!)「すべての有効な開始位置を試してみてください」、2つのモードがありますシーケンス内の最初の有効な開始位置。

/(\ d +)X /と文字列 "123456Y"を考慮すると、 "123456"とマッチしてXにマッチしなかった場合、 "23456"とにかく最適化を無効にする)ので、チェック/失敗/となるまで前方にスキップしてから、実際の一致を探し始めることができます。これはフリップフロップモードです。

/\s+/トリガフリップフロップモード。 /\s*/,/\s\s*/、および/\s\s+/はありません。 \s*のような「星」ノードにはこのような最適化を適用できないため、シーケンスの1つのポイントでの障害は後で同じシーケンスでエラーを示すものではありません。


あなたは、各正規表現のためのデバッグ出力でこれを見ることができます。スキップされた文字を^と強調表示しました。

$ 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+/に適用されないことに注意してください、なぜならこれを(一度に4つの文字をスキップします)の比較\s+はパターンの先頭にありません。しかし、/\s\s+/(論理的には/\s{2,}/と等価)および/\s\s*/(論理的には/\s+/と等価)の両方がおそらくであり、が最適化される可能性があります。どちらかが努力する価値があるかどうかについては、perl5-portersで質問するのが理にかなっているかもしれません。あなたが興味を持っている場合は


、「フリップフロップモード」、それがコンパイルされています正規表現にPREGf_SKIPフラグを設定することで有効になります。5.24.0ソースのの行7344と7405と、regexec.cの1585行のコードを参照してください。

+0

ありがとう、これは私が探していた答え(正確にはCソースを掘り下げて最適化について説明しています)です。どうもありがとうございます!! – rjh

+1

http://stackstatus.net/post/147710624694/outage-postmortem-july-20-2016に照らして、これは特に局所的なようです! – rjh

+0

'R +'が 'RR'よりもうまく動作するならば、なぜ正規表現コンパイラは' RR * 'を' R + 'で認識してASTレベルで置き換えないのでしょうか? – Kaz

7

、私は正規表現エンジンの内部のかわからないが、それは二番目に、それはスペースを一致するため\s+は、何らかの方法で\s\s*と同じであることを認識していないように見えますますます多くのスペースに対応しようとしていますが、最初は一致がないとすぐに判断します。でも結果ならば、

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" 
+0

私は既に私の質問に同様のデバッグ出力を添付しましたが、それを調べてくれてありがとう。なぜそれが起こっているのか興味があります:) – rjh

28

まず:

use re qw(Debug);を使用して、出力は明らかにはるかに短い文字列を使用して、このことを示しています正規表現は同じ意味を持ちません。正規表現を01に減らしましょうおよび\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がbe optimized for failureに思えます。まず、定数文字列(O(N)のみを消費する)を探します。ここでは、それは0を探します:Found floating substr "0" at offset 5...

そして、それがチェックするために全体の最小の文字列に対して、正規表現の変数一部、respectivly \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 

後012:それは位置0

  • \s*0で、ここで、stclass要件を満たす最初の位置を探しましょうということ0で
    • 開始し、その後、失敗する4つのスペースを見つけます。
    • 1で始まり、3つのスペースを見つけて失敗します。
    • 2で始まり、2つのスペースを見つけて失敗します。
    • 3で始まり、1スペースを見つけて失敗します。 4時
    • 開始、その後、を失敗しない0のスペースを見つけます。
    • 正確な0
  • \s+0を探す:0で
    • 開始、4つのスペースは、その後失敗見つけます。空白の最小数が一致しないので、正規表現は即座に失敗します。

あなたはPerlの正規表現の最適化を楽しんでいるしたい場合は、以下の正規表現/ *\n/ * \nを考慮することができます。一見すると、彼らは同じように見える、同じ意味を持っている...しかし、あなたがそれに対して(" " x 40000) . "_\n"を実行すると、最初のものはすべての可能性をチェックし、2番目のものは" \n"を探してすぐに失敗する。バニラ、非最適化された正規表現エンジンで

彼らはそれに沿ってバンプとパターンを再試行する必要があるため、両方の正規表現は、壊滅的なバックトラックを引き起こす可能性があります。それはあなたがJeff Atwood's blog上の別の例を見ることができますfind floating substr "0%n"


に最適化されているのでしかし、上記の例では、第2は、Perlで失敗しません。

注意も問題がおよそ\s配慮ではなく、xx*が代わりにx+の使用されている任意のパターンは、行動は「検索可能」ですが、あなたがプレイを開始した場合、このような短い例でexample with 0sともregex explosive quantifiers

を見ること複雑なパターンの場合は、次のように簡単に見つけ出すことはできません。

+0

ほとんどの正規表現エンジンでは、\ s + 'はまったく同じ方法で致命的なバックトラックを引き起こします。だから私の質問は、どのようにPerlが '\ s +'のケースをもっと速く最適化するのだろうか? – rjh

+8

** **両方とも、このケースの最適化がないため、PCREエンジンを使用して致命的なバックトラッキングが発生する可能性があります。['\ s \ s * \ n'](https://regex101.com/r/ tU6sX9/1)、['\ s + \ n'](https://regex101.com/r/iY9jT3/1))。ここでの違いは、Perlの正規表現には最適化されている最適化が原因です。 – nhahtdh

+0

エンジンが逆戻りする必要があるのは、私にはすぐには分かりません。明確にできますか? – jpmc26

12

\s+\nは、\nより前の文字がSPACEであることが必要です。

use re qw(debug)によれば、コンパイルでは、入力で最初にチェックされる部分文字列\nまでの既知数のスペースのストレート文字列が必要であることが確認されています。次に、入力の残りの部分に対して固定長のスペースのみの部分文字列をチェックします。失敗するのは_です。入力のスペース数に関係なく、チェックするのは1つの可能性です。 (_\nが複数存在する場合は、デバッグ出力ごとに同じエラーが直接発​​生します)

このように見ても、これはほぼ予想される最適化であり、特定の検索パターンを利用してこの入力で幸運を祈る。他のエンジンと比較して撮影した場合を除き、この種の分析をしていないことは明らかです。

\s*\nでは、これは該当しません。 \nが見つかり、前の文字が空白でない場合、\s*は何も許可しないので、検索は失敗しません(ゼロ文字)。固定長の部分文字列もなく、バックトラックゲームにもあります。

+0

'/ \ s \ s *?\ n /'はバックトラッキングに苦しんでいますか? – Zaid

+2

@Zaid:貪欲で非貪欲なパターンはバックトラックに関して*同一*です。唯一の違いは、欲張りパターンは可能な限り* long *から始まり、正規表現の残りの部分が一致するまで短縮され、非貪欲なパターンは可能な限り短く*始まり残りの部分正規表現は一致します。 – Borodin

+0

デバッグモードの出力は、 '\ n 'の前に' _'があるかどうかを調べるのではなく、実際にエンジンが '\ s *'または '\ s +'に左から右に移動して一致することを示しています。ソースコードに追加のログを追加しましたが、明らかにそこにループがあります。 – nhahtdh

関連する問題