【问题标题】:Is $_ more efficient than a named variable in Perl's foreach?$_ 是否比 Perl 的 foreach 中的命名变量更有效?
【发布时间】:2009-01-28 08:58:55
【问题描述】:

我是 Perl 的新手,我想知道以下哪个循环更有效:

my @numbers = (1,3,5,7,9);
foreach my $current (@numbers){
    print "$current\n";
}

my @numbers = (1,3,5,7,9);
foreach (@numbers){
    print "$_\n";
}

我想知道这一点是为了知道 $_ 的使用是否更有效,因为它被放置在寄存器中,因为它是常用的还是不常用的。我已经编写了一些代码,我正在尝试清理它,我发现我使用第一个循环比第二个循环更频繁。

【问题讨论】:

  • 您使用的是 Perl,一种脚本语言,那么您为什么要关心琐碎循环的效率呢?
  • 好吧,这个循环只是一个例子来说明我的疑问(可能有大约 30、40 个这样的循环)。我使用未编译的语言这一事实​​并不意味着我不应该处理这些事情......顺便说一句@brian。问题的版本很棒!谢谢布莱恩
  • +1 史蒂文。 @mandel:关心代码效率是件好事,但最好花时间思考要使用的正确算法和数据结构。你的直觉应该告诉你,一种循环风格不会比另一种慢得多——即使 Perl 的设计也没有那么糟糕。

标签: perl variables loops performance


【解决方案1】:

您是否发现使用这些循环的代码部分存在性能问题?如果没有,您想选择更具可读性和可维护性的版本。速度上的任何差异都可能可以忽略不计,尤其是与系统的其他部分相比。始终首先为可维护性编写代码,然后是配置文件,然后是性能代码

“过早的优化是万恶之源”[1]

[1] 克努斯,唐纳德。带有 go 语句的结构化编程,ACM 期刊计算调查,第 6 卷,第 4 期,1974 年 12 月。第 268 页。

【讨论】:

  • 我试图让代码看起来更“Perly”而不是优化...我注意到 Perl 开发人员通常使用 $_ 这是我的主要原因之一...跨度>
  • 此外,这不是问题所在。 OP没有问是否他应该优化?他只是问哪个更快。
  • @mandel:在这种情况下,你应该问哪个更 Perlish :P
  • 真的......但我也很感兴趣 Perlish 是否有效;)
  • @mandel:Perl 中有一些值得使用的习语,但人们高估了“Perl”这个概念。遵守常见习语的唯一点是让其他程序员更容易阅读和理解您的代码,但“Perlish”代码通常更难其他人理解。
【解决方案2】:

即使知道过早的优化是万恶之源

{
  local $\ = "\n";
  print foreach @numbers;
}

但有些期望可能是错误的。测试有点奇怪,因为输出会产生一些奇怪的副作用,而且顺序很重要。

#!/usr/bin/env perl
use strict;
use warnings;
use Benchmark qw(:all :hireswallclock);

use constant Numbers => 10000;

my @numbers = (1 .. Numbers);

sub no_out (&) {
    local *STDOUT;
    open STDOUT, '>', '/dev/null';
    my $result  = shift()->();
    close STDOUT;
    return $result;
};

my %tests = (
    loop1 => sub {
        foreach my $current (@numbers) {
            print "$current\n";
        }
    },
    loop2 => sub {
        foreach (@numbers) {
            print "$_\n";
        }

    },
    loop3 => sub {
        local $\ = "\n";
        print foreach @numbers;
        }
);

sub permutations {
    return [
        map {
            my $a = $_;
            my @f = grep {$a ne $_} @_;
            map { [$a, @$_] } @{ permutations( @f ) }
            } @_
        ]
        if @_;
    return [[]];
}

foreach my $p ( @{ permutations( keys %tests ) } ) {
    my $result = {
        map {
            $_ => no_out { sleep 1; countit( 2, $tests{$_} ) }
            } @$p
    };

    cmpthese($result);
}

可以预期 loop2 应该比 loop1 快

       Rate loop2 loop1 loop3
loop2 322/s    --   -2%  -34%
loop1 328/s    2%    --  -33%
loop3 486/s   51%   48%    --
       Rate loop2 loop1 loop3
