Почему `\ s +` намного быстрее, чем `\ s \ s *` в этом регулярном выражении Perl?

55

Почему замена \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/ },
});

Ссылка на онлайн-версию

Я заметил медленное регулярное выражение s/\s*\n\s*/\n/g в моем коде - при заданном входном файле 450 КБ, состоящем из множества пробелов с несколькими не-пробелами здесь и там, и окончательной новой строки в конце - регулярное выражение зависало и никогда не заканчивалось.

Я интуитивно заменил регулярное выражение на 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+ будет немедленно вызывать регулярное выражение (без его запуска), если новая строка отсутствует. Я подозреваю, что он использует расположение новой строки для уменьшения объема поиска. Для всех вышеперечисленных случаев он, кажется, умело уменьшает задействованный backtracking (обычно /\s*\n/ против строки пространств занимает экспоненциальное время). Может ли кто-нибудь дать понять, почему версия \s+ намного быстрее?

Также обратите внимание, что \s*? не предлагает ускорения.

  • 8
    Это не помогает, что \s также соответствует \n . Пробельным символом, который не является новой строкой, является [^\S\n] , или вы можете использовать «горизонтальный пробел» \h .
  • 0
    Вы можете сузить сравнение с /\s*\n/ и /\s+\n/ посмотреть вживую . И обратите внимание, что это только быстрее, если строка не совпадает. В случае совпадения, кажется, это заняло бы то же время
Показать ещё 3 комментария
Теги:
regex-greedy

4 ответа

18
Лучший ответ

Если в начале шаблона есть "плюс" node (например, \s+), а node не соответствует, механизм регулярных выражений перескакивает вперед к точке отказа и снова пытается; с \s*, с другой стороны, движок только продвигает один символ за раз.

Ив Ортон прекрасно объясняет эту оптимизацию здесь:

Оптимизация начального класса имеет два режима: "Попробуйте каждую допустимую начальную позицию" (doevery) и "flip flop mode" (! doevery), где он пытается только первую действительную начальную позицию в последовательности.

Рассмотрим/(\ d +) X/и строку "123456Y", теперь мы знаем, что если мы не сможем сопоставить X после сопоставления "123456", то мы также не сможем сопоставить "23456" (если не считать злых трюков на месте, что в любом случае отключает оптимизацию), поэтому мы знаем, что мы можем пропустить вперед до тех пор, пока проверка/не сработает/и только тогда начнется поиск реального соответствия. Это режим триггера.

/\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 в регулярном выражении при компиляции. См. Код вокруг строк 7344 и 7405 в regcomp.c и строке 1585 в regexec.c в источнике 5.24.0.

  • 0
    Спасибо, это именно тот ответ, который я искал (который на самом деле копает в C-источник и объясняет оптимизацию). Огромное спасибо!!
  • 1
    В свете stackstatus.net/post/147710624694/… это кажется особенно актуальным!
Показать ещё 2 комментария
29

Во-первых, даже если результирующее результирующее выражение не будет иметь то же значение, уменьшите регулярные выражения до \s*0 и \s+0 и используйте (" " x 4) . "_0" в качестве ввода. Для скептиков вы можете увидеть здесь, что отставание все еще присутствует.

Теперь рассмотрим следующий код:

$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 кажется оптимизирован для отказа. Сначала он будет искать постоянные строки (которые потребляют только O (N)). Здесь он будет искать 0: Found floating substr "0" at offset 5...

Затем он начнется с переменной части регулярного выражения, порядочно \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"


Вы можете увидеть другой пример в блог Джеффа Атвуда.

Обратите внимание также, что проблема не в рассмотрении \s, но любой шаблон, где xx* используется вместо x+, см. пример с 0s, а также кванторы экспрессии регулярных выражений

С таким более коротким примером поведение "находят", но если вы начинаете играть со сложными шаблонами, это далеко не легко заметить, например: Программа зависания регулярных выражений (100 % Использования ЦП)

  • 0
    В большинстве движков регулярных выражений \s+ также будет вызывать катастрофический откат точно так же. Поэтому мой вопрос заключается в том, как Perl оптимизирует дело \s+ чтобы оно было намного быстрее?
  • 8
    И то, и другое может привести к катастрофическому откату назад (например, с использованием механизма PCRE, так как он не имеет оптимизации для этого случая: \s\s*\n , \s+\n ). Разница здесь, скорее всего, вызвана некоторой оптимизацией, которой нагружено регулярное выражение Perl.
Показать ещё 8 комментариев
12

\s+\n требует, чтобы символ, предшествующий \n, был SPACE.

Согласно use re qw(debug) компиляция устанавливает, что для нее требуется прямая строка известного числа пробелов, вплоть до подстроки \n, которая сначала проверяется на входе. Затем он проверяет подстроку с фиксированной длиной пробела на оставшуюся часть ввода, в противном случае, как это делается в _. Это единственная возможность проверить, независимо от того, сколько пробелов у входа. (Когда больше _\n обнаружено, что каждый из них не работает одинаково напрямую, на каждый отладочный вывод.)

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

С \s*\n это не так. Когда найден \n и предыдущий символ не является пробелом, поиск не сработал, поскольку \s* ничего не дает (нулевые символы). Также нет подстрок с фиксированной длиной и в игре с возвратом.

  • 0
    /\s\s*?\n/ страдает от возврата?
  • 2
    @Zaid: Жадные и не жадные модели идентичны в отношении возврата. Единственное отличие состоит в том , что жадный картина будет начать как можно дольше и не быть сокращен до остальной части регулярного выражения соответствует, в то время как нежадные один будет начать как можно более короткой и не быть продлено до остального регулярных выражений матчей.
Показать ещё 3 комментария
7

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

Результат с использованием 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"
  • 0
    Я уже приложил подобный отладочный вывод к своему вопросу, но спасибо за его изучение. Мне интересно, почему это происходит :)

Ещё вопросы

Сообщество Overcoder
Наверх
Меню