遍历Perl数组的最佳方法

111

在Perl数组中迭代,哪种实现方式(从速度和内存使用的角度考虑)最佳?是否有更好的方法?(@Array不需要保留)。

实现方式1

foreach (@Array)
{
      SubRoutine($_);
}

实现方法2

while($Element=shift(@Array))
{
      SubRoutine($Element);
}

实现方式3

while(scalar(@Array) !=0)
{
      $Element=shift(@Array);
      SubRoutine($Element);
}

实现4

for my $i (0 .. $#Array)
{
      SubRoutine($Array[$i]);
}

实现方式五

map { SubRoutine($_) } @Array ;

2
为什么会有“最佳”呢?特别是考虑到我们不知道如何衡量一个方案相对于另一个方案的优劣(速度比内存使用更重要吗?map是一个可接受的答案吗?等等)。 - Max Lybbert
2
你发布的三个问题中有两个让我感到“什么鬼?!”除非有额外的上下文来解释它们是合理的替代方案。无论如何,这个问题就像是“最好的方法是什么来相加两个数字?”大多数情况下,只有一种方法。然后,有些情况下,你需要另一种方法。投票关闭。 - Sinan Ünür
4
我理解您的观点(即只有一种方法可以将两个数字相加),但是这个比喻不足以用来轻蔑地否定其他方法。显然,有不止一种方法,而且原帖作者想要了解哪种方法是好的,哪种方法不好。 - CodeClown42
2
《Perl编程语言》第三版的第24章有一节关于效率的内容,值得一读。它涉及不同类型的效率,如时间、程序员和维护者等。该部分以“请注意,为时间进行优化有时可能会在空间或程序员效率方面付出代价(下面的冲突提示所示)。这就是生活。”作为开头。 - user289086
1
有一种方法可以将两个数字相加吗?如果你深入了解底层调用/实现,就不是这样的了……想想进位前瞻、进位保存加法器等等。 - workwise
到目前为止,有一件事情没有明确说明。99%的时间,你想要编写清晰易懂的代码,以便下一个查看它的人能够理解,而不是追求速度或聪明。如果你让它变得聪明了,请确保在注释中很好地解释它。所以foreach my $thing (@array) { ...; }是正确的方式。 - lordadmira
6个回答

92
  • 就速度而言:#1和#4略快,但在大多数情况下区别不大。

    你可以编写一个基准测试来确认,但我怀疑你会发现#1和#4略微更快,因为迭代工作是用C语言完成的,而不是Perl,并且不存在不必要的数组元素复制。(在#1中,$_与元素别名,而#2和#3实际上复制了标量值。)

    #5可能相似。

  • 就内存使用而言:它们都一样,除了#5。

    for (@a)被特殊处理以避免将数组变成扁平结构。该循环迭代数组的索引。

  • 就可读性而言:#1。

  • 就灵活性而言:#1/#4和#5。

    #2不支持false元素。#2和#3具有破坏性。


10
哇,你在简洁的句子中添加了大量信息。 - jaypal singh
2
#2 在处理队列(例如广度优先搜索)时非常好用:my @todo = $root; while (@todo) { my $node = shift; ...; push @todo, ...; ...; } - ikegami
实现4是否创建了一个索引的中间数组,这可能会引入大量的内存使用?如果是这样,那么似乎不应该使用这种方法。参考链接:https://dev59.com/WFjUa4cB1Zd3GeqPSIM3 https://rt.cpan.org/Public/Bug/Display.html?id=115863 - Thorsten Schöning
@ikegami 真是你一贯的冠军风范 - 很棒的回答 :) - skeetastax

33
如果你只关心 @Array 的元素,请使用以下代码:
for my $el (@Array) {
# ...
}

或者

如果索引值很重要,请使用:

for my $i (0 .. $#Array) {
# ...
}

或者,从 perl 5.12.1 开始,你可以使用:

while (my ($i, $el) = each @Array) {
# ...
}

如果在循环体内需要元素及其索引,建议使用each,但这会牺牲与5.12.1之前版本的perl的兼容性。

在某些情况下,可能适合使用其他模式。


我预计 each 是最慢的。它要完成其他操作的所有工作,再加上一个别名、一个列表赋值、两个标量复制和两个标量清除。 - ikegami
1
根据我的测量能力,你是正确的。使用for循环遍历数组索引时,速度大约快了45%,当遍历数组引用的索引(在循环体中访问$array->[$i])时,速度快了20%,相比于使用each结合while循环。 - Sinan Ünür

4

决定这类问题的最佳方法是进行基准测试:

use strict;
use warnings;
use Benchmark qw(:all);

our @input_array = (0..1000);

my $a = sub {
    my @array = @{[ @input_array ]};
    my $index = 0;
    foreach my $element (@array) {
       die unless $index == $element;
       $index++;
    }
};

