为什么正则表达式/ [\ w \ W] + x / i运行速度极慢?

时间:2015-11-30 23:31:59

标签: regex perl

尝试:

time perl -E '$x="a" x 100000; $x =~ /[\w\W]+x/i'  

将运行很长时间(在我的笔记本上20秒)。没有/i,例如

time perl -E '$x="a" x 100000; $x =~ /[\w\W]+x/' 

以0.07秒结束。

无论正则表达式[\w\W]没有多大意义,这个巨大的差异让我感到惊讶。

为什么会有这么大的差异?

修改

更确切地说:

$ time perl -E '$x="a" x 100000; $x =~ /[\w\W]+x/i'  

real    0m19.479s
user    0m19.419s
sys 0m0.038s

我的perl

Summary of my perl5 (revision 5 version 20 subversion 3) configuration:

  Platform:
    osname=darwin, osvers=15.0.0, archname=darwin-2level
    uname='darwin nox.local 15.0.0 darwin kernel version 15.0.0: sat sep 19 15:53:46 pdt 2015; root:xnu-3247.10.11~1release_x86_64 x86_64 '
    config_args='-Dprefix=/opt/anyenv/envs/plenv/versions/5.20.3 -de -Dusedevel -A'eval:scriptdir=/opt/anyenv/envs/plenv/versions/5.20.3/bin''
    hint=recommended, useposix=true, d_sigaction=define
    useithreads=undef, usemultiplicity=undef
    use64bitint=define, use64bitall=define, uselongdouble=undef
    usemymalloc=n, bincompat5005=undef
  Compiler:
    cc='cc', ccflags ='-fno-common -DPERL_DARWIN -fno-strict-aliasing -pipe -fstack-protector -I/opt/local/include',
    optimize='-O3',
    cppflags='-fno-common -DPERL_DARWIN -fno-strict-aliasing -pipe -fstack-protector -I/opt/local/include'
    ccversion='', gccversion='4.2.1 Compatible Apple LLVM 7.0.0 (clang-700.1.76)', gccosandvers=''
    intsize=4, longsize=8, ptrsize=8, doublesize=8, byteorder=12345678
    d_longlong=define, longlongsize=8, d_longdbl=define, longdblsize=16
    ivtype='long', ivsize=8, nvtype='double', nvsize=8, Off_t='off_t', lseeksize=8
    alignbytes=8, prototype=define
  Linker and Libraries:
    ld='env MACOSX_DEPLOYMENT_TARGET=10.3 cc', ldflags =' -fstack-protector -L/opt/local/lib'
    libpth=/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/../lib/clang/7.0.0/lib /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib /usr/lib /opt/local/lib
    libs=-lpthread -lgdbm -ldbm -ldl -lm -lutil -lc
    perllibs=-lpthread -ldl -lm -lutil -lc
    libc=, so=dylib, useshrplib=false, libperl=libperl.a
    gnulibc_version=''
  Dynamic Linking:
    dlsrc=dl_dlopen.xs, dlext=bundle, d_dlsymun=undef, ccdlflags=' '
    cccdlflags=' ', lddlflags=' -bundle -undefined dynamic_lookup -L/opt/local/lib -fstack-protector'


Characteristics of this binary (from libperl): 
  Compile-time options: HAS_TIMES PERLIO_LAYERS PERL_DONT_CREATE_GVSV
                        PERL_HASH_FUNC_ONE_AT_A_TIME_HARD PERL_MALLOC_WRAP
                        PERL_NEW_COPY_ON_WRITE PERL_PRESERVE_IVUV
                        PERL_USE_DEVEL USE_64_BIT_ALL USE_64_BIT_INT
                        USE_LARGE_FILES USE_LOCALE USE_LOCALE_COLLATE
                        USE_LOCALE_CTYPE USE_LOCALE_NUMERIC USE_PERLIO
                        USE_PERL_ATOF
  Locally applied patches:
    Devel::PatchPerl 1.38
  Built under darwin
  Compiled at Oct 28 2015 14:46:19
  @INC:
    /opt/anyenv/envs/plenv/versions/5.20.3/lib/perl5/site_perl/5.20.3/darwin-2level
    /opt/anyenv/envs/plenv/versions/5.20.3/lib/perl5/site_perl/5.20.3
    /opt/anyenv/envs/plenv/versions/5.20.3/lib/perl5/5.20.3/darwin-2level
    /opt/anyenv/envs/plenv/versions/5.20.3/lib/perl5/5.20.3
    .

从问题的背景来看:真正的代码将字符串与大量正则表达式(反垃圾邮件)相匹配,因此我无法可靠地手动检查正则表达式数据库。真正的代码片段是

