【问题标题】:Why is `\s+` so much faster than `\s\s*` in this Perl regex?为什么在这个 Perl 正则表达式中 `\s+` 比 `\s\s*` 快得多?
【发布时间】:2016-11-20 18:40:56
【问题描述】:

为什么用\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 - 当给定一个 450KB 的输入文件时,该文件由大量空格和一些非空格组成,最后有一个换行符 - 正则表达式挂起并且从未完成.

我直观地将正则表达式替换为s/\s+\n/\n/g; s/\n\s+/\n/g;,一切都很好。

但为什么它的速度这么快?使用re Debug => "EXECUTE" 后,我注意到\s+ 版本以某种方式优化为仅在一次迭代中运行: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*? 不提供任何加速。

【问题讨论】:

  • \s 也匹配 \n 也无济于事。不是换行符的空白字符是[^\S\n],或者您可以使用“水平空白”\h。
  • 您可以将比较范围缩小到/\s*\n/ 和/\s+\n/ see live。请注意,如果字符串不匹配,它只会更快。在匹配的情况下,似乎需要相同的时间
  • @ThomasAyoub 我不认为这会缩小比较范围。 \s\s* 应该与 \s+ 相同,而您发布的两个是不同的正则表达式。但是,我同意即使您发布的两者之间的性能差异也令人惊讶!
  • @Borodin [^\S\n]*\n 也很慢,不过...
  • 由于最近的中断 (stackstatus.net/post/147710624694/…),这似乎很有趣。

标签: regex perl regex-greedy


【解决方案1】:

首先,即使生成的正则表达式不会保持相同的含义,让我们将正则表达式简化为 \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 似乎是be optimized for failure。它将首先查找常量字符串(仅消耗 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" 运行它,第一个将检查所有可能性,而第二个将查找 " \n" 并立即失败。

在普通的、未优化的正则表达式引擎中,两个正则表达式都可能导致灾难性的回溯,因为它们需要在模式颠簸时重试该模式。但是,在上面的示例中,第二个不会因 Perl 而失败,因为它已优化为 find floating substr "0%n"


您可以在 Jeff Atwood's blog 上查看另一个示例。

还要注意,问题不在于考虑\s,而是使用xx* 代替x+ 的任何模式,请参阅example with 0s 和regex explosive quantifiers

对于这样较短的示例,行为是“可找到的”,但如果您开始使用复杂的模式,则很难发现,例如:Regular expression hangs program (100% CPU usage)

【讨论】:

  • 两者都可能导致灾难性的回溯(例如,使用 PCRE 引擎,因为它没有针对这种情况进行优化:\s\s*\n、\s+\n)。这里的差异很可能是由 Perl 正则表达式的一些优化引起的。
  • @jpmc26 编写正则表达式时,请确保尽可能具体。如果我将 20 个网球排在你面前,让你从以下任意数量的网球中挑选一个,那么有很多方法可以完成任务。如果我要求您选择一个和 all 以下它会限制可能性。请参阅 [regular-expressions.info/catastrophic.html](Runaway 正则表达式:灾难性回溯),它解释得很好。我希望我很清楚,不要犹豫,问更多
  • 您无法真正将"0_\n" =~ /0+\n/ 与" _\n" =~ /\s+\n/ 进行比较。对于前者,优化器知道输入必须包含字符串0\n;它没有,因此优化器可以在运行完整的正则表达式引擎之前退出。对于后者,优化器只知道输入必须包含字符串\n;确实如此,因此必须运行完整的正则表达式引擎。
  • 我对@9​​87654359@ 匹配多个空格的描述有点困惑。您能否澄清一下,它是在尝试在不同位置找到单个空间,并且可以通过锚定来解决问题?
  • @ThomasAyoub 我感谢您的努力,但坦率地说,您似乎对这个话题并不了解。您的第一个答案表明一个版本有灾难性的回溯,而另一个没有(不正确)。您的第二个编辑答案也是错误的(正如 ThisSuitIsBlackNot 指出的那样),因为一个输入缺少零,因此 Perl 甚至不执行正则表达式。我想没有人确切知道优化是什么。
【解决方案2】:

当模式的开头有一个“加号”节点(例如\s+)并且该节点无法匹配时,正则表达式引擎会向前跳到失败点并再次尝试;另一方面,使用\s*,引擎一次只能前进一个字符。

Yves Orton 很好地解释了这个优化here:

开始类优化有两种模式,“尝试每个有效的开始位置”(doevery)和“触发器模式”(!doevery),它只尝试序列中的第一个有效开始位置。

考虑 /(\d+)X/ 和字符串 "123456Y",现在我们知道如果我们在匹配 "123456" 之后无法匹配 X,那么我们也会在 "23456" 之后匹配失败(假设没有恶意技巧到位,无论如何都会禁用优化),所以我们知道我们可以向前跳到检查 /fails/ ,然后才开始寻找真正的匹配。这是触发器模式。

/\s+/ 触发触发器模式; /\s*/、/\s\s*/ 和 /\s\s+/ 不要。 这种优化不能应用于像\s* 这样的“星形”节点,因为它们可以匹配零个字符,因此序列中某一点的失败并不表示同一序列中稍后的失败。


您可以在每个正则表达式的调试输出中看到这一点。我用^ 突出显示了跳过的字符。比较一下(一次跳过四个字符):

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

到这个(一次跳过一个或两个字符):

$ 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+/)可能可以进行优化;在perl5-porters 上询问是否值得付出努力可能是有意义的。


