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。
但是你衡量的东西是错误的。在这两种情况下,这些值都会展平并传递到map
或reduce
子例程中!
文档讨论的是子例程内部发生的情况。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 { ... } @strings
和reduce { ... } 0, @strings
之间实际上没有性能差异。两者都创建一个列表,并且都向列表/堆栈添加大致相同数量的元素。
异常:
for (@a)
经过优化,可以for* (@a)
。
这样可以节省内存,并在循环过早退出时节省时间。sub f(@); f(@a)
相当于&f(@a)
。
AFAIK、map
和grep
不会以这种方式进行优化。
详细地:
$ 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
为这项任务做得更好。
至于一般的清单问题,必须取决于如何使用完整清单。
在您的基准测试中,有一个对新阵列的分配,因此必须清楚地完成数据复制。然后更长的阵列需要更长的时间,大约是一个数量级,就像它们的大小之比一样。
对于map
和reduce
等函数的列表输入,我看不出有理由进行额外的数据复制。 这可以通过基准测试进行检查,比较相同的操作
my @ary = 1..10_000;
# benchmark:
my $r1 = sum map { length } @ary;
my $r2 = sum map { length } (1..5000, 5001..10_000);
报告的速率几乎相同,例如780/s
和782/s
,表明map
输入范围的扁平化不涉及数据复制。 (这些范围在编译时转换为数组,感谢池上的评论。