sub docheck {
    ...
    ...
    foreach my $regex (@$regexs) {
        if ( $_[0] =~ /$regex/i ) {

并且[\w\W]+是10k正则表达式之一:(,例如:[\w\W]+medicine\.netfirms\.com - 正则表达式DB可能需要清理 - 但是......你知道:)

现在代码已更改:

sub docheck {
    ...
    my $str = lc($_[0]);
    foreach my $regex (@$regexs) {
        if ( $str =~ /$regex/ ) {

所以避免使用/i

2 个答案:

答案 0 :(得分:12)

TL; DR

在第二种情况下,优化器非常智能并且意识到这一点 字符串中没有"x",因此无法匹配,并且先前失败。 但是,对于/i情况,测试两者并不是那么聪明 大写和小写x,所以它继续并尝试匹配整个正则表达式。

调试

虽然我无法在性能上重现如此大的差异,但是对于区分大小写的匹配会触发优化。

让我们以'debug'模式运行它:

代码

use re 'debug';
$x="a" x 100000;
$x =~ /[\w\W]+x/;
  • 您还可以将-Mre=debug添加到perl调用。

输出

Compiling REx "[\w\W]+x"
Final program:
   1: PLUS (13)
   2:   ANYOF[\x{00}-\x{7F}][{non-utf8-latin1-all}{unicode_all}] (0)
  13: EXACT <x> (15)
  15: END (0)
floating "x" at 1..9223372036854775807 (checking floating) stclass ANYOF[\x{00}-\x{7F}][{non-utf8-latin1-all}{unicode_all}] plus minlen 2 
Matching REx "[\w\W]+x" against "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"...
Intuit: trying to determine minimum start position...
  Did not find floating substr "x"...
Match rejected by optimizer
Freeing REx: "[\w\W]+x"

注意最后一部分:

Intuit: trying to determine minimum start position...
  Did not find floating substr "x"...
Match rejected by optimizer

优化程序尝试查找"x"的第一个匹配项,并且由于找不到它,它会在正则表达式引擎尝试之前拒绝匹配。

Perl正则表达式被优化为尽可能早地失败,而不是成功。

慢速

我无法在我的最终重现相同的行为(Perl v5.20.2),它在以后的优化中失败,可能是因为版本特定的差异。但是,在这种情况下,不会出现不区分大小写的"x"的优化。

此优化不会被触发,因为它在主题中匹配的可能性超过1(小写"x"和大写"X")。

现在,在没有优化的情况下,正则表达式引擎理论上会尝试匹配"x"

  • [\w\W]+中的每个可能的匹配(消耗整个字符串,然后回溯1个字符,另一个......等等)和
  • 主题字符串中的每个起始位置(99,999个位置)。

当然,还有其他优化可以减少这个数字,但这就是为什么它如此缓慢。请注意,它会随着较长的字符串呈指数级增长。

变通

如果你不特别要求在x之前至少有一个字符,你应该使用/.*x/is,因为它在第一个位置尝试匹配后失败(优化器实际上锚定{{1}到文本的开头) *感谢@nhahtdh提出这个问题。

但是,对于可能出现此行为的更一般情况,提高性能的一个选项是在其余部分之前检查.*(作为独立条件或在相同的正则表达式中):

"x"
  • $x =~ /(?:^(*COMMIT)(?=.*x))?[\w\W]+x/is; ... (?:^只检查一次,如果在字符串的开头。
  • )?如果前面有(?=.*x)
  • x否则,它会回溯,而COMMIT是一个控件动词,会使整个匹配失败。

这会跑得更快。

答案 1 :(得分:3)

Mariano is exactly right:性能的差异归功于优化器。要找出为什么在一种情况下触发优化器而不在另一种情况下触发优化器需要深入了解Perl源代码。

许多优化器代码依赖于关于模式的两个数据:

  • 最长的固定子字符串,

  • 最长的浮动子字符串

这在Perl源代码中的regcomp.c中的注释中进行了解释:

  

在优化期间,我们... [获取]有关的信息       什么字符串必须出现在模式中。我们寻找最长的       必须出现在固定位置的字符串,我们寻找       可能出现在浮动位置的最长字符串。所以举个例子       在模式中:

     
/FOO[xX]A.*B[xX]BAR/
     

两个&#39; FOO&#39;和&#39; A&#39;是固定的字符串。两个&#39; B&#39;和&#39; BAR&#39;漂浮着       字符串(因为它们遵循。*结构)。 study_chunk将识别       FOO和BAR分别是最长的固定和浮动字符串。

(来自Perl 5.22.0)

为什么优化器会使用这些子字符串?因为当你可以进行简单的字符串比较和长度检查而不是运行完整的正则表达式引擎时,它更容易快速失败。

所以/.+x/我们有:

  • 最长固定子字符串:无
  • 最长的浮动子字符串:x

我们拥有/.+x/i

  • 最长固定子字符串:无
  • 最长的浮动子字符串:无(大小写折叠意味着我们不再使用简单的文字字符串)

现在,当编译包含文字字符串(固定或浮动)的模式时,在编译的正则表达式中设置一个特殊标志RXf_USE_INTUIT。执行正则表达式时,此标志会触发名为re_intuit_start()的优化例程,该例程位于regexec.c中:

/* re_intuit_start():
 *
 * Based on some optimiser hints, try to find the earliest position in the
 * string where the regex could match.
 *
 * ...
 *
 * The basic idea of re_intuit_start() is to use some known information
 * about the pattern, namely:
 *
 *   a) the longest known anchored substring (i.e. one that's at a
 *      constant offset from the beginning of the pattern; but not
 *      necessarily at a fixed offset from the beginning of the
 *      string);
 *   b) the longest floating substring (i.e. one that's not at a constant
 *      offset from the beginning of the pattern);
 *   c) Whether the pattern is anchored to the string; either
 *      an absolute anchor: /^../, or anchored to \n: /^.../m,
 *      or anchored to pos(): /\G/;
 *   d) A start class: a real or synthetic character class which
 *      represents which characters are legal at the start of the pattern;
 *
 * to either quickly reject the match, or to find the earliest position
 * within the string at which the pattern might match, thus avoiding
 * running the full NFA engine at those earlier locations, only to
 * eventually fail and retry further along.

使用/.+x/,触发re_intuit_start()并搜索匹配的字符串以查找最长的浮动子字符串(x)。如果找不到任何x,则可以立即拒绝整个匹配。

另一方面,/.+x/i永远不会触发re_intuit_start(),因此我们会失去我们的快速失败优化。