Perl的最佳实践命名约定,用于具有输出参数

时间:2016-06-08 19:16:38

标签: perl maintenance

简介

我通常不使用输出参数(作为输入和输出值或仅作为输出的参数)。这是为了提高我的代码的可读性和可维护性。但话说回来,前几天我突然有一个深深嵌套的子程序调用。我发现自己试图优化它以获得一些速度提升。

这是一个例子:我有一个字符串表示从文件中读取的空白修剪行。如果该行为空,我想用表示返回键的unicode符号替换它。假设我在谷歌搜索并发现符号(unicode U+21B5)看起来不错 [1] 。所以我写了一个简短的子程序:

sub handle_empty_lines {
    my ( $str ) = @_;
    if ( (!defined $str) || $str eq '' ) { 
        return "\x{21B5}";
    }
    return $str;
}

我用过这样的话:

$line = handle_empty_lines( $line );

现在,我想优化此调用,但仍然可以使代码具有可读性和可维护性。

第一个选项是将其内联:

$line = "\x{21B5}" if (!defined $str) || $str eq ''; 

当然是自然而快速的,但我们假设我不想使用if语句 2 来混淆代码并拒绝此选项。

以下是其他两个选项,

  1. 传递对$str的引用以避免在子例程中复制输入参数(即:将按值调用转换为按引用调用),或

  2. 通过引用机制利用Perl的内置调用。

  3. 两个选项都引入了"input/output argument"(即:作为输入和输出的参数),从而降低了代码的可读性,并使维护更加困难(在我看来)。

    让第三个选项保留原始版本(按值调用)。以下是仅限速度(不可读性)的三个选项的快速比较。

    use strict;
    use warnings;
    use Benchmark qw(timethese);
    
    my $str1 = '';
    
    timethese(
        2_000_000,
        {
            case1 => sub { my $test = $str1; case1( \$test ) },
            case2 => sub { my $test = $str1; case2( $test ) },
            case3 => sub { my $test = $str1; $test = case3( $test ) },
        }
    );
    
    sub case1 {
        if ( (!defined $$_[0]) || $$_[0] eq '' ) { 
            $$_[0] = "\x{21B5}";
        }
    }
    
    sub case2 {
        if ( (!defined $_[0]) || $_[0] eq '' ) { 
            $_[0] = "\x{21B5}";
        }
    }
    
    sub case3 {
        my ( $str ) = @_;
        if ( (!defined $str) || $str eq '' ) { 
            return "\x{21B5}";
        }
        return $str;
    }
    

    输出为(Ubuntu笔记本电脑,Intel(R)Core(TM)i7-4702MQ CPU @ 2.20GHz):

    Benchmark: timing 2000000 iterations of case1, case2, case3...
         case1:  1 wallclock secs ( 0.84 usr +  0.00 sys =  0.84 CPU) @ 2380952.38/s (n=2000000)
         case2:  1 wallclock secs ( 0.45 usr +  0.00 sys =  0.45 CPU) @ 4444444.44/s (n=2000000)
         case3:  1 wallclock secs ( 0.70 usr +  0.00 sys =  0.70 CPU) @ 2857142.86/s (n=2000000)
    

    请注意,案例2比案例1快87% 3 ,比案例3快56%。

    有趣的是,引用调用(案例1)比按值调用(案例3)慢。

    问题:

    假设我现在想要保留案例2:

    sub handle_empty_lines {
        if ( (!defined $_[0]) || $_[0] eq '' ) { 
            $_[0] = "\x{21B5}";
        }
    }
    

    然后,如果我用它来调用它:

    handle_empty_lines( $line );
    

    它没有告诉读者它修改了$line

    我该如何处理?我可以想到两个选择:

    • 致电后发表评论:

      handle_empty_lines( $line ); # Note: modifies $line
      
    • 更改子程序的名称。给出一个给出指示的名称 $line被修改的读者,例如:

      handle_empty_lines__modifies_arg( $line );
      

    <子>的脚注:

    <子> 1.我后来发现我可以使用N{}转义来使代码更易读,使用&#34; \ N {DOWNWARDS ARROW WITH CORNER LEFTWARDS}&#34;而不是&#34; \ x&#34;&#34;

    <子> 对于这个简单的案例,我同意这是否可以被称为任何形式的混乱是值得怀疑的。

    3.4444444.44 / 2380952.38 = 1.87 功能

3 个答案:

答案 0 :(得分:2)

我会写这个

use utf8;
use strict;
use warnings 'all';
use feature 'say';
use open qw/ :std :encoding(utf-8) /;

for ( '', undef, 'xx' ) {
    my $s = $_;
    fix_nulls($s);
    say $s;
}