如果您有兴趣,可以在编译时通过在正则表达式上设置PREGf_SKIP 标志来启用“触发器模式”。请参阅 5.24.0 源代码中 regcomp.c 中第 7344 和 7405 行以及 regexec.c 中第 1585 行的代码。

【讨论】:

  • 谢谢,这正是我正在寻找的答案(实际上深入研究了 C 源代码并解释了优化)。非常感谢!!
  • 鉴于stackstatus.net/post/147710624694/…,这似乎特别热门!
  • 如果R+ 比RR 工作得更好,为什么正则表达式编译器不能在AST 级别识别并用R+ 替换RR*?
  • @Kaz 我在最后一段中提到了这一点。可能会添加该优化,但它需要 Perl 维护者的努力,但收益甚微。
【解决方案3】:

\s+\n 要求\n 前面的字符是SPACE。

根据use re qw(debug),编译确定它需要一个已知数量的空格的直字符串,直到子字符串\n,它首先在输入中检查。然后它根据输入的剩余部分检查固定长度的仅限空格的子字符串,当涉及到_ 时失败。无论输入有多少空格,都可以进行检查。 (当有更多 _\n 时,每个调试输出都会直接失败。)

这样看,这是您几乎可以预料到的优化,它利用了一种相当具体的搜索模式并幸运地使用了这个输入。除非与其他引擎进行比较,后者显然不进行此类分析。

\s*\n 并非如此。一旦找到\n 并且前一个字符不是空格,搜索就不会失败,因为\s* 不允许任何内容(零字符)。也没有定长子串,在回溯游戏中。

【讨论】:

  • /\s\s*?\n/ 是否存在回溯问题?
  • @Zaid:贪婪和非贪婪模式在回溯方面相同。唯一的区别是贪婪模式将尽可能以 long 开始并缩短直到正则表达式的其余部分匹配,而非贪婪模式将以 short开始> 尽可能地扩展,直到其余的正则表达式匹配为止。
  • 调试模式的输出表明引擎实际上是从左到右匹配\s*或\s+,而不是检查\n之前是否有_ .我在源代码中添加了额外的日志记录,那里显然有一个循环。
  • @nhahtdh 谢谢你的评论——我不知道上面是怎么读的。我怀疑它可能会向后检查并最初写了它,但后来我发现同样的,一旦我查看re=debug 输出,它确实从左到右(并更改了帖子)。我的意思是它有一个固定长度的纯空格字符串(直接跟踪并)撞到_。固定长度的“棍子”的存在非常巨大,那时一切都非常快。 (另外,这个输入非常特别。)我认为这已经足够优化了。我应该改写那句话,谢谢。
【解决方案4】:

我不确定正则表达式引擎的内部结构,但它似乎无法识别\s+ 在某种程度上与@ 相同相同 987654322@,因为在第二个中它匹配一个空格,然后尝试匹配越来越多的空格,而在第一个中,它立即得出没有匹配的结论。

使用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"

【讨论】:

  • 我已经在我的问题中附加了一个类似的调试输出,但感谢您对其进行调查。我对它发生的原因很感兴趣:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-09-26
  • 2011-05-31
  • 2016-05-16
  • 2018-03-02
  • 1970-01-01
  • 2013-03-15
相关资源
最近更新 更多