正则表达式中的回溯比预期的要快

Posted

技术标签:

【中文标题】正则表达式中的回溯比预期的要快【英文标题】:Backtracking in regexp faster than expected 【发布时间】:2015-08-20 07:41:59 【问题描述】:

根据perlre,以下代码需要几秒钟才能执行:

$ time perl -E '$_="((()" . ("a") x 18;  say "ok" if m \(([^()]+|\( [^()]* \))+\)x;'

real    0m0.006s
user    0m0.000s
sys 0m0.005s

文档说:

考虑上面的模式如何检测到不匹配 ((()aaaaaaaaaaaaaaaaaa 在几秒钟内,但每个额外的 这次字母加倍。

正如所见,在我的笔记本电脑上只需要几分之一秒.. 即使运行一百万个a 也可以在半秒内完成:

$ time perl -E '$_="((()" . ("a") x 1000000;  say "ok" if m \(([^()]+|\( [^()]* \))+\)x;'

real    0m0.454s
user    0m0.446s
sys 0m0.008s

我在这里错过了什么?

【问题讨论】:

perl 编译器可能会为您解决这个问题。尝试将 ((()aaaaaa 改为标准输入。 @ZanLynx 你是说echo "((()aaaaaaaaaaaaaaaaaa" | perl -nE 'say "ok" if m \(([^()]+|\( [^()]* \))+\)x;' 吗? 是的。或者从另一个 perl 脚本输出它,这样你就可以使用 x 10000 @ZanLynx perl -E 'say "((()" . ("a") x 1000000;' | perl -nE 'say "ok" if m \(([^()]+|\( [^()]* \))+\)x;' 也在半秒内运行 试试use re 'debug',它会告诉你处理re的步骤。 【参考方案1】:

找出正则表达式引擎在做什么的技巧之一是:

use re 'debug'; 

例如:

use strict;
use warnings;
use re 'debug'; 

my $str = "a" x 18;

$str =~ m \(([^()]+|\( [^()]* \))+\)x;

这将打印正则表达式引擎实际在做什么:

Compiling REx " \(([^()]+|\( [^()]* \))+\)"
Final program:
   1: EXACT <(> (3)
   3: CURLYX[0] 1,32767 (40)
   5:   OPEN1 (7)
   7:     BRANCH (20)
   8:       PLUS (37)
   9:         ANYOF[^()][above_bitmap_all] (0)
  20:     BRANCH (FAIL)
  21:       EXACT <(> (23)
  23:       STAR (35)
  24:         ANYOF[^()][above_bitmap_all] (0)
  35:       EXACT <)> (37)
  37:   CLOSE1 (39)
  39: WHILEM[1/3] (0)
  40: NOTHING (41)
  41: EXACT <)> (43)
  43: END (0)
anchored "(" at 0 floating ")" at 2..2147483647 (checking floating) minlen 3 
Matching REx " \(([^()]+|\( [^()]* \))+\)" against "aaaaaaaaaaaaaaaaaa"
Intuit: trying to determine minimum start position...
  doing 'check' fbm scan, [2..18] gave -1
  Did not find floating substr ")"...
Match rejected by optimizer
Freeing REx: " \(([^()]+|\( [^()]* \))+\)"

如果你重新添加括号,你会得到不同的结果——我需要大约 2000 个步骤来处理正则表达式。每增加一个字母,这就会变得更长 - 大约 300 步。

所以我会说是的 - 灾难性的回溯正在发生,但您可能会发现处理器(和正则表达式引擎优化)意味着时间要短得多。

use strict;
use warnings;
use re 'debug'; 

my $str = "((()"."a" x 100_000;
$str =~ m \(([^()]+|\( [^()]* \))+\)x;

运行时间相当长 - 但至少有一部分是因为将文本打印到屏幕上实际上相对而言相当“昂贵”。

我的测试表明(“a”的数量)

10 : 1221 lines of output (broadly correlated with regex steps)
11 : 1324 (+103)
12 : 1467 (+143)
13 : 1590 (+129)
14 : 1728 (+138)
15 : 1852 (+124)

20 : 2630 (approx +155/extra)
30 : 4536 (+190/extra)
40 : 6940 (+240/extra)
50 : 9846 (+290/extra)

100 - 31,846 (+440/extra letter)

所以看起来像指数行为 - 但在这两种情况下,处理时间仍然非常快,我将其归因于更快的 cpu(并且可能对正则表达式引擎进行了更好的优化)

【讨论】:

感谢re pragma 的提示。是的,显然会发生回溯,但它比文档声称的要快得多。 这可能就像文档来自旧版本的 perl 一样简单,摩尔定律已经胜过了它。 @Sobrique - 确实。自 perl 5.6.1(参见search.cpan.org/~gsar/perl-5.6.1/pod/perlre.pod)以来,perlre 手册页的那部分没有变化,而且很可能更早。 Perl 5.6.1 于 2001 年 4 月发布。15 年后,对 perl 的改进加上摩尔定律已经过时了。【参考方案2】:

考虑上面的模式如何在几秒钟内检测到((()aaaaaaaaaaaaaaaaaa 上的不匹配。

这句话至少可以追溯到 2001 年 4 月,当时 perl 5.6.1 发布。您可以在search.cpan.org/~gsar/perl-5.6.1/pod/perlre.pod 上查看该版本的 perlre 手册页。

这也许是在记录软件方面要吸取的教训。请注意您所写的内容,因为尽管经过多年的改进和其他人的多次分叉,您的书面文字仍将保持不变。

【讨论】:

效率点仍然有效 - 这样做是非常低效的,也许具有欺骗性。 @Sobrique - 没错。可以轻松编写一个需要很长时间执行的正则表达式。

以上是关于正则表达式中的回溯比预期的要快的主要内容,如果未能解决你的问题,请参考以下文章

为啥我使用这些 Raku 正则表达式会得到不同的回溯?

正则表达式匹配回溯

Java 9+中的灾难性回溯正则表达式示例[关闭]

前端正则表达式

为啥这段代码的执行速度比预期的要快?

使用正则表达式解析 C 样式注释,避免回溯