为什么Perl中的函数调用循环如此缓慢?

时间:2010-07-15 12:32:44

标签: performance perl

我在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的功能是函数调用是这样的开销?

4 个答案:

答案 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)

您提出的问题与循环没有任何关系。您的AB示例在这方面都是相同的。相反,问题在于直接在线编码与通过函数调用相同代码之间的区别。

函数调用确实涉及不可避免的开销。我不能谈论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