loop2 322/s    --   -0%  -34%
loop1 323/s    0%    --  -34%
loop3 486/s   51%   50%    --
       Rate loop2 loop1 loop3
loop2 323/s    --   -0%  -33%
loop1 324/s    0%    --  -33%
loop3 484/s   50%   49%    --
       Rate loop2 loop1 loop3
loop2 317/s    --   -3%  -35%
loop1 328/s    3%    --  -33%
loop3 488/s   54%   49%    --
       Rate loop2 loop1 loop3
loop2 323/s    --   -2%  -34%
loop1 329/s    2%    --  -33%
loop3 489/s   51%   49%    --
       Rate loop2 loop1 loop3
loop2 325/s    --   -1%  -33%
loop1 329/s    1%    --  -32%
loop3 488/s   50%   48%    --

有时我一直观察到 loop1loop2 快大约 15%-20%,但我无法确定原因。

我观察到为 loop1 和 loop2 生成了字节码,在创建 my 变量时只有一个不同。这个变量内部没有分配也没有复制,因此这个操作非常便宜。我认为差异来自"$_\n" 构造,它并不便宜。这些循环应该非常相似

for (@numbers) {
  ...
}

for my $a (@numbers) {
  ...
}

但这个循环更昂贵

for (@numbers) {
  my $a = $_;
  ...
}

还有

print "$a\n";

print $a, "\n";

【讨论】:

  • 我对使用 $_ 而不是打印的一般想法更感兴趣......无论如何,如果你能解释一下上面的代码,我会很高兴:D
  • 使用 $_ 的主要思想是避免我的变量创建和分配。无论如何,这些操作会消耗一些时间,for my $a (@numbers) {} 不会为数组成员分配内存(请注意,更改 $a 会影响 @numbers)。
  • 如果你想改变循环内的循环变量而不影响数组,你可以使用for (@numbers) {my $a = $_},这会导致复制和分配。另一个优化技巧是避免“$_\n”创建字符串。另一种方法是 print $_, "\n";
  • 我已经在“perl, v5.10.0 built for MSWin32-x86-multi-thread”上运行了你的测试。循环之间没有区别(我认为print 吃了所有的区别)。
  • 对于这个特定的例子,最快的是say foreach @numbers; 作为一般规则,访问全局总是比词法慢。需要从命名空间哈希中获取一个全局变量。 $_ 是一个神奇的全局变量,存储在 PL_defgv 中,但是 perl 仍然必须按名称获取它。词法访问只是对数组的编译时固定索引。只有 @_ 在 shift 和 pop 上进行了优化。
【解决方案3】:

你可以看看this tutorial,还有一章“Benchmark Your Code”可以用来比较这两种方法。

