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

Mar*_*usM 16 perl performance

我在Perl中编写了一个文件解析器,因此不得不循环遍历文件.文件由固定长度的记录,我想作出这样的分析给定记录一个单独的函数,并调用该函数在一个循环.但是,对于大文件,最终结果变得缓慢,我的猜测是我不应该使用外部函数.所以我在循环中使用和不使用函数调用进行了一些虚拟测试:

[一个]

foreach (1 .. 10000000) {
$a = &get_string();
}

sub get_string {
return sprintf("%s\n", 'abc');
}
Run Code Online (Sandbox Code Playgroud)

[B]

foreach (1 .. 10000000) {
$a = sprintf "%s\n", 'abc';
}
Run Code Online (Sandbox Code Playgroud)

测量表明,代码运行比我事先知道代码中的本来运行速度变慢代码B.慢约3-4次,但我还是很惊讶,差别那么大.还试图用Python和Java运行类似的测试.在Python代码中的当量为除B慢约20%和Java代码被以相同的速度乳宁或多或少(如预期).将函数从sprintf更改为其他内容并未显示任何显着差异.

有没有办法帮助Perl更快地运行这样的循环?我在这里做了一些完全错误的事情,还是Perl的功能是函数调用是这样的开销?

Sch*_*ern 27

Perl函数调用很慢.它很糟糕,因为您想要做的事情,将代码分解为可维护的功能,这将会减慢您的程序速度.他们为什么慢?Perl在进入子程序时会做很多事情,因为它非常动态(即你可以在运行时搞乱很多东西).它必须得到该名称的代码参考,检查它是一个代码引用,设置一个新的词法暂存器(存储my变量),一个新的动态范围(存储local变量),设置@_为几个,检查什么它被调用的上下文并传递返回值.已经尝试优化这个过程,但他们没有付出代价.有关详细信息,请参见pp_entersubpp_hot.c.

此外,5.10.0中存在一个减慢功能的错误.如果您使用的是5.10.0,请升级.

因此,避免在长循环中反复调用函数.特别是如果它的嵌套.你可以使用Memoize缓存结果吗?是否必须在循环内完成工作?它是否必须在最内层循环中完成?例如:

for my $thing (@things) {
    for my $person (@persons) {
        print header($thing);
        print message_for($person);
    }
}
Run Code Online (Sandbox Code Playgroud)

可以将呼叫header移出@persons循环,从而将呼叫数量减少@things * @persons到刚好@things.

for my $thing (@things) {
    my $header = header($thing);

    for my $person (@persons) {
        print $header;
        print message_for($person);
    }
}
Run Code Online (Sandbox Code Playgroud)


dol*_*men 14

如果你的sub没有参数并且在你的例子中是一个常量,你可以通过在子声明中使用空原型"()"来获得一个大的加速:

sub get_string() {
    return sprintf(“%s\n”, ‘abc’);
}
Run Code Online (Sandbox Code Playgroud)

但是,这可能是您的示例的特殊情况,与您的实际情况不符.这只是为了向您展示基准测试的危险性.

通过阅读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);
Run Code Online (Sandbox Code Playgroud)

及其结果:

                          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%                  --
Run Code Online (Sandbox Code Playgroud)

  • 常量文件夹似乎在5.10.0和5.10.1之间变得更加智能.它曾经是Perl只能不断折叠非常简单的表达式.5.10.1现在可以处理更复杂的事情,比如sprintf调用. (3认同)

FMc*_*FMc 9

你提出的问题与循环没有任何关系.在这方面,你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);
Run Code Online (Sandbox Code Playgroud)

这是我在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%          --
Run Code Online (Sandbox Code Playgroud)

一般来说,如果你需要优化一些Perl代码以提高速度,并且你试图挤出最后一滴效率,那么直接编码是可行的方法 - 但这通常会带来较低的可维护性和可读性.但是,在开始进行这种微优化之前,您需要确保您的基础算法是可靠的,并且您已经牢牢掌握了代码的缓慢部分实际所在的位置.在错误的事情上浪费很多精力很容易.