Почему замена \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*?
не предлагает ускорения.
Если в начале шаблона есть "плюс" 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.
Во-первых, даже если результирующее результирующее выражение не будет иметь то же значение, уменьшите регулярные выражения до \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
\s+0
:
Если вы хотите получать удовольствие от оптимизации регулярных выражений в Perl, вы можете рассмотреть следующие регулярные выражения / *\n
и / * \n
. На первый взгляд они выглядят одинаково, имеют одинаковое значение... Но если вы запустите его против (" " x 40000) . "_\n"
, то первый проверит все возможности, а второй будет искать " \n"
и немедленно сработает.
В ванильном неоптимизированном двигателе регулярных выражений оба регулярных выражения могут привести к катастрофическому обратному отскоку, поскольку им нужно повторить шаблон, когда он сталкивается. Однако в приведенном выше примере второй не прерывается с Perl, поскольку он оптимизирован для find floating substr "0%n"
Вы можете увидеть другой пример в блог Джеффа Атвуда.
Обратите внимание также, что проблема не в рассмотрении \s
, но любой шаблон, где xx*
используется вместо x+
, см. пример с 0s, а также кванторы экспрессии регулярных выражений
С таким более коротким примером поведение "находят", но если вы начинаете играть со сложными шаблонами, это далеко не легко заметить, например: Программа зависания регулярных выражений (100 % Использования ЦП)
\s+
также будет вызывать катастрофический откат точно так же. Поэтому мой вопрос заключается в том, как Perl оптимизирует дело \s+
чтобы оно было намного быстрее?
\s+\n
требует, чтобы символ, предшествующий \n
, был SPACE
.
Согласно use re qw(debug)
компиляция устанавливает, что для нее требуется прямая строка известного числа пробелов, вплоть до подстроки \n
, которая сначала проверяется на входе. Затем он проверяет подстроку с фиксированной длиной пробела на оставшуюся часть ввода, в противном случае, как это делается в _
. Это единственная возможность проверить, независимо от того, сколько пробелов у входа. (Когда больше _\n
обнаружено, что каждый из них не работает одинаково напрямую, на каждый отладочный вывод.)
Посмотрел на это таким образом, это оптимизация, о которой вы почти ожидали, используя довольно специфический шаблон поиска и повезший с этим вводом. За исключением случаев, когда они сделаны по сравнению с другими двигателями, которые явно не проводят такого рода анализ.
С \s*\n
это не так. Когда найден \n
и предыдущий символ не является пробелом, поиск не сработал, поскольку \s*
ничего не дает (нулевые символы). Также нет подстрок с фиксированной длиной и в игре с возвратом.
/\s\s*?\n/
страдает от возврата?
Я не уверен в внутренности механизма регулярных выражений, но похоже, что он не признает, что \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"
\s
также соответствует\n
. Пробельным символом, который не является новой строкой, является[^\S\n]
, или вы можете использовать «горизонтальный пробел»\h
./\s*\n/
и/\s+\n/
посмотреть вживую . И обратите внимание, что это только быстрее, если строка не совпадает. В случае совпадения, кажется, это заняло бы то же время