Компиляция регулярных выражений, модификатор o, функция study, хронометраж
13.4. Хронометраж времени выполнения регулярных выражений
В поставке Perl есть стандартный модуль Benchmark. Он позволяет измерять время выполнения участков кода. При этом можно учитывать только время, которое потрачено процессором на выполнение кода вашей программы, а не на всю систему. Механизм применения этого модуля таков:
use Benchmark; … my $t1=new Benchmark; # Здесь находится участок кода, время работы которого измеряется … my $t2=new Benchmark; print timestr(timediff $t2,$t1);
В переменной $t1 запоминается время начала исполнения участка кода, в переменной $t2 запоминается время окончания выполнения этого участка кода. Затем с помощью функций timediff и timestr выводится разница между временем окончания и временем начала работы участка кода, который тестируется. Но не забывайте, что при первом обращении к регулярному выражению тратится время на его компиляцию!
В качестве примера применения хронометража времени поставим задачу скорейшего определения факта наличия между тегами непробельного символа. При этом предполагается, что все теги закрыты и нет вложенных тегов. Для этого разработаем такой алгоритм регулярного выражения:
$_=' <pppp>' x 13000; $_.='<table>a'; my $re=qr /\A # начало текста (?> # атомарная группировка (?: # цикл пропуска пробелов и тегов (?>\s*) # пропускаем пробельные символы <(?>[^>]*)> # пропускаем тег с его содержимым )* # повтор любое число раз ) \S # и вот он наконец - непробельный символ /x; my $t1=new Benchmark; for (1..1000000) { /$re/; } my $t2=new Benchmark; print timestr(timediff $t2,$t1);
В этом примере избран такой подход: в цикле (?: … )* пропускаются пробельные символы и теги, а после окончания цикла таких пропусков должен встретиться непробельный символ \S. Если он встретится, то поиск завершится удачей. В переменной $_ создается длинная строка с тегами и пробелами, которая завершается непробельным символом a вне тегов. Чтобы время компиляции регулярного выражения> не вошло в интересующий нас интервал времени, мы используем объект регулярного выражения $re. Поскольку регулярное выражение работает быстро, создаем цикл из миллиона повторов применения этого регулярного выражения к тексту.
При прогоне этой программы на моем компьютере выводится следующее значение времени выполнения заданного участка кода:
1 wallclock secs ( 1.11 usr + 0.00 sys = 1.11 CPU)
Итак, это регулярное выражение тратит 1.11 секунды на миллион повторов цикла. (Кстати, на компиляцию этого регулярного выражения тратится 0.02 секунды, что намного больше, чем тратится на его выполнение для 90000 символов.)
Теперь давайте уберем атомарную группировку:
$_=' <pppp>' x 13000; $_.='<table>a'; my $re=qr /\A # начало текста (?: # цикл пропуска пробелов и тегов (?>\s*) # пропускаем пробельные символы <(?>[^>]*)> # пропускаем тег с его содержимым )* # повтор любое число раз \S # и вот он наконец - непробельный символ /x; my $t1=new Benchmark; for (1..1000000) { /$re/; } my $t2=new Benchmark; print timestr(timediff $t2,$t1);
Распечатка времени показывает, что программа стала работать быстрее и теперь тратит всего 1.08 секунды. Это можно понять так: зачем трудиться по уничтожению сохраненных состояний, когда регулярное выражение заканчивается? Если убрать атомарные скобки вокруг подшаблона \s*, то регулярное выражение начиает работать еще немного быстрее. Но здесь все зависит от данных, которые обрабатывает это регулярное выражение. В нашем случае пробелов мало, они встречаются по одному, а если группы пробелов будут большими, то в этом случае уничтожение состояний для подшаблона \s* может пригодиться. Кроме того, у нас нет возвратов при переборе. Если убрать последнюю атомарную группировку:
/\A # начало текста (?: # цикл пропуска пробелов и тегов \s* # пропускаем пробельные символы <[^>]*> # пропускаем тег с его содержимым )* # повтор любое число раз \S # и вот он наконец - непробельный символ /x;
то время выполнения участка кода практически не изменится и станет равным 1.03 секунды.