sub fix_nulls {
    for ( $_[0] ) {
        $_ = "\x{21B5}" unless defined and length;
    }
}

答案 1 :(得分:1)

我总是追求设计和清晰度。我觉得一旦性能问题开始引入界面设计,就应该重新设计了。 (对我而言,通常意味着引入额外的层。)我发现你的最后讨论表明,由于所有选项都存在问题,因此需要付出相当多的代价。我进入@_

所以在这种情况下,我会继续按值返回

$line = handle_empty_lines( $line );

sub handle_empty_lines {
    defined $_[0] && $_[0] ne '' && return $_[0];
    return "\x{21B5}";
}

或通过引用,我希望它会更快一些。

handle_empty_lines( \$line);

sub handle_empty_lines {
    defined ${$_[0]} && ${$_[0]} ne '' && return 1;
    ${$_[0]} = "\x{21B5}";
}

更常见的情况应该首先出现,而删除分支if测试的优化有助于,如下面的基准测试所示,但很少。

我会选择哪种更适合设计。

我已将这两个添加到ikegami发布的基准

sub micro {
    defined $_[0] && $_[0] ne '' && return $_[0];
    return "\x{21B5}";
}

sub microref {
    defined ${$_[0]} && ${$_[0]} ne '' && return 1;
    ${$_[0]} = "\x{21B5}";
}

添加了这两个功能的ikegami基准测试的完整结果

              Rate with_ref microref baseline   by_ref    micro  inplace
with_ref 1522762/s       --     -13%     -19%     -34%     -36%     -43%
microref 1742620/s      14%       --      -8%     -24%     -26%     -35%
baseline 1890650/s      24%       8%       --     -18%     -20%     -29%
by_ref   2296676/s      51%      32%      21%       --      -3%     -14%
micro    2360880/s      55%      35%      25%       3%       --     -12%
inplace  2680740/s      76%      54%      42%      17%      14%       --

事实证明,具有按价值返回的'mirco'优化版本表现良好。在我看来,这些结果意味着在这里不需要为速度做出艰难的设计决策。

在上面,传递给函数的字符串是空的。这是一个简单的单词时的结果。

              Rate with_ref microref baseline   by_ref    micro  inplace
with_ref 1470954/s       --     -16%     -21%     -34%     -39%     -55%
microref 1742620/s      18%       --      -6%     -21%     -27%     -47%
baseline 1854974/s      26%       6%       --     -16%     -23%     -43%
by_ref   2213644/s      50%      27%      19%       --      -8%     -32%
micro    2399206/s      63%      38%      29%       8%       --     -27%
inplace  3267412/s     122%      87%      76%      48%      36%       --

这证明了在如此精细地估计实际使用中更好的方面的困难。实际程序中还有其他因素可能会扭曲这些因素。我仍然认为没有理由为速度设计而烦恼。

答案 2 :(得分:1)

您的测试每个都会改变多个变量。这是一个更好的测试:

use strict;
use warnings;

use Benchmark qw( cmpthese );

sub baseline {
    my ( $str ) = @_;
    if ( (!defined $str) || $str eq '' ) {
        return "\x{21B5}";
    }
    return $str;
}


sub with_ref {
    my ( $ref ) = @_;
    if ( (!defined $$ref) || $$ref eq '' ) {
        return "\x{21B5}";
    }
    return $$ref;
}


sub by_ref {
    if ( (!defined $_[0]) || $_[0] eq '' ) {
        return "\x{21B5}";
    }
    return $_[0];
}


sub inplace {
    if ( (!defined $_[0]) || $_[0] eq '' ) {
        $_[0] = "\x{21B5}";
    }
}

{
    my $str = '';

    cmpthese(-3, {
        baseline => sub { my $test = $str; $test = baseline  $test; },
        with_ref => sub { my $test = $str; $test = with_ref \$test; },
        by_ref   => sub { my $test = $str; $test = by_ref    $test; },
        inplace  => sub { my $test = $str;         inplace   $test; },
    });
}

结果:

              Rate with_ref baseline   by_ref  inplace
with_ref 1252657/s       --     -14%     -27%     -45%
baseline 1461434/s      17%       --     -15%     -36%
by_ref   1718499/s      37%      18%       --     -24%
inplace  2271005/s      81%      55%      32%       --
  • 添加引用可以减慢速度。
  • 保持匿名比基线快18%,节省0.14微秒。

    $ perl -E'say 1/1718499 - 1/2271005'
    1.41569475973388e-07
    
  • 就地版本比基线快55%,节省0.24微秒。

    $ perl -E'say 1/1461434 - 1/2271005'
    2.4392574799202e-07
    

除非这是一个常用功能,否则这似乎是过早优化的情况。