此答案将教您如何分析您的脚本,以了解其缓慢的原因和位置。它将使用 Perl 分析器 Devel::NYTProf,您必须从 CPAN 安装它。
在您自己执行此操作之前,请观看作者的this talk 了解分析。重要的是要知道这应该只在极少数情况下进行。你就是这样。
首先,使用以下命令创建测试输入,使用the JSON API of baconipsum.com:
$ curl -s \
'https://baconipsum.com/api/?type=meat-and-filler&format=text&sentences=100' \
| perl -nE 'for $i ( 1 .. 10_000 ) { say for map lc, split /\. / }' >log.txt
此文件大小约为 70MB,有 100 万行。这足以进行测试。
$ ls -lah
-rw-rw-r-- 1 simbabque simbabque 69M Nov 28 13:36 log.txt
$ tail log.txt
Nisi magna pig pastrami, in chicken elit meatball
Consequat laborum rump kevin beef ham hock proident tempor ex strip steak
Shankle kielbasa in nulla
Consectetur picanha pork belly, drumstick tail tempor alcatra pariatur eiusmod
Tongue tail meatloaf cupim ut do sed, cillum kevin id ex dolore t-bone
Ut cow nulla brisket ball tip ipsum ham strip steak culpa cillum
Doner chicken sint duis in, andouille labore eiusmod
Bacon tempor nostrud, short loin occaecat cow nulla ipsum strip steak pastrami corned beef turducken
Ball tip labore chicken pancetta cupim
Ham leberkas pastrami, exercitation id porchetta tri-tip beef voluptate shoulder ipsum meatloaf sunt ea.
接下来,我们准备您的脚本。我进行了一些更改以使其更现代 Perl,例如三参数 open 和词法文件句柄。
$ cat patterns.pl
use strict;
use warnings;
use Data::Dumper;
my $InListOfPatterns = 'bacon@@@loin@@@steak';
my $InStartLineNumber = 2;
my @pat_array = split( '@@@', $InListOfPatterns );
my $num_pat = @pat_array;
my @match_count;
for ( my $i = 0; $i < $num_pat; $i = $i + 1 ) {
$match_count[$i] = 0;
}
open my $fh,'<','log.txt' or die "can not open file :$!";
while (<$fh>) {
chomp;
if ( $. > $InStartLineNumber ) {
for ( my $j = 0; $j < $num_pat; $j = $j + 1 ) {
if ( $_ =~ m/\Q$pat_array[$j]\E/ ) {
$match_count[$j] = ( $match_count[$j] + 1 );
}
}
}
}
print Dumper \@match_count;
这在我的机器上大约需要 6 秒,并且会在最后打印每个模式的匹配数。
现在让我们看看我们如何使用Devel::NYTProf 对此进行分析。你只需要运行这个命令。 -d 标志告诉 Perl 使用调试器接口,:NYTProf 表示使用 Devel::NYTProf 调试器。
$ perl -d:NYTProf patterns.pl
$VAR1 = [
20000,
300000,
90000
];
现在您的目录中有一个名为 nytprof.out 的文件。
$ nytprofhtml --no-flame --open
Reading nytprof.out
Processing nytprof.out data
Writing line reports to nytprof directory
100% ...
它将在您现有的浏览器中打开一个浏览器窗口或一个新选项卡,并显示如下内容:
我们想转到 patterns.pl 的 line 报告。红线是 NYTProf 认为非常慢的线。
最明显的是第 17 行中的 chomp。即使在被丢弃的行上也会调用它。当然,在我们的示例中它只跳过了一行,但在您的情况下可能会更多。将 chomp 移到 if 之后。
我们还可以看到,最重要的时间花在if 上。作为chorboa says in his answer on your cross-posted Perlmonks question,您可以使用带有命名捕获组的单一模式。我将分两步演示这一点,以便您了解他为什么这样做。
use strict;
use warnings;
use Data::Dumper;
my $InListOfPatterns = 'bacon@@@loin@@@steak';
my $InStartLineNumber = 2;
my @pat_array = split( '@@@', $InListOfPatterns );
open my $fh, '<', 'log.txt' or die "can not open file :$!";
my %matched;
while (<$fh>) {
if ( $. > $InStartLineNumber ) {
chomp;
# these two are inside the loop, which is bad
my $i;
my $regex = join '|', map +( $i++, "(?<m$i>$_)" )[1], map quotemeta, @pat_array;
$matched{ (grep defined $-{$_}[0], keys %-)[0] }++ if /$regex/;
}
}
print Dumper \%matched;
让我们重新运行分析器,并检查结果。现在需要更长的时间,因为它在循环内部执行了更复杂的操作。这很糟糕。
它在第 18 行的那个循环中花费了将近 2 秒,为近 100 万行中的每一行重新编译相同的模式。
所以你显然想把它移出循环,就像 choroba 在他的帖子中所说的那样。
use strict;
use warnings;
use Data::Dumper;
my $InListOfPatterns = 'bacon@@@loin@@@steak';
my $InStartLineNumber = 2;
my @pat_array = split( '@@@', $InListOfPatterns );
my $i;
my $regex = join '|', map +( $i++, "(?<m$i>$_)" )[1], map quotemeta, @pat_array;
open my $fh, '<', 'log.txt' or die "can not open file :$!";
my %matched;
while (<$fh>) {
if ( $. > $InStartLineNumber ) {
chomp;
$matched{ (grep defined $-{$_}[0], keys %-)[0] }++ if /$regex/;
}
}
print Dumper \%matched;
如果我们通过分析器重新运行它,它会报告这个。
模式生成的调用现在只调用一次。这明显更快。
不幸的是,总体胜率不是很高,至少在这个文件大小的情况下。原始代码中 for 循环的时间为 12.4 秒 + 1.1 秒,现在为 12.2。只有 100 万行,这 1.3 秒并不重要。但是你的文件可能会有更多的行,并且总体上会变得更快,特别是如果你添加更多可能的模式。
如果我们将模式增加到五个,那么新实现将是 23.6s,而原来的实现是 1.78s + 23.6s。那是1.78s的差异。
在循环中使用一个匹配而不是三个匹配的好处非常明显,但是要找出匹配哪个模式的捕获组是有代价的,并且每次创建查找哈希的命名捕获组更加昂贵。
如果我们将其与解决方案 in Sobrique's answer 进行比较,我们得到 3.69 秒,而原来的 1.78 秒 + 23.6 秒。这种差异现在几乎是一个数量级,非常显着。要获得带有序号的模式,您必须在循环之外编写一两行附加代码,这可以忽略不计。
请注意,所有测量值因机器而异,并且还会受到同时运行的其他进程的影响。在您的计算机上,它们可能完全不同。基准测试很难,而且通常不是很准确。