我在Perl中编写了一个文件解析器,因此不得不循环遍历文件。文件由固定长度的记录组成,我想创建一个单独的函数来解析给定的记录并在循环中调用该函数。但是,对于大文件,最终结果变得缓慢,我的猜测是我不应该使用外部函数。所以我在循环中使用和不使用函数调用进行了一些虚拟测试:
[A]
foreach (1 .. 10000000) {
$a = &get_string();
}
sub get_string {
return sprintf("%s\n", 'abc');
}
[B]
foreach (1 .. 10000000) {
$a = sprintf "%s\n", 'abc';
}
测量显示A代码运行速度比代码B慢3-4倍。我事先知道代码A应该运行得更慢,但我仍感到惊讶的是差异很大。还试图用Python和Java运行类似的测试。在Python代码中,A等价物比B慢约20%,Java代码以相同的速度或多或少地运行(如预期的那样)。将函数从sprintf更改为其他内容并没有显示出任何显着差异。
有没有办法帮助Perl更快地运行这样的循环?我在这里做了一些完全错误的事情,还是Perl的功能是函数调用是这样的开销?
答案 0 :(得分:26)
Perl函数调用很慢。它很糟糕,因为您想要做的事情,将代码分解为可维护的功能,这将使您的程序减慢。他们为什么慢? Perl在进入子程序时会做很多事情,因为它非常动态(即你可以在运行时搞乱很多东西)。它必须得到该名称的代码引用,检查它是代码引用,设置一个新的词法暂存器(存储my
变量),一个新的动态范围(存储local
变量) ,设置@_
来命名一些,检查它被调用的上下文并传递返回值。已经尝试优化这个过程,但他们没有付出代价。有关血腥的详细信息,请参阅pp_entersub
in pp_hot.c。
5.10.0中还有一个错误减慢功能。如果您使用的是5.10.0,请升级。
因此,避免在长循环中反复调用函数。特别是如果它的嵌套。您可以使用Memoize缓存结果吗?是否必须在循环内完成工作?它是否必须在最内层循环中完成?例如:
for my $thing (@things) {
for my $person (@persons) {
print header($thing);
print message_for($person);
}
}
对header
的调用可能会移出@persons
循环,从而减少从@things * @persons
到@things
的来电次数。
for my $thing (@things) {
my $header = header($thing);
for my $person (@persons) {
print $header;
print message_for($person);
}
}
答案 1 :(得分:13)
如果您的sub没有参数且在示例中是常量,则可以通过在子声明中使用an empty prototype "()"来获得大幅加速:
sub get_string() {
return sprintf(“%s\n”, ‘abc’);
}
然而,这可能是您的示例的特殊情况,与您的实际案例不符。这只是为了向您展示基准测试的危险性。
您可以通过阅读perlsub了解此提示以及其他许多提示。
这是一个基准:
use strict;
use warnings;
use Benchmark qw(cmpthese);
sub just_return { return }
sub get_string { sprintf "%s\n", 'abc' }
sub get_string_with_proto() { sprintf "%s\n", 'abc' }
my %methods = (
direct => sub { my $s = sprintf "%s\n", 'abc' },
function => sub { my $s = get_string() },
just_return => sub { my $s = just_return() },
function_with_proto => sub { my $s = get_string_with_proto() },
);
cmpthese(-2, \%methods);
及其结果:
Rate function just_return direct function_with_proto
function 1488987/s -- -65% -90% -90%
just_return 4285454/s 188% -- -70% -71%
direct 14210565/s 854% 232% -- -5%
function_with_proto 15018312/s 909% 250% 6% --
答案 2 :(得分:8)
您提出的问题与循环没有任何关系。您的A
和B
示例在这方面都是相同的。相反,问题在于直接在线编码与通过函数调用相同代码之间的区别。
函数调用确实涉及不可避免的开销。我不能谈论Perl相对于其他语言是否以及为什么这种开销更昂贵的问题,但我可以提供一个更好的方法来衡量这种事情:
use strict;
use warnings;
use Benchmark qw(cmpthese);
sub just_return { return }
sub get_string { my $s = sprintf "%s\n", 'abc' }
my %methods = (
direct => sub { my $s = sprintf "%s\n", 'abc' },
function => sub { my $s = get_string() },
just_return => sub { my $s = just_return() },
);
cmpthese(-2, \%methods);
这是我在Perl v5.10.0(MSWin32-x86-multi-thread)上获得的内容。非常粗略地说,简单地调用一个不执行任何操作的函数与直接运行我们的sprintf
代码一样昂贵。
Rate function just_return direct
function 1062833/s -- -70% -71%
just_return 3566639/s 236% -- -2%
direct 3629492/s 241% 2% --
一般情况下,如果你需要优化一些Perl代码以提高速度,并且你试图挤出最后一滴效率,那么直接编码是可行的方法 - 但这通常会带来较低的可维护性和价格。可读性。但是,在开始进行这种微优化之前,您需要确保您的基础算法是可靠的,并且您已经牢牢掌握了代码的缓慢部分实际所在的位置。很容易浪费大量精力处理错误的事情。
答案 3 :(得分:1)
perl优化器会不断折叠示例代码中的sprintf
调用。
你可以解析它以发现它:
$ perl -MO=Deparse sample.pl
foreach $_ (1 .. 10000000) {
$a = &get_string();
}
sub get_string {
return "abc\n";
}
foreach $_ (1 .. 10000000) {
$a = "abc\n";
}
- syntax OK