Perl 列表插值性能



Background

Perldoc for List::Util 建议map的某些用法可以用reduce代替,以避免创建不必要的中间列表:

例如,要查找 列表,我们可以使用

$total = sum map { length } @strings;

但是,这将生成一个临时整数值列表,只要 原始字符串列表,只是将其减少到单个值 再。我们可以通过使用reduce更有效地计算相同的结果 使用一个代码块,该代码块通过将其编写为来累积长度:

$total = reduce { $a + length $b } 0, @strings;

这是有道理的。但是,reduce为了在此示例中工作,需要"标识值",该值将附加到输入列表的前面:

$total = reduce { $a + length $b } 0, @strings;
#                                  ^^^^^^^^^^^

这让我想到,0, @strings不会创建一个新列表,从而抵消map中不创建列表的任何收益吗?

问题

列表插值($scalar, @list)如何在 Perl 中工作?它是否涉及从源列表中复制元素还是以更智能的方式完成?我的简单基准测试建议进行复制:

use strict;
use warnings;
use Benchmark qw/cmpthese/;
my @a1 = 1..10;
my @a2 = 1..100;
my @a3 = 1..1000;
my @a4 = 1..10000;
my @a5 = 1..100000;
my @a6 = 1..1000000;
cmpthese(10000, {
'a1' => sub { my @l = (0, @a1); },
'a2' => sub { my @l = (0, @a2); },
'a3' => sub { my @l = (0, @a3); },
'a4' => sub { my @l = (0, @a4); },
'a5' => sub { my @l = (0, @a5); },
'a6' => sub { my @l = (0, @a6); },
});

结果:

(warning: too few iterations for a reliable count)
Rate       a6       a5       a4       a3       a2       a1
a6    17.6/s       --     -90%     -99%    -100%    -100%    -100%
a5     185/s     952%       --     -90%     -99%    -100%    -100%
a4    1855/s   10438%     902%       --     -90%     -99%    -100%
a3   17857/s  101332%    9545%     862%       --     -91%     -98%
a2  200000/s 1135940%  107920%   10680%    1020%       --     -80%
a1 1000000/s 5680100%  540000%   53800%    5500%     400%       --

奖励问题:如果我的假设是正确的(即0, @strings创建一个新列表),将map替换为reduce有意义吗?

0, @strings不会创建新

列表

没有。如果反编译代码,则只需增加一个 SVOP。

但是你衡量的东西是错误的。在这两种情况下,这些值都会展平并传递到mapreduce子例程中!

文档讨论的是子例程内部发生的情况。map创建一个包含尽可能多的输入值的列表并返回它们,然后sum获取该列表并将其压缩为一个值。返回列表是临时的,不直接在代码中表示。(此列表传递效率不高,可以通过使用引用来加快速度。

相比之下,在reduce,没有这样的返回列表。reduce仅适用于输入值列表并返回单个值。

">只要原始字符串列表,就会生成一个临时整数值列表"是指map堆栈上放置 N 个标量。 问题是,reduce方法创建了同样多的标量,并且它们也都在堆栈上。唯一的区别是reduce方法一次只在堆栈上保留一个。这意味着reduce方法使用更少的内存,但它根本不影响其性能。它给出的reduce更有效地计算相同结果的原因是无稽之谈。

可能存在性能差异,但不是出于这个原因。如果你想找到哪一个对你来说表现更好,需要运行一个基准测试。


这让我想到,0, @strings不创建一个新列表

不。reduce不合时宜地创建单个列表。这与参数列表中的数字表达式无关。

列表不是数组。当我们说"sub 返回一个列表"或"op 计算到一个列表"时,我们实际上的意思是"sub 或 op 在堆栈上放置一定数量的标量"。

列表是为将从堆栈中弹出可变数量的标量的操作创建的。只需将标记推到堆栈上即可完成此操作。例如,reduce { ... } 0, @a将为entersub操作创建一个列表。{ ... }最终会在列表/堆栈上留下一个代码引用,0最终会在列表/堆栈上留下一个数字,@strings最终会将其元素留在列表/堆栈上。在调用 sub 之前,最后一件事被添加到列表/堆栈中:全球*reduce

请注意,创建列表实际上是免费的,因为它只是在堆栈上推送标记。在堆栈上放置一个数组与其元素的数量成正比,但它仍然相当便宜,因为我们只复制一个指针块(在 C 意义上)。

这意味着reduce { ... } @stringsreduce { ... } 0, @strings之间实际上没有性能差异。两者都创建一个列表,并且都向列表/堆栈添加大致相同数量的元素。

异常:

  • for (@a)经过优化,可以for* (@a)
    这样可以节省内存,并在循环过早退出时节省时间。
  • sub f(@); f(@a)相当于&f(@a)

AFAIK、mapgrep不会以这种方式进行优化。


详细地:

$ perl -MO=Concise,-exec -MList::Util=reduce -e'reduce { ... } @a'
...
3  <0> pushmark s                        <-- Creates list (adds mark to the stack).
4  <$> anoncode[CV ] sRM                 <-- Adds CV to the stack.
5  <1> srefgen sKM/1                     <-- Replaces CV with a ref to the CV.
6  <#> gv[*a] s                          <-- Places *a on the stack.
7  <1> rv2av[t4] lKM/1                   <-- Replaces *a with the contents of @a.
8  <#> gv[*reduce] s                     <-- Places *reduce on the stack.
9  <1> entersub[t5] vKS/TARG             <-- Will remove the entire list from the stack.
...
$ perl -MO=Concise,-exec -MList::Util=reduce -e'reduce { ... } 0, @a'
...
3  <0> pushmark s
4  <$> anoncode[CV ] sRM
5  <1> srefgen sKM/1
6  <$> const[IV 0] sM                    <-- The only difference.
7  <#> gv[*a] s
8  <1> rv2av[t4] lKM/1
9  <#> gv[*reduce] s
a  <1> entersub[t5] vKS/TARG
...

直接的问题可以通过基准测试直接回答

use strict;
use warnings;
use List::Util qw(sum reduce);    
use Benchmark qw(cmpthese);
my @ary = 1..10_000;
sub by_reduce { my $res = reduce { $a + length $b } 0, @ary }
sub by_map { my $res = sum map { length } @ary }
cmpthese(-3, {
reduce  => sub { by_reduce  },  
map     => sub { by_map },
});

它打印在我的v5.16上

速率地图减少 地图 780/秒 -- -41% 降低 1312/s 68%

--因此reduce为这项任务做得更好。

至于一般的清单问题,必须取决于如何使用完整清单。

在您的基准测试中,有一个对新阵列的分配,因此必须清楚地完成数据复制。然后更长的阵列需要更长的时间,大约是一个数量级,就像它们的大小之比一样。

对于mapreduce等函数的列表输入,我看不出有理由进行额外的数据复制。 这可以通过基准测试进行检查,比较相同的操作

my @ary = 1..10_000;
# benchmark:
my $r1 = sum map { length } @ary;
my $r2 = sum map { length } (1..5000, 5001..10_000);

报告的速率几乎相同,例如780/s782/s,表明map输入范围的扁平化不涉及数据复制。 (这些范围在编译时转换为数组,感谢池上的评论。

最新更新