代码之家  ›  专栏  ›  技术社区  ›  MariusM

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

  •  15
  • MariusM  · 技术社区  · 14 年前

    我正在用Perl编写一个文件解析器,所以必须循环文件。文件由固定长度的记录组成,我想创建一个单独的函数来解析给定的记录并在循环中调用该函数。然而,最终的结果变慢了,大文件和我的猜测是,我不应该使用外部功能。所以我在循环中做了一些有和没有函数调用的虚拟测试:

    [甲]

    foreach (1 .. 10000000) {
    $a = &get_string();
    }
    
    sub get_string {
    return sprintf("%s\n", 'abc');
    }
    

    [乙]

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

    测量结果显示,代码的运行速度大约是代码B的3-4倍。我之前就知道代码A应该运行得更慢,但我还是很惊讶,差异竟如此之大。还尝试用Python和Java运行类似的测试。在Python代码中,一个等价的代码比B慢20%左右,而Java代码的运行速度或多或少与预期的一样。将函数从sprintf更改为其他函数并没有显示出任何显著的差异。

    有什么方法可以帮助Perl更快地运行这样的循环吗?我在这里做的是完全错误的事情吗?还是Perl的特性使函数调用如此开销?

    4 回复  |  直到 11 年前
        1
  •  27
  •   Schwern    5 年前

    Perl函数调用很慢。这很糟糕,因为正是你想做的事情,把你的代码分解成可维护的函数,会让你的程序慢下来。他们为什么慢?Perl在进入子例程时会做很多事情,这是因为它是非常动态的(即,您可以在运行时处理很多事情)。它必须获得该名称的代码引用,检查它是一个代码REF,设置一个新的词汇暂存器(存储)。 my 变量),一个新的动态作用域(存储 local 变量),设置 @_ 举几个例子,检查调用它的上下文并传递返回值。已经有人试图优化这个过程,但没有成功。见 pp_entersub in pp_hot.c 为了血淋淋的细节。

    此外,5.10.0版本中的一个bug也会减慢功能的速度。如果您使用的是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);
        }
    }
    
        2
  •  14
  •   dolmen    14 年前

    如果您的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%                  --
    
        3
  •  9
  •   FMc TLP    14 年前

    您提出的问题与循环无关。都是你的 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代码以提高速度,并且您正试图挤出效率的最后一滴水,那么直接编码是一条路要走——但这通常伴随着可维护性和可读性较差的代价。然而,在你开始进行这种微观优化之前,你需要确保你的底层算法是可靠的,并且你对你的代码中那些缓慢的部分的实际位置有一个明确的理解。做错事很容易浪费很多精力。

        4
  •  2
  •   salva    11 年前

    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