欧拉计划第36题:三种Perl实现的性能差异原因问询
欧拉计划第36题:Perl实现性能差异解析
这是欧拉计划(Euler Project)的第36题:求一百万以下同时在十进制和二进制下为回文数的所有数字之和。你尝试了三种Perl实现,性能表现差异明显:
三种实现对比
- 函数式风格实现(耗时近6秒):
[1..1_000_000] .grep( * !%% 2 ) .grep( -> $x { $x == $x.flip } ) .grep( -> $y { $y.base(2) == $y.base(2).flip } ) .sum.say - 生成奇数的流式实现(耗时12秒):
(1,3 ... 1_000_000) .grep( -> $x { $x == $x.flip } ) .grep( -> $y { $y.base(2) == $y.base(2).flip } ) .sum.say - 迭代式实现(耗时约3秒):
my @pals; for (1,3 ... 1_000_000) -> $x { next unless $x == $x.flip; next unless $x.base(2) == $x.base(2).flip; @pals.push($x); } say [+] @pals;
下面针对你的两个疑问逐一解答:
1. 为何流式版本比迭代版本慢这么多?
流式实现的性能瓶颈来自链式筛选的多次遍历与中间数据结构开销:
- 每次调用
.grep都会遍历当前的完整序列,并生成一个新的列表存储筛选结果。比如你的流式代码中,先遍历所有数字筛选出奇数(生成奇数列表),再遍历该列表筛选十进制回文(生成回文列表),最后遍历这个列表筛选二进制回文——三次完整遍历+两次中间列表的内存分配与复制,带来了大量额外的时间和内存成本。 - 而迭代版本是单次遍历完成所有校验:在一次循环中,对每个奇数依次检查十进制回文和二进制回文,不符合条件直接跳过,符合才加入结果数组。全程没有中间列表的生成,只需要一次遍历,效率自然高出不少。
2. 为何两种for循环的性能差异显著?
这是惰性序列与预生成数组的本质区别导致的:
for (1,3 ... 1_000_000) -> $x {...}使用的是惰性序列:Perl不会一次性生成所有一百万以内的奇数,而是在每次循环迭代时才计算出下一个元素。这种方式的优势是节省内存(无需一次性存储所有元素),但每次生成元素都需要额外的计算(比如判断是否超出上限、生成下一个奇数),循环时的单次迭代开销更高。for [1,3 ... 1_000_000] -> $x {...}使用的是预生成数组:Perl会先一次性计算出所有一百万以内的奇数并存储到数组中,之后的遍历就是直接访问内存中的元素,没有额外的计算开销。虽然初始化数组时会有一次开销,但对于小范围序列来说,后续的遍历速度会远快于惰性序列。
简单总结:惰性序列适合处理超大甚至无限序列(优先节省内存),但在小范围场景下,预生成数组的遍历性能更优。
内容的提问来源于stack exchange,提问作者jmcneirney
相关产品推荐
相关产品推荐

