【问题标题】:why are function calls in Perl loops so slow?为什么 Perl 循环中的函数调用这么慢?
【发布时间】:2019-03-15 01:10:51
【问题描述】:

我正在用 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 的特性导致函数调用如此开销?

【问题讨论】:

  • get_string() 究竟是做什么的?
  • @roe 我们假设这是一个存根,而您没有使用 sprintf 只是将换行符粘贴到常量字符串上。那将是愚蠢的。那么它到底有什么作用呢?
  • 奇怪,我的屏幕格式很奇怪,以前没有。火狐混蛋..
  • sprintf 仅用于测试。在实际情况中,函数是多次应用 substr 拆分记录(ASCII 字符串)(在这种情况下解包被证明更慢)。速度差异几乎相同。

标签: performance perl


【解决方案1】:

Perl 函数调用很慢。这很糟糕,因为您想要做的事情,将您的代码分解为可维护的函数,这正是会减慢您的程序速度的事情。他们为什么慢? Perl 在进入子程序时会做很多事情,这是因为它非常动态(即,您可以在运行时搞砸很多事情)。它必须获取该名称的代码引用,检查它是否是代码引用,设置一个新的词法暂存器(存储my 变量),一个新的动态范围(存储local 变量),设置@ 987654326@ 仅举几例,检查它被调用的上下文并传递返回值。已尝试优化此过程,但尚未取得成功。有关血腥细节,请参阅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);
    }
}

【讨论】:

  • 您的pp_entersub 链接已失效;也许永久链接会更好?例如,https://github.com/Perl/perl5/blob/bfa9f5ee70ce509f0e66dcff9e9fda131ea8a133/pp_hot.c#L4941。 (我没有将它编辑到你的答案中,因为我不知道你的观点是否仍然适用于这个版本的pp_entersub,比你的答案晚了 9 年)
  • @Dada 我将链接到该文件并让用户搜索。我的观点仍然存在,Perl 中的函数调用仍然很慢,我相信这是由于在 Perl 中调用函数和更改范围的所有复杂性。
【解决方案2】:

如果你的 sub 没有参数并且是一个常量,就像你的例子一样,你可以通过在 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%                  --

【讨论】:

  • 常量文件夹在 5.10.0 和 5.10.1 之间似乎变得更加智能。以前 Perl 只能常量折叠非常简单的表达式。 5.10.1 现在可以处理更复杂的事情,比如 sprintf 调用。
【解决方案3】:

您提出的问题与循环无关。在这方面,您的 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 代码以提高速度,并且您试图挤出最后一滴效率,那么直接编码是可行的方法 - 但这通常会以较低的可维护性和成本为代价可读性。但是,在您开始进行此类微优化业务之前,您需要确保您的底层算法是可靠的,并且您牢牢掌握代码中慢速部分的实际位置。很容易在错误的事情上浪费大量精力。

【讨论】:

  • 我得到的函数和 proto 函数之间只有 1% 或 2% 的差异。 Perl 5.10/windows XP 和 Perl 5.8.5 i386/Linux 2.6.12 i386 和 Perl 5.8.8 x86_64/Linux 2.6.18 x86_64。
  • @M42 我想你是想评论 dolmen 的回答。
  • 注意:5.10.0 有一个错误,它会严重降低函数调用的速度。试试 5.10.1。
  • @Schwern 谢谢,很高兴知道。我可能应该达到 5.12。
【解决方案4】:

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

【讨论】:

    猜你喜欢
    • 2013-08-05
    • 1970-01-01
    • 2020-10-08
    • 2011-04-15
    • 1970-01-01
    • 1970-01-01
    • 2017-03-19
    • 2022-12-04
    • 1970-01-01
    相关资源
    最近更新 更多