Подтвердить что ты не робот

Почему регулярное выражение /[\ w\W] + x/i будет чрезвычайно медленным для запуска?

попробовать:

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

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

4b9b3361

Ответ 1

TL; DR

Во втором случае оптимизатор довольно умный и реализует там no "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" и, поскольку он не может его найти, он отклоняет соответствие до того, как движок regex даже попытается его выполнить.

Регулятор Perl оптимизирован для того, чтобы скорее сбой как можно раньше, чем для успеха.


Медленный случай

Я не могу воспроизвести одно и то же поведение на моем конце (Perl v5.20.2), он не работает при более поздней оптимизации, вероятно, из-за различий в версии. Однако оптимизация для нечувствительности к регистру "x" в этом случае не происходит.

Эта оптимизация не запускается, поскольку она имеет более одной возможности для совпадения объекта (как в нижнем регистре "x", так и в верхнем регистре "x").

Теперь, без оптимизации, двигатель регулярных выражений теоретически пытается сопоставить "x" для:

  • каждое возможное совпадение в [\w\W]+ (потребляющее всю строку, затем обратное отслеживание 1 char и другое... и т.д.) и
  • каждое исходное положение в строке темы (99 999 позиций).

Конечно, есть и другие оптимизации, которые уменьшают это число, но почему это так медленно. Обратите внимание, что он увеличивается экспоненциально с более длинными строками.

Работа вокруг

Если вам не требуется, чтобы перед x был хотя бы один символ, вы должны использовать /.*x/is, поскольку он не работает после попытки совпадения в первой позиции (оптимизатор фактически привязывает .* к началу текст).
* Благодаря @nhahtdh для этого.

Однако для более общего случая, когда такое поведение может возникнуть, одним из вариантов повышения производительности является проверка для "x" перед остальными (либо как независимое условие, либо в том же регулярном выражении):

$x =~ /(?:^(*COMMIT)(?=.*x))?[\w\W]+x/is;
  • (?:^... )? Проверяет только один раз, если в начале строки.
  • (?=.*x) Если есть x впереди
  • (*COMMIT) В противном случае он возвращается и COMMIT - это контрольный глагол, который приводит к сбою всего матча.

Это будет работать намного быстрее.

Ответ 2

Мариано в точности правка: разница в производительности - благодаря оптимизатору. Чтобы узнать, почему оптимизатор запускается в одном случае, но не другой, требуется погружение в исходный код Perl.

Большая часть кода оптимизатора опирается на две части данных о шаблоне:

  • самая длинная фиксированная подстрока и

  • самая длинная плавающая подстрока

Это объясняется в комментариях в regcomp.c в источнике Perl:

Во время оптимизации мы... [получите] информацию о     какие строки ДОЛЖНЫ появляться в шаблоне. Мы ищем самый длинный     строка, которая должна отображаться в фиксированном месте, и мы ищем     длинная строка, которая может отображаться в плавающей позиции. Так, например,     в шаблоне:

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

Оба "FOO" и "A" являются фиксированными строками. Оба "B" и "BAR" являются плавающими     (потому что они следуют за конструкцией. *). study_chunk будет идентифицировать     как FOO, так и BAR как самые длинные фиксированные и плавающие строки соответственно.

(от Perl 5.22.0)

Почему оптимизатор использует эти подстроки? Потому что гораздо проще быстро выполнить сбой, когда вы можете выполнять простые сравнения строк и длины, вместо того, чтобы запускать полный механизм регулярных выражений.

Итак, с /.+x/ имеем:

  • самая длинная фиксированная подстрока: none
  • самая длинная плавающая подстрока: x

И с /.+x/i мы имеем:

  • самая длинная фиксированная подстрока: none
  • самая длинная плавающая подстрока: none (сложение флагов означает, что мы больше не работаем с простой литеральной строкой)

Теперь, когда шаблон скомпилирован, который содержит литеральную строку (фиксированную или плавающую), в скомпилированном регулярном выражении устанавливается специальный флаг 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 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 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 s, полное совпадение может быть немедленно отклонено.

С /.+x/i, с другой стороны, re_intuit_start() никогда не срабатывает, поэтому мы теряем свою неудачную оптимизацию.