my $b = sub {
    my @array = @{[ @input_array ]};
    my $index = 0;
    while (defined(my $element = shift @array)) {
       die unless $index == $element;
       $index++;
    }
};

my $c = sub {
    my @array = @{[ @input_array ]};
    my $index = 0;
    while (scalar(@array) !=0) {
       my $element = shift(@array);
       die unless $index == $element;
       $index++;
    }
};

my $d = sub {
    my @array = @{[ @input_array ]};
    foreach my $index (0.. $#array) {
       my $element = $array[$index];
       die unless $index == $element;
    }
};

my $e = sub {
    my @array = @{[ @input_array ]};
    for (my $index = 0; $index <= $#array; $index++) {
       my $element = $array[$index];
       die unless $index == $element;
    }
};

my $f = sub {
    my @array = @{[ @input_array ]};
    while (my ($index, $element) = each @array) {
       die unless $index == $element;
    }
};

my $count;
timethese($count, {
   '1' => $a,
   '2' => $b,
   '3' => $c,
   '4' => $d,
   '5' => $e,
   '6' => $f,
});

在 perl 5, version 24, subversion 1 (v5.24.1) built for x86_64-linux-gnu-thread-multi 上运行此命令,我得到了以下结果:

Benchmark: running 1, 2, 3, 4, 5, 6 for at least 3 CPU seconds...
         1:  3 wallclock secs ( 3.16 usr +  0.00 sys =  3.16 CPU) @ 12560.13/s (n=39690)
         2:  3 wallclock secs ( 3.18 usr +  0.00 sys =  3.18 CPU) @ 7828.30/s (n=24894)
         3:  3 wallclock secs ( 3.23 usr +  0.00 sys =  3.23 CPU) @ 6763.47/s (n=21846)
         4:  4 wallclock secs ( 3.15 usr +  0.00 sys =  3.15 CPU) @ 9596.83/s (n=30230)
         5:  4 wallclock secs ( 3.20 usr +  0.00 sys =  3.20 CPU) @ 6826.88/s (n=21846)
         6:  3 wallclock secs ( 3.12 usr +  0.00 sys =  3.12 CPU) @ 5653.53/s (n=17639)

因此,'foreach (@Array)'的速度大约是其他方法的两倍。其他方法非常相似。

@ikegami还指出,这些实现中除了速度之外还存在许多差异。


1
比较$index < $#array实际上应该是$index <= $#array,因为$#array不是数组的长度,而是它的最后一个索引。 - josch

4

在我看来,方案一是典型的做法,并且对于 Perl 来说,简洁和惯用要比其他因素更加重要。至少,对这三个选择进行基准测试可以让您了解速度方面的见解。


3

在一行中打印元素或数组。

print $_ for (@array);

注意:记住,$_ 在循环中内部引用 @array 的元素。对 $_ 所做的任何更改都将反映在 @array 中; 例如:

my @array = qw( 1 2 3 );
for (@array) {
        $_ = $_ *2 ;
}
print "@array";

输出:2 4 6


2

1与2和3有很大的区别,因为它保留了数组,而其他两个则将其清空。

我认为#3非常奇怪,可能效率更低,所以忘记它吧。

这就让你只剩下#1和#2了,它们并不做同样的事情,因此一个不能比另一个“更好”。如果数组很大而且你不需要保留它,一般情况下作用域会处理它(但请看注意),所以一般情况下,#1仍然是最清晰和最简单的方法。将每个元素移出不会加速任何东西。即使有需要从引用中释放数组,我也只会这么做:

undef @Array;

完成后。

  • 注意:包含数组范围的子程序实际上保留了数组并在下次重复使用相同的空间。通常来说,这是可以接受的(见注释)。

@Array = (); 不会释放底层数组。即使超出作用域也不会释放。如果你想要释放底层数组,你需要使用 undef @Array; - ikegami
2
演示; perl -MDevel::Peek -e'my @a; Dump(\@a,1); @a=qw( a b c ); Dump(\@a,1); @a=(); Dump(\@a,1); undef @a; Dump(\@a,1);' 2>&1 | grep ARRAY - ikegami
什么?我原本以为GC的全部意义在于,当引用计数等于0时,所涉及的内存就可以被回收利用了。 - CodeClown42
@ikegami:我明白()undef的区别,但如果超出作用域不会释放一个数组局部作用域使用的内存,那么这是否意味着Perl有内存泄漏问题?这不可能是真的。 - CodeClown42
@ArtM:我不同意。 将以前子例程的内存保留为下一个子例程的缓存是有意义的,因为通常每次子例程都会执行类似的操作,并且所需的块是相似的(因此,“针对速度进行优化”,perl不是“针对内存使用进行优化”,哈哈,但前者通常比后者更好的权衡)。 在您看到潜在例外的情况下,请使用“undef”。 但我永远不会将其设置为整个进程的默认值。 - CodeClown42
显示剩余9条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接