【讨论】:

    【解决方案4】:

    基准测试:

    use Benchmark qw(timethese cmpthese);
    
    my $iterations = 500000;     
    
    cmpthese( $iterations,
      {
        'Loop 1' => 'my @numbers = (1,3,5,7,9);
        foreach my $current (@numbers)
        {
          print "$current\n";
        }', 
    
        'Loop 2' => 'my @numbers = (1,3,5,7,9);
        foreach (@numbers)
        {
          print "$_\n";
        }'
      }
    );
    

    输出:

             Rate     Loop 2 Loop 1
    Loop 2  23375/s     --    -1%
    Loop 1  23546/s     1%     --
    

    我已经运行了几次,结果各不相同。我认为可以肯定地说没有太大区别。

    【讨论】:

    • 在一个循环中只有 5 次迭代的 1% 对于使用 $_ 来说是相当多的,每个循环是一个可能的差异......
    • 基准测试并不像某些人想象的那么容易。看,你的结果是每秒大约 100k 个数字,而我的 stackoverflow.com/questions/486949/… 显示 3220k/s。您主要测量了终端 IO 速度:)
    【解决方案5】:

    我对总体思路更感兴趣 使用 $_ 而不是打印...

    附带说明,如果您想开始学习要避免哪些习惯用法以及为什么要避免使用,Perl 最佳实践 是一个不错的选择。我不同意他写的所有内容,但他在大多数情况下都很到位。

    【讨论】:

      【解决方案6】:

      通过“perl -MO=Concise,-terse,-src test.pl”运行这两个选项,得到这两个 OpTree:

      for my $n (@num){ ... }

      LISTOP (0x9c08ea0) 离开 [1] OP (0x9bad5e8) 输入 # 5: 我的@num = 1..9; COP (0x9b89668) 下一个状态 BINOP (0x9b86210) 分配 [4] UNOP (0x9bacfa0) 空 [142] OP (0x9b905e0) 推送标记 UNOP (0x9bad5c8) rv2av SVOP (0x9bacf80) 常量 [5] AV (0x9bd81b0) UNOP (0x9b895c0) 空 [142] OP (0x9bd95f8) 推送标记 OP (0x9b4b020) padav [1] # 6: 对于我的 $n (@num){ COP (0x9bd12a0) 下一个状态 BINOP (0x9c08b48) 离开循环 循环 (0x9b1e820) 输入 [6] 操作 (0x9b1e808) 空 [3] UNOP (0x9bd1188) 空 [142] OP (0x9bb5ab0) 推送标记 OP (0x9b8c278) padav [1] UNOP (0x9bdc290) 空 LOGOP (0x9bdc2b0) 和 操作 (0x9b1e458) 迭代 LISTOP (0x9b859b8) # 7: 说 $n; COP (0x9be4f18) 下一个状态 LISTOP (0x9b277c0) 说 OP (0x9c0edd0) 推送标记 OP (0x9bda658) padsv [6] #

      for(@num){ ... }

      LISTOP (0x8cdbea0) 离开 [1] OP (0x8c805e8) 输入 # 5: 我的@num = 1..9; COP (0x8c5c668) 下一个状态 BINOP (0x8c59210) 分配 [4] UNOP (0x8c7ffa0) 空 [142] OP (0x8ccc1f0) 推送标记 UNOP (0x8c805c8) rv2av SVOP (0x8c7ff80) 常量 [7] AV (0x8cab1b0) UNOP (0x8c5c5c0) 空 [142] OP (0x8cac5f8) 推送标记 OP (0x8c5f278) padav [1] # 6: for (@num){ COP (0x8cb7f18) 下一个状态 BINOP (0x8ce1de8) 离开循环 循环 (0x8bf1820) 输入器 操作 (0x8bf1458) 空 [3] UNOP (0x8caf2b0) 空 [142] OP (0x8bf1808) 推送标记 OP (0x8c88ab0) padav [1] PADOP (0x8ca4188) gv GV (0x8bd7810) *_ #

      我添加了“<===”来标记两者之间的差异。

      如果您注意到“for(@num){...}”版本实际上有更多操作。

      因此,“for(@num){...}”版本可能比“for my $n (@num){...}”版本慢。

      【讨论】:

        【解决方案7】:

        Using $_ 是一个 Perl 习惯用法,它向经验丰富的程序员表明使用了“当前上下文”。另外,很多函数默认使用$_作为参数,从而使代码更加简洁。

        有些人可能也只是争辩说,“它很难写,应该很难读”。

        【讨论】:

        • 决定不给你-1,但是任何真正相信“如果很难写,就应该很难读”的人都不是专业的程序员。不幸的是,这种幼稚的态度在 Perl 社区中似乎太常见了。
        • @j_random_hacker:您对以 TIMTOWTDI 为第一座右铭的语言(-社区)有何期望?巧合的是,当我注意到可读的 Perl 看起来更像 $@{Java} 而不是“惯用的”Perl 时,我就停止了 Perl 编程。
        • @David:是的,我完全同意。我常常希望我能回到过去学习 Python 而不是 Perl,因为现在我知道 Perl 语言的许多无端矛盾之处太难了。
        【解决方案8】:

        我不知道,但是……首先你在循环的第二个版本中保存一个变量赋值。我可以想象,由于 $_ 经常使用,它应该以某种方式进行优化。您可以尝试对其进行分析,一个非常好的 Perl 分析器是由 Tim Bunce 编写的 NYTProf 2

        那么,这些小东西真的值得优化吗?我不认为循环会有所作为。我建议您使用分析器来衡量您的性能并确定真正的瓶颈。通常,速度问题位于运行 90% 时间的 10% 代码中(也许不会是 10-90,但这是“著名”比率:P)。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-11-28
          • 2015-06-17
          • 2016-05-08
          • 2011-12-09
          • 2010-09-22
          • 1970-01-01
          相关资源
          最近更新 更多