Perl 中带有输出参数的子例程的最佳实践命名约定

Posted

技术标签:

【中文标题】Perl 中带有输出参数的子例程的最佳实践命名约定【英文标题】:Best practice naming convention in Perl for subroutine with output arguments 【发布时间】:2016-06-08 19:16:37 【问题描述】:

简介:

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

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

sub handle_empty_lines 
    my ( $str ) = @_;
    if ( (!defined $str) || $str eq '' )  
        return "\x21B5";
    
    return $str;

我是这样使用它的:

$line = handle_empty_lines( $line );

现在,我想优化这个调用,但代码仍然可读和可维护。

第一个选项是内联:

$line = "\x21B5" if (!defined $str) || $str eq ''; 

当然是自然而快速的,但让我们假设我不想用 if 语句2 弄乱代码并拒绝这个选项。

这里有另外两个选项,

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

    利用 Perl 的内置引用机制调用。

这两个选项都引入了"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] = "\x21B5";
    


sub case2 
    if ( (!defined $_[0]) || $_[0] eq '' )  
        $_[0] = "\x21B5";
    


sub case3 
    my ( $str ) = @_;
    if ( (!defined $str) || $str eq '' )  
        return "\x21B5";
    
    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] = "\x21B5";
    

然后,如果我使用:

handle_empty_lines( $line );

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

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

在通话后发表评论:

handle_empty_lines( $line ); # Note: modifies $line

更改子程序的名称。取一个能表明 $line 被修改的读者,例如:

handle_empty_lines__modifies_arg( $line );

脚注:

1. 后来我发现我可以使用N 转义来使代码更具可读性,使用“\NDOWNWARDS ARROW WITH CORNER LEFTWARDS”而不是“\x21B5”

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

3. 4444444.44 / 2380952.38 = 1.87

【问题讨论】:

Re "注意案例 2 比案例 1 快 87%3,比案例 3 快 56%。"曾使用cmpthese 而不是timethese @ikegami 是的,我考虑过使用cmpthese,但我发现这些数字让我感到困惑 您的每个功能都以两种方式与其他功能不同。明显缺席:sub case4 if ( (!defined $_[0]) || $_[0] eq '' ) return "\x21B5"; return $_[0]; 最好的建议是根本不要使用输出参数。副作用是一种必要的邪恶,它们使你的代码更难组合和推理,所以尽可能避免它们。修复对函数的调用,而不是函数本身。 【参考方案1】:

我会写这个

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] ) 
        $_ = "\x21B5" unless defined and length;
    

【讨论】:

对不起,我不太明白。为什么fix_nulls( $s ) 更容易阅读? 我认为这意味着参数将被修改得比handle_empty_lines 更好,这几乎可以做任何事情,比如忽略它们【参考方案2】:

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

use strict;
use warnings;

use Benchmark qw( cmpthese );

sub baseline 
    my ( $str ) = @_;
    if ( (!defined $str) || $str eq '' ) 
        return "\x21B5";
    
    return $str;



sub with_ref 
    my ( $ref ) = @_;
    if ( (!defined $$ref) || $$ref eq '' ) 
        return "\x21B5";
    
    return $$ref;



sub by_ref 
    if ( (!defined $_[0]) || $_[0] eq '' ) 
        return "\x21B5";
    
    return $_[0];



sub inplace 
    if ( (!defined $_[0]) || $_[0] eq '' ) 
        $_[0] = "\x21B5";
    



    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

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

【讨论】:

感谢添加的测试!有趣但inplace 仍然更快。遗憾的是,仅由于语言设计,无法在实践中使用它。我认为语言设计应该允许例如$str = #inplace( $str ),然后编译器只需将调用更改为inplace $str。然后可以同时实现效率和可维护性.. :) 它对sort 确实如此。一般很难做到。 我猜这因 perl 版本而异,我有类似的“排序”,但结果却大不相同。我注意到的一件事是 subs 不是相当等价的,因为“inplace”变体缺少 return 语句。对我来说,这在 5.16.2(Mac OS X)和 5.18.1(Linux in VM)中占了大约 3-4% 的差异。 @polettix,不,删除退货是公平的。其他人需要它,但对于就地潜艇来说没有意义。如果删除它是一种增益,那么增益是切换到就地的一部分,将它留在原地是不公平的。 @ikegami 我明白了。那么不应该将通过提供的输入引用更改输入参数的“with_ref”添加到批次中吗?【参考方案3】:

我总是追求设计和清晰。我觉得一旦性能问题开始影响界面设计,就该重新设计了。 (对我而言,这通常意味着引入额外的层。)我发现您的最后讨论表明,由于所有选项都存在问题,因此必须付出很多。我不会进入@_

所以在这种情况下,我会选择按值返回

$line = handle_empty_lines( $line );

sub handle_empty_lines 
    defined $_[0] && $_[0] ne '' && return $_[0];
    return "\x21B5";

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

handle_empty_lines( \$line);

sub handle_empty_lines 
    defined $$_[0] && $$_[0] ne '' && return 1;
    $$_[0] = "\x21B5";

更常见的情况当然应该首先出现,而删除分支if 测试的优化会有所帮助,如下面的基准测试所示,但作用不大。

我会根据哪个更适合整体代码设计来选择。


我已将这两个添加到ikegami 发布的基准中,作为

sub micro 
    defined $_[0] && $_[0] ne '' && return $_[0];
    return "\x21B5";


sub microref 
    defined $$_[0] && $$_[0] ne '' && return 1;
    $$_[0] = "\x21B5";

添加这两个函数的ikegami 的基准测试的完整结果

用_ref microref 基线评价 by_ref micro inplace with_ref 1522762/s -- -13% -19% -34% -36% -43% 微参考 1742620/s 14% -- -8% -24% -26% -35% 基线 1890650/s 24% 8% -- -18% -20% -29% by_ref 2296676/s 51% 32% 21% -- -3% -14% 微 2360880/s 55% 35% 25% 3% -- -12% 就地 2680740/s 76% 54% 42% 17% 14% --

事实证明,具有按值返回的“mirco”优化版本运行良好。在我看来,这些结果意味着为了速度而做出艰难的设计决策是没有必要的。


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

用_ref microref 基线评价 by_ref micro inplace with_ref 1470954/s -- -16% -21% -34% -39% -55% 微参考 1742620/s 18% -- -6% -21% -27% -47% 基线 1854974/s 26% 6% -- -16% -23% -43% by_ref 2213644/s 50% 27% 19% -- -8% -32% 微 2399206/s 63% 38% 29% 8% -- -27% 就地 3267412/s 122% 87% 76% 48% 36% --

这证明了难以如此精细地估计实际使用中什么更好。实际程序中还有其他因素可能会影响这些因素。我仍然认为没有理由为速度设计而烦恼。

【讨论】:

你说的是你永远不会使用案例2(即使它会导致你的程序显着加速),因为不可能为这种情况编写可维护的代码? (所以你会为了维护者的舒适而牺牲用户对程序的体验) @HåkonHægland 嗯......很难做出如此明确的声明。但是什么是重要足够的呢?如果简单的函数调用会导致明显的减速,我认为绝对应该回到绘图板上。这不仅仅是方便 - 它是开发时间和错误。他们经历了设计中的小裂缝。 #2 的所有设计解决方案都是笨拙且危险的。第三个最少,但在名称中包含 how 并不是好的设计。此外,我还尝试过在wxWidgets GUI 中加速最简单的函数调用——这很棘手。很难估计得这么精细。 @HåkonHægland 我已将这两个函数添加到ikegami 的基准测试中。它似乎只落后 %14。让我知道将上面的推理添加到帖子中是否是个好主意。

以上是关于Perl 中带有输出参数的子例程的最佳实践命名约定的主要内容,如果未能解决你的问题,请参考以下文章

Perl 中左值子例程的用途是啥?

perl - 帮助修改代码以包含子例程的使用

在 Perl 脚本中隔离命名空间

如何从 unix shell 调用带有参数的子例程

九十SAP中ALV事件之四,事件子例程的参数

将参数传递给 Perl 子例程时,是不是会影响数据